diff --git a/.github/workflows/vsix-release.yml b/.github/workflows/vsix-release.yml new file mode 100644 index 0000000..efc31c5 --- /dev/null +++ b/.github/workflows/vsix-release.yml @@ -0,0 +1,77 @@ +name: Build & Release VSIX + +on: + release: + types: [published] + workflow_dispatch: + inputs: + upload_to_release: + description: 'Upload VSIX to the latest GitHub release' + required: false + default: 'false' + type: boolean + +permissions: + contents: write + +jobs: + build-vsix: + name: Build VSIX + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + working-directory: vscode-extension + run: npm install + + - name: Lint extension source files + run: | + ERRORS=0 + for file in vscode-extension/extension.js vscode-extension/agent-bridge.mjs; do + if [ -f "$file" ]; then + if ! node --check "$file" 2>/tmp/lint-err.txt; then + echo "::error file=${file}::Syntax error in ${file}" + cat /tmp/lint-err.txt + ERRORS=$((ERRORS + 1)) + else + echo "::notice::${file} β€” OK" + fi + fi + done + if [ "$ERRORS" -gt 0 ]; then + echo "::error::${ERRORS} extension file(s) have syntax errors" + exit 1 + fi + + - name: Package VSIX + working-directory: vscode-extension + run: npm run package + + - name: Find VSIX file + id: find_vsix + working-directory: vscode-extension + run: | + VSIX_FILE=$(ls *.vsix | head -1) + echo "vsix_file=$VSIX_FILE" >> "$GITHUB_OUTPUT" + echo "vsix_path=vscode-extension/$VSIX_FILE" >> "$GITHUB_OUTPUT" + + - name: Upload VSIX as workflow artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.find_vsix.outputs.vsix_file }} + path: ${{ steps.find_vsix.outputs.vsix_path }} + retention-days: 30 + + - name: Upload VSIX to GitHub Release + if: github.event_name == 'release' || inputs.upload_to_release == 'true' + uses: softprops/action-gh-release@v2 + with: + files: ${{ steps.find_vsix.outputs.vsix_path }} + tag_name: ${{ github.event_name == 'release' && github.ref_name || '' }} diff --git a/.gitignore b/.gitignore index 9e57b6b..73bdadf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ dist/ build/ *.tgz +# Bundled v2 source (copied by prepackage script, not committed) +vscode-extension/v2/ + # Environment .env .env.local diff --git a/README.md b/README.md index 80f7a69..c8fb771 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,13 @@

- Tests + Tests Tools Commands npm License Nightly + VSCode

> **Automated Nightly Releases** β€” Open Claude Code automatically detects new [Claude Code](https://www.npmjs.com/package/@anthropic-ai/claude-code) releases, runs 903+ tests to verify zero regressions, and publishes verified builds with AI-powered discovery analysis. See [Releases](https://github.com/ruvnet/open-claude-code/releases) | [ADR-001](docs/adr/ADR-001-nightly-verified-release-pipeline.md) | [pi.ruv.io](https://pi.ruv.io) @@ -42,6 +43,59 @@ npx @ruvnet/open-claude-code "what files are in this directory?" --- +## πŸ–₯️ VSCode Extension + +A **Cursor-style AI coding assistant** built directly into VSCode β€” no terminal required. + +### Quick install (pre-built VSIX) + +The extension package is included in the repo and ready to install: + +```bash +code --install-extension vscode-extension/open-claude-code-1.4.0.vsix +``` + +Or use **Extensions β†’ … β†’ Install from VSIX…** and pick the file from the `vscode-extension/` folder. + +### Build from source + +```bash +cd vscode-extension +npm install +npm run package # prepackage β†’ package β†’ postpackage +code --install-extension open-claude-code-1.4.0.vsix +``` + +### Highlights + +- **Dedicated activity bar icon** β€” opens a full Cursor-style chat panel in the sidebar +- **`@claude` chat participant** β€” access Claude directly from VS Code's built-in Chat panel +- **Full tool access** β€” all 25+ agent tools (Read, Write, Edit, Bash, Glob, Grep, WebFetch, …) +- **Rich markdown + syntax highlighting** β€” code blocks with copy & Apply-to-file buttons +- **Copy whole answer** β€” ⎘ Copy button on every assistant reply copies the full response to the clipboard +- **Session memory** β€” the model remembers the **full conversation from the beginning** across VS Code restarts (Claude Premium-style); auto-saves after every response and restores on reopen +- **Chat history** β€” History button shows all past conversations (Cursor-style panel); "New" auto-saves the current session; sessions persist across VS Code restarts; up to 30 sessions are retained +- **β–Ά Resume** β€” any past session can be fully resumed: all messages are restored in the chat panel and the model's context is re-injected into the agent bridge +- **βš™ Settings shortcut** β€” gear button in the header opens the extension settings directly β€” no need to navigate through the marketplace +- **Streaming responses** β€” tokens arrive in real time with an animated cursor +- **Tool visualization** β€” collapsible cards showing each tool execution and result +- **`@file` context injection** β€” type `@filename` or click πŸ“„ to add a file to the prompt +- **`@git` context chip** β€” type `@git` or click the git button to inject current branch, changed files, and diff summary into the conversation +- **`@errors` context chip** β€” type `@errors` or click the ⚠ button to inject all workspace errors and warnings (VSCode diagnostics) into the conversation +- **`@openfiles` autocomplete** β€” type `@openfiles` to pick from your currently open editor tabs +- **Auto-attach active file** β€” toggle the πŸ”— button (or `openClaudeCode.autoAttachActiveFile` setting) to automatically include the active editor with every message +- **Context-full warning** β€” a banner appears when the context window exceeds 85%, with a quick "New chat" shortcut +- **Per-message response time** β€” each assistant reply shows how long it took to generate (visible on hover) +- **Ctrl/Cmd+Enter** β€” additional keyboard shortcut for sending a message +- **Multi-provider** β€” Anthropic Claude, OpenAI GPT, Google Gemini, NVIDIA NIM +- **Model & permission-mode selector** β€” switch model and mode directly from the UI +- **Session stats** β€” token count, cost estimate, and elapsed time always visible +- **Proactive workspace analysis** β€” the agent explores your project automatically before answering; never asks you to paste code + +See [`vscode-extension/README.md`](./vscode-extension/README.md) for the full setup and configuration guide. + +--- + ## 🧠 What Is This? **Open Claude Code** is a ground-up open source rebuild of Anthropic's [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code), informed by [ruDevolution's](https://github.com/ruvnet/rudevolution) AI-powered decompilation of the published npm package. @@ -301,8 +355,13 @@ AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... occ -m bedrock/claude-sonnet "he # Google Vertex GOOGLE_APPLICATION_CREDENTIALS=... occ -m vertex/claude-sonnet "hello" + +# NVIDIA NIM (kimi-k2.5, deepseek-r1, and other thinking models supported) +NVIDIA_API_KEY=nvapi-... occ -m kimi-k2.5 "hello" ``` +> **Note β€” NVIDIA models (Kimi K2.5, DeepSeek R1):** These models support **full tool-calling by default** β€” they can Read, Write, Bash, Grep, and run all 25+ agent tools exactly like Cursor or opencode. Set `NVIDIA_THINKING_MODE=true` (or toggle the `openClaudeCode.nvidiaThinkingMode` setting in the VSCode extension) to opt into extended reasoning mode; in that mode tools are replaced with a rich workspace snapshot injected into the system prompt (file tree + key file contents), since NVIDIA NIM does not allow tools and thinking simultaneously. + --- ## πŸ”— MCP Integration @@ -351,6 +410,81 @@ This is a **clean-room implementation** β€” no leaked source used. Architecture --- +## πŸ†• What's New + +### v1.4.0 β€” Context Injection, Auto-Attach & UX Improvements + +**Richer context injection** +- **`@git` chip** β€” type `@git` in the chat input (or click the new git toolbar button) to instantly inject the current branch name, `git status`, and `git diff --stat` as a context chip. The content is appended inline to your message so the model has full awareness of your working-tree state +- **`@errors` chip** β€” type `@errors` (or click the ⚠ toolbar button) to pull all workspace errors and warnings from the VSCode diagnostics API into the conversation, grouped by severity +- **`@openfiles` autocomplete** β€” type `@openfiles` to populate the `@` autocomplete dropdown with every file currently open in the editor, making it easy to add multiple files without typing paths +- **Auto-attach active file** β€” click the new πŸ”— toolbar button (or set `openClaudeCode.autoAttachActiveFile: true` in settings) to automatically include the currently active editor file with every message you send β€” no need to type `@` every time + +**UX improvements** +- **Context-full warning banner** β€” a visible warning appears above the input when context usage exceeds 85%, with a one-click "New chat" shortcut to start fresh before responses degrade +- **Per-message response time** β€” each assistant reply now shows how long it took to generate (e.g. `3.2s`) in the message header, visible on hover +- **Ctrl/Cmd+Enter** β€” added as a second keyboard shortcut for sending messages (in addition to plain Enter) +- **Consistent keyboard hint** β€” the input area now shows `Enter / Ctrl+Enter send` to surface the new shortcut + +### v1.3.1 β€” Bridge Stability & Session Memory Fixes + +- **Session memory loss fix** β€” agent bridge now auto-injects the full session history whenever a new bridge process is created (covers crashes, settings changes, and VS Code restarts) +- **Bridge queue stability** β€” message queue is drained reliably on bridge restart so in-flight requests are not lost +- **Max-turns UX hint** β€” when the agent loop hits its turn limit a visible `βš™ Max turns reached` notice is shown with instructions to continue + +### v1.3.0 β€” Session Memory (Claude Premium-style) + +**Session Memory algorithm** β€” the model now remembers the full conversation from the very beginning, just like Claude premium sessions: + +- **Auto-persist active session** β€” the current conversation is automatically saved to VS Code `globalState` after every response. If you close and reopen VS Code, your conversation is fully restored β€” messages in the chat panel **and** the model's context in the bridge subprocess +- **Session resume from history** β€” every past conversation in the History panel now has a **β–Ά Resume** button. Clicking it: + 1. Saves your current in-progress chat to history + 2. Restores all messages of the selected session in the main chat panel + 3. Re-injects the full conversation into the agent loop (`resume` command in `agent-bridge.mjs`) so the model has complete memory of everything said β€” no context lost +- **New `resume` bridge protocol** β€” `agent-bridge.mjs` accepts `{"type":"resume","messages":[…]}` which converts the stored UI message format back to the Anthropic/OpenAI API message format and sets it directly on the agent loop's state +- **Auto-clear on new chat** β€” starting a new conversation clears the persisted active session so the next restart begins fresh + +### v1.2.0 β€” Chat History, Settings Button & Copy Answer + +**New UI features** _(this PR)_ +- **βš™ Settings button** β€” a gear icon added to the chat header opens `openClaudeCode` settings directly; no more navigating through the Extensions marketplace +- **⎘ Copy whole answer** β€” each finalized assistant reply now has a `⎘ Copy` button in the message header; one click copies the full raw markdown response to the clipboard +- **Chat history panel (Cursor-style)** β€” clicking the new **History** header button opens a full-overlay panel listing all past sessions. Pressing **New** auto-saves the current conversation to history first. Sessions are persisted in VS Code `globalState` and survive restarts; up to 30 sessions are retained +- **Cursor-style session navigation** β€” click any session in the history list to view its messages read-only (with Copy buttons intact); a Back button returns to the session list + +**Fix: Proactive workspace analysis for all models** +- All models now receive a strong agentic system prompt declaring the workspace `cwd` and instructing them to explore files with LS / Glob / Read / Grep / Bash before answering β€” never asking the user to paste code +- New `buildWorkspaceSnapshot` helper recursively walks the workspace (skipping `node_modules`, `.git`, `dist`, etc.) and returns a compact indented file tree capped at 200 entries +- New `buildWorkspaceContent` helper reads key project files (README, package.json, entry points, etc.) and returns their contents for inline injection β€” capped at 64 KB total +- **Kimi K2.5 and DeepSeek R1 now use full tool-calling by default** β€” Read, Write, Bash, Grep, and all 25+ tools work exactly like Cursor or opencode; thinking/reasoning mode is an opt-in setting (`NVIDIA_THINKING_MODE=true` or `openClaudeCode.nvidiaThinkingMode` in VSCode settings) +- When thinking mode IS enabled a rich workspace snapshot (file tree + key file contents) is injected into the system prompt with a purpose-built thinking-model system prompt +- Extension model descriptions updated; version bumped to 1.2.0 + +### v1.1.0 β€” VSCode Extension & Bug Fixes + +**VSCode Extension β€” Cursor-style sidebar panel** _(PR #2)_ +- New dedicated activity bar icon with a full-screen Cursor-style chat panel +- Rich markdown rendering, syntax-highlighted code blocks, copy & Apply-to-file buttons +- Streaming responses with animated cursor, tool visualization cards, extended thinking blocks +- `@file` context injection, file picker, model/mode selector, session stats, and stop button +- Pre-built VSIX committed to `vscode-extension/open-claude-code-1.1.0.vsix` β€” install with one command + +**VSCode Extension β€” `@claude` Chat Participant** _(PR #1)_ +- New `vscode-extension/` package exposing the `v2/src` agent loop as a native VSCode Chat participant +- Long-lived `agent-bridge.mjs` subprocess keeps conversation history across turns +- API key stored in VSCode `SecretStorage` β€” never written to disk in plaintext +- `/clear` and `/model` slash commands; configurable permission mode, max turns, and tool visibility + +**Fix: NVIDIA NIM thinking models** _(PR #3)_ +- `kimi-k2.5`, `deepseek-r1`, and other NVIDIA thinking models no longer crash with HTTP 400 +- Tool array is now omitted for thinking models (the two parameters are mutually exclusive) +- Clean system prompt (without tool descriptions) is used for thinking-model calls + +**VSCode extension package now tracked in git** _(PR #4)_ +- `*.vsix` removed from `.gitignore` β€” the built package lives at `vscode-extension/open-claude-code-1.1.0.vsix` + +--- + ## πŸ“š Related - [ruDevolution](https://github.com/ruvnet/rudevolution) β€” AI-Powered JavaScript Decompiler diff --git a/scripts/last-known-claude-version.txt b/scripts/last-known-claude-version.txt index 7b71b36..6b81aaa 100644 --- a/scripts/last-known-claude-version.txt +++ b/scripts/last-known-claude-version.txt @@ -1 +1 @@ -2.1.112 +2.1.177 diff --git a/v2/src/config/env.mjs b/v2/src/config/env.mjs index 9ec0e15..265d3f0 100644 --- a/v2/src/config/env.mjs +++ b/v2/src/config/env.mjs @@ -17,6 +17,7 @@ export const ENV_SCHEMA = { OPENAI_BASE_URL: { type: 'string', default: 'https://api.openai.com/v1', description: 'OpenAI-compatible base URL' }, GOOGLE_API_KEY: { type: 'string', description: 'Google AI API key' }, GEMINI_API_KEY: { type: 'string', description: 'Alias for GOOGLE_API_KEY' }, + NVIDIA_API_KEY: { type: 'string', description: 'NVIDIA NIM API key (integrate.api.nvidia.com)' }, // Model Configuration CLAUDE_CODE_MAX_OUTPUT_TOKENS: { type: 'number', default: 16384, description: 'Max output tokens' }, diff --git a/v2/src/core/agent-loop.mjs b/v2/src/core/agent-loop.mjs index 36e7a3f..bee803e 100644 --- a/v2/src/core/agent-loop.mjs +++ b/v2/src/core/agent-loop.mjs @@ -4,9 +4,27 @@ */ import { streamResponse, accumulateStream } from './streaming.mjs'; import { ContextManager } from './context-manager.mjs'; -import { buildSystemPrompt } from './system-prompt.mjs'; +import { buildSystemPrompt, buildWorkspaceSnapshot, buildWorkspaceContent, buildThinkingModelSystemPrompt } from './system-prompt.mjs'; +import { isNvidiaModel } from './providers.mjs'; import fs from 'fs'; import path from 'path'; + +/** + * NVIDIA NIM models that CAN use chat_template_kwargs.thinking=true for + * extended reasoning β€” but only when NVIDIA_THINKING_MODE=true is set. + * + * By default (NVIDIA_THINKING_MODE unset / false) these models work in + * standard tool-calling mode: Read, Write, Bash, Grep, etc. all work. + * + * When NVIDIA_THINKING_MODE=true the thinking flag is added and tools are + * omitted (NVIDIA NIM rejects the combination), falling back to workspace + * snapshot injection. + */ +const NVIDIA_THINKING_CAPABLE_MODELS = new Set([ + 'moonshotai/kimi-k2.5', + 'deepseek-ai/deepseek-r1', +]); + export function createAgentLoop({ model, tools, permissions, settings, hooks }) { const contextManager = new ContextManager(settings.maxContextTokens || 180000); @@ -21,6 +39,7 @@ export function createAgentLoop({ model, tools, permissions, settings, hooks }) const state = { messages: [], systemPrompt: promptResult.full, + systemPromptStatic: promptResult.staticPrefix, // tool-free prefix for providers that don't use tools turnCount: 0, tokenUsage: { input: 0, output: 0 }, model, @@ -53,14 +72,16 @@ export function createAgentLoop({ model, tools, permissions, settings, hooks }) yield { type: 'stream_request_start', turn: state.turnCount }; - // Detect provider and call API - const provider = detectProvider(model); + // Detect provider and call API β€” read state.model so that model + // switches (via handleModelSwitch) take effect on the next turn. + const currentModel = state.model; + const provider = detectProvider(currentModel); let response; try { if (settings.stream !== false) { // Streaming mode - response = await callApiStreaming(provider, model, state, tools.list(), settings); + response = await callApiStreaming(provider, currentModel, state, tools.list(), settings); const collectedContent = []; let currentText = ''; let currentThinking = ''; @@ -87,7 +108,7 @@ export function createAgentLoop({ model, tools, permissions, settings, hooks }) response = response.accumulated; } else { // Non-streaming mode - response = await callApi(provider, model, state, tools.list(), settings); + response = await callApi(provider, currentModel, state, tools.list(), settings); } } catch (err) { yield { type: 'error', message: err.message }; @@ -207,17 +228,18 @@ export function createAgentLoop({ model, tools, permissions, settings, hooks }) function detectProvider(model) { if (model.startsWith('gpt-') || model.startsWith('o1') || model.startsWith('o3')) return 'openai'; if (model.startsWith('gemini')) return 'google'; + if (isNvidiaModel(model)) return 'nvidia'; return 'anthropic'; } async function callApi(provider, model, state, toolDefs, settings) { - const callers = { anthropic: callAnthropic, openai: callOpenAI, google: callGoogle }; + const callers = { anthropic: callAnthropic, openai: callOpenAI, google: callGoogle, nvidia: callNvidia }; const caller = callers[provider] || callers.anthropic; return caller(model, state, toolDefs, settings, false); } async function callApiStreaming(provider, model, state, toolDefs, settings) { - const callers = { anthropic: callAnthropic, openai: callOpenAI, google: callGoogle }; + const callers = { anthropic: callAnthropic, openai: callOpenAI, google: callGoogle, nvidia: callNvidia }; const caller = callers[provider] || callers.anthropic; return caller(model, state, toolDefs, settings, true); } @@ -365,6 +387,349 @@ async function callGoogle(model, state, toolDefs, settings, stream) { return convertGoogleResponse(data); } +/** + * NVIDIA NIM β€” OpenAI-compatible chat completions endpoint. + * + * Uses the same message format as OpenAI but targets + * https://integrate.api.nvidia.com/v1/chat/completions. + * + * For thinking-capable models (kimi-k2.5, deepseek-r1) the + * `chat_template_kwargs: { thinking: true }` parameter is added + * automatically so the model returns its reasoning trace. + * + * Streaming uses the standard OpenAI SSE format (data: {...} / data: [DONE]). + * The thinking content is surfaced as a "thinking" text block so the existing + * agent-loop thinking display works without modification. + */ +async function callNvidia(model, state, toolDefs, settings, stream) { + const apiKey = process.env.NVIDIA_API_KEY; + if (!apiKey) throw new Error('NVIDIA_API_KEY not set'); + + // Thinking mode is opt-in: only enabled when NVIDIA_THINKING_MODE=true. + // By default, capable models (kimi-k2.5, deepseek-r1) use standard + // function-calling mode β€” tools work exactly as in any other provider. + const thinkingEnabled = process.env.NVIDIA_THINKING_MODE === 'true'; + const supportsThinking = thinkingEnabled && NVIDIA_THINKING_CAPABLE_MODELS.has(model); + + // When thinking mode is active the tool-list suffix would be misleading + // (NVIDIA NIM rejects tools + thinking together), so swap in a special + // system prompt with a rich workspace snapshot instead. + let systemPrompt = state.systemPrompt; + if (supportsThinking) { + if (!state.systemPromptStatic) { + process.stderr.write('[open-claude-code] Warning: systemPromptStatic missing β€” falling back to full system prompt for ' + model + '\n'); + } + const base = state.systemPromptStatic || state.systemPrompt; + const workspaceContent = buildWorkspaceContent(process.cwd()); + systemPrompt = buildThinkingModelSystemPrompt(base, workspaceContent.summary); + } + const effectiveState = supportsThinking + ? { ...state, systemPrompt } + : state; + + // Build OpenAI-style messages + const messages = buildOpenAIMessages(effectiveState); + + const body = { + model, + messages, + max_tokens: settings.maxTokens || 16384, + temperature: 1.00, + top_p: 1.00, + stream: !!stream, + ...(supportsThinking && { + chat_template_kwargs: { thinking: true }, + }), + // Include tools unless thinking mode is active (NVIDIA NIM rejects + // the combination of chat_template_kwargs.thinking + tools). + ...(!supportsThinking && toolDefs.length > 0 && { + tools: toolDefs.map(t => ({ + type: 'function', + function: { name: t.name, description: t.description, parameters: t.input_schema }, + })), + }), + }; + + const res = await fetch('https://integrate.api.nvidia.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'Accept': stream ? 'text/event-stream' : 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + const err = await res.text(); + throw new Error(`NVIDIA API error ${res.status}: ${err}`); + } + + if (stream) { + // Stream with the generic OpenAI-style SSE parser + const collected = []; + const eventGenerator = async function* () { + for await (const event of streamOpenAIResponse(res)) { + collected.push(event); + yield event; + } + }; + return { + events: eventGenerator(), + get accumulated() { + return accumulateOpenAIStream(collected); + }, + }; + } + + const data = await res.json(); + return convertNvidiaResponse(data); +} + +/** + * Build an OpenAI-style messages array from agent loop state. + * Handles system prompt, plain-text turns, and tool_result turns. + */ +function buildOpenAIMessages(state) { + const messages = []; + if (state.systemPrompt) { + messages.push({ role: 'system', content: state.systemPrompt }); + } + for (const msg of state.messages) { + if (typeof msg.content === 'string') { + messages.push({ role: msg.role, content: msg.content }); + } else if (Array.isArray(msg.content)) { + for (const block of msg.content) { + if (block.type === 'tool_result') { + messages.push({ + role: 'tool', + tool_call_id: block.tool_use_id, + content: typeof block.content === 'string' + ? block.content + : JSON.stringify(block.content), + }); + } else if (block.type === 'text') { + messages.push({ role: msg.role, content: block.text }); + } + } + } + } + return messages; +} + +/** + * Convert a non-streaming NVIDIA response to the internal format. + * NVIDIA NIM returns the same shape as OpenAI, but may include a + * "thinking" field directly on the message for supported models. + */ +function convertNvidiaResponse(data) { + const choice = data.choices?.[0]; + if (!choice) throw new Error('No choices in NVIDIA response'); + + const content = []; + + // Some NVIDIA models surface thinking as a separate message field + const thinkingText = choice.message?.thinking || choice.message?.reasoning_content; + if (thinkingText) { + content.push({ type: 'thinking', thinking: thinkingText }); + } + + if (choice.message?.content) { + content.push({ type: 'text', text: choice.message.content }); + } + + if (choice.message?.tool_calls) { + for (const tc of choice.message.tool_calls) { + content.push({ + type: 'tool_use', + id: tc.id, + name: tc.function.name, + input: (() => { + try { return JSON.parse(tc.function.arguments || '{}'); } catch { return {}; } + })(), + }); + } + } + + return { + content, + stop_reason: choice.finish_reason === 'stop' ? 'end_turn' : (choice.finish_reason || 'end_turn'), + usage: { + input_tokens: data.usage?.prompt_tokens || 0, + output_tokens: data.usage?.completion_tokens || 0, + }, + }; +} + +/** + * Parse an OpenAI-style SSE stream into events. + * Each line has the format "data: {json}" or "data: [DONE]". + * Thinking content in delta is surfaced as a synthetic + * content_block_delta with type "thinking_delta" so the agent loop + * can display it without changes. + */ +async function* streamOpenAIResponse(response) { + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + let thinkingBuffer = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + + const lines = buffer.split('\n'); + buffer = lines.pop(); // keep incomplete last line + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed === ':') continue; + if (!trimmed.startsWith('data:')) continue; + + const raw = trimmed.slice(5).trim(); + if (raw === '[DONE]') { + yield { type: 'done' }; + return; + } + + let chunk; + try { chunk = JSON.parse(raw); } catch { continue; } + + // Surface API errors returned inside the SSE stream (HTTP 200 with error body) + if (chunk.error) { + throw new Error(chunk.error.message || JSON.stringify(chunk.error)); + } + + const delta = chunk.choices?.[0]?.delta; + if (!delta) continue; + + // Thinking token (deepseek-r1 / kimi-k2.5) + const thinkingDelta = delta.reasoning_content || delta.thinking; + if (thinkingDelta) { + thinkingBuffer += thinkingDelta; + yield { + type: 'content_block_delta', + delta: { type: 'thinking_delta', thinking: thinkingDelta }, + }; + } + + // Regular text content + if (delta.content) { + // If we have accumulated thinking, emit block boundaries once + if (thinkingBuffer) { + thinkingBuffer = ''; + } + yield { + type: 'content_block_delta', + delta: { type: 'text_delta', text: delta.content }, + }; + } + + // Tool calls + if (delta.tool_calls) { + for (const tc of delta.tool_calls) { + yield { + type: 'content_block_delta', + delta: { + type: 'tool_call_delta', + index: tc.index, + id: tc.id, + name: tc.function?.name, + partial_json: tc.function?.arguments || '', + }, + }; + } + } + + // Finish reason + const finishReason = chunk.choices?.[0]?.finish_reason; + if (finishReason) { + yield { + type: 'message_delta', + delta: { stop_reason: finishReason === 'stop' ? 'end_turn' : finishReason }, + }; + } + } + } + + // Flush remaining buffer + if (buffer.trim()) { + const trimmed = buffer.trim(); + if (trimmed.startsWith('data:')) { + const raw = trimmed.slice(5).trim(); + if (raw && raw !== '[DONE]') { + try { + const chunk = JSON.parse(raw); + const delta = chunk.choices?.[0]?.delta; + if (delta?.content) { + yield { + type: 'content_block_delta', + delta: { type: 'text_delta', text: delta.content }, + }; + } + } catch { + // ignore malformed final chunk + } + } + } + } + } finally { + reader.releaseLock(); + } +} + +/** + * Accumulate streamed OpenAI-style events into an internal message object. + */ +function accumulateOpenAIStream(events) { + const message = { + content: [], + stop_reason: null, + usage: { input_tokens: 0, output_tokens: 0 }, + }; + + let textContent = ''; + let thinkingContent = ''; + const toolCallMap = {}; // index -> { id, name, arguments } + + for (const event of events) { + if (event.type === 'content_block_delta') { + if (event.delta?.type === 'thinking_delta') { + thinkingContent += event.delta.thinking || ''; + } else if (event.delta?.type === 'text_delta') { + textContent += event.delta.text || ''; + } else if (event.delta?.type === 'tool_call_delta') { + const idx = event.delta.index ?? 0; + if (!toolCallMap[idx]) { + toolCallMap[idx] = { id: event.delta.id || `call_${idx}`, name: event.delta.name || '', arguments: '' }; + } + if (event.delta.name) toolCallMap[idx].name = event.delta.name; + if (event.delta.id) toolCallMap[idx].id = event.delta.id; + toolCallMap[idx].arguments += event.delta.partial_json || ''; + } + } else if (event.type === 'message_delta') { + if (event.delta?.stop_reason) message.stop_reason = event.delta.stop_reason; + } + } + + if (thinkingContent) { + message.content.push({ type: 'thinking', thinking: thinkingContent }); + } + if (textContent) { + message.content.push({ type: 'text', text: textContent }); + } + for (const tc of Object.values(toolCallMap)) { + let input = {}; + try { input = JSON.parse(tc.arguments || '{}'); } catch { input = {}; } + message.content.push({ type: 'tool_use', id: tc.id, name: tc.name, input }); + } + + if (!message.stop_reason) message.stop_reason = 'end_turn'; + return message; +} + function convertOpenAIResponse(data) { const choice = data.choices?.[0]; if (!choice) throw new Error('No choices in OpenAI response'); diff --git a/v2/src/core/providers.mjs b/v2/src/core/providers.mjs index 0af0aef..f3edbdd 100644 --- a/v2/src/core/providers.mjs +++ b/v2/src/core/providers.mjs @@ -1,7 +1,7 @@ /** * Multi-Provider β€” unified provider config and request/response transforms. * - * Supports: Anthropic, OpenAI, Google, Bedrock (stub), Vertex (stub). + * Supports: Anthropic, OpenAI, Google, NVIDIA, Bedrock (stub), Vertex (stub). * Each provider defines endpoint, auth headers, and optional transforms. */ @@ -140,6 +140,47 @@ const PROVIDERS = { }, }, + /** + * NVIDIA NIM β€” OpenAI-compatible endpoint hosted at integrate.api.nvidia.com. + * + * Supported models include: + * moonshotai/kimi-k2.5 β€” Kimi K2.5 with extended thinking + * nvidia/llama-3.1-nemotron-70b-instruct + * meta/llama-3.1-405b-instruct + * meta/llama-3.3-70b-instruct + * mistralai/mistral-large-2-instruct + * mistralai/mixtral-8x22b-instruct-v0.1 + * google/gemma-3-27b-it + * deepseek-ai/deepseek-r1 + * + * Auth: Bearer token via NVIDIA_API_KEY environment variable. + * Endpoint: https://integrate.api.nvidia.com/v1/chat/completions + * Streaming: OpenAI-style SSE (data: {...} lines ending with data: [DONE]). + */ + nvidia: { + name: 'NVIDIA NIM', + endpoint: 'https://integrate.api.nvidia.com/v1/chat/completions', + envKey: 'NVIDIA_API_KEY', + authHeader(key) { + return { + 'Authorization': `Bearer ${key}`, + 'Content-Type': 'application/json', + }; + }, + models: [ + 'moonshotai/kimi-k2.5', + 'nvidia/llama-3.1-nemotron-70b-instruct', + 'meta/llama-3.1-405b-instruct', + 'meta/llama-3.3-70b-instruct', + 'mistralai/mistral-large-2-instruct', + 'mistralai/mixtral-8x22b-instruct-v0.1', + 'google/gemma-3-27b-it', + 'deepseek-ai/deepseek-r1', + ], + /** Models that support extended thinking via chat_template_kwargs */ + thinkingModels: ['moonshotai/kimi-k2.5', 'deepseek-ai/deepseek-r1'], + }, + bedrock: { name: 'AWS Bedrock', endpoint: null, // Dynamic based on region @@ -178,9 +219,25 @@ export function getProvider(model) { if (model.startsWith('claude') || model.startsWith('anthropic')) return PROVIDERS.anthropic; if (model.startsWith('gpt') || model.startsWith('o1') || model.startsWith('o3')) return PROVIDERS.openai; if (model.startsWith('gemini')) return PROVIDERS.google; + // NVIDIA-hosted models use a namespaced format: "publisher/model-name" + if (isNvidiaModel(model)) return PROVIDERS.nvidia; return PROVIDERS.anthropic; // default } +/** + * Return true when the model ID belongs to the NVIDIA NIM catalogue. + * NVIDIA models use the format "publisher/model-name". + */ +export function isNvidiaModel(model) { + if (!model || !model.includes('/')) return false; + const [publisher] = model.split('/'); + const nvidiaPublishers = new Set([ + 'moonshotai', 'nvidia', 'meta', 'mistralai', 'google', + 'deepseek-ai', 'microsoft', 'qwen', 'baichuan-inc', + ]); + return nvidiaPublishers.has(publisher); +} + /** * Get a provider by name. * @param {string} name diff --git a/v2/src/core/system-prompt.mjs b/v2/src/core/system-prompt.mjs index a748cac..e68087a 100644 --- a/v2/src/core/system-prompt.mjs +++ b/v2/src/core/system-prompt.mjs @@ -6,11 +6,192 @@ * - Merges in order (global -> project -> local) * - Splits at cache boundary (static prefix cached, dynamic suffix not) * - Includes tool schemas in the system prompt + * - Exports buildWorkspaceSnapshot for injecting a file-tree into prompts + * - Exports buildWorkspaceContent for injecting key file contents into prompts + * (used for thinking models that cannot make live tool calls) */ import fs from 'fs'; import path from 'path'; import os from 'os'; +// Directories to skip when building the workspace snapshot. +const SNAPSHOT_EXCLUDE = new Set([ + 'node_modules', '.git', 'dist', 'build', 'out', '.next', '.nuxt', + '__pycache__', '.cache', 'coverage', '.nyc_output', '.turbo', +]); + +// Dot-files/dirs are hidden by default; these are shown because they are +// commonly checked into source control and useful for project analysis. +const SNAPSHOT_INCLUDE_DOTFILES = new Set([ + '.env.example', '.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml', + '.prettierrc', '.prettierrc.js', '.prettierrc.json', '.prettierrc.yml', + '.babelrc', '.babelrc.js', '.gitignore', '.dockerignore', '.editorconfig', +]); + +/** + * Build a compact, indented directory tree for a workspace. + * + * The tree is capped at `maxFiles` entries so it never bloats the prompt. + * Directories that match SNAPSHOT_EXCLUDE are skipped entirely. + * Any I/O error returns an empty string gracefully. + * + * @param {string} [cwd] - root directory to scan (defaults to process.cwd()) + * @param {number} [maxFiles=200] - maximum number of entries to include + * @returns {string} indented tree string, or '' on error / empty workspace + */ +export function buildWorkspaceSnapshot(cwd = process.cwd(), maxFiles = 200) { + const lines = []; + let count = 0; + + function walk(dir, indent) { + if (count >= maxFiles) return; + let entries; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + // Directories first, then files, both sorted alphabetically + entries.sort((a, b) => { + if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1; + return a.name.localeCompare(b.name); + }); + for (const entry of entries) { + if (count >= maxFiles) { + lines.push(indent + '… (truncated)'); + return; + } + if (SNAPSHOT_EXCLUDE.has(entry.name)) continue; + if (entry.name.startsWith('.') && !SNAPSHOT_INCLUDE_DOTFILES.has(entry.name)) continue; + lines.push(indent + entry.name + (entry.isDirectory() ? '/' : '')); + count++; + if (entry.isDirectory()) { + walk(path.join(dir, entry.name), indent + ' '); + } + } + } + + try { + walk(path.resolve(cwd), ''); + } catch { + return ''; + } + + return lines.join('\n'); +} + +// Priority-ordered list of project meta/config files to read for thinking models. +// These give the model the most structural insight per token spent. +const CONTENT_PRIORITY_FILES = [ + // Documentation + 'README.md', 'readme.md', 'README.txt', + // Package / dependency manifests + 'package.json', 'Cargo.toml', 'pyproject.toml', 'setup.py', 'setup.cfg', + 'go.mod', 'pom.xml', 'build.gradle', 'composer.json', 'Gemfile', + // Entry points + 'index.js', 'index.mjs', 'index.ts', 'main.js', 'main.mjs', 'main.ts', + 'main.py', '__main__.py', 'app.py', 'app.js', 'app.ts', + 'src/index.js', 'src/index.mjs', 'src/index.ts', + 'src/main.js', 'src/main.mjs', 'src/main.ts', 'src/main.py', + // Config + 'CLAUDE.md', '.claude/CLAUDE.md', + 'tsconfig.json', '.eslintrc.json', '.prettierrc.json', + 'Makefile', 'Dockerfile', +]; + +/** + * Build a rich workspace context string that includes: + * 1. The compact file-tree snapshot (always) + * 2. Contents of high-value project files (README, package.json, entry points, etc.) + * + * This is intended for thinking models (e.g. Kimi K2.5, DeepSeek R1) that cannot + * make live tool calls. By providing actual file contents up front, the model can + * give accurate, project-specific answers without needing tool access. + * + * @param {string} [cwd] - workspace root (defaults to process.cwd()) + * @param {object} [opts] + * @param {number} [opts.maxFileBytes=8192] - max bytes to include per file + * @param {number} [opts.maxTotalBytes=65536] - hard cap on total injected content + * @returns {{ tree: string, files: Array<{path: string, content: string}>, summary: string }} + */ +export function buildWorkspaceContent(cwd = process.cwd(), opts = {}) { + const { maxFileBytes = 8192, maxTotalBytes = 65536 } = opts; + const root = path.resolve(cwd); + + // 1. Build the file tree + const tree = buildWorkspaceSnapshot(root); + + // 2. Collect priority file contents + const files = []; + let totalBytes = 0; + + for (const rel of CONTENT_PRIORITY_FILES) { + if (totalBytes >= maxTotalBytes) break; + const abs = path.join(root, rel); + if (!fs.existsSync(abs)) continue; + try { + const stat = fs.statSync(abs); + if (!stat.isFile()) continue; + let content = fs.readFileSync(abs, 'utf-8'); + const originalLength = content.length; + if (originalLength > maxFileBytes) { + content = content.slice(0, maxFileBytes) + `\n... (truncated β€” ${originalLength - maxFileBytes} more bytes)`; + } + const contentLength = content.length; + if (totalBytes + contentLength > maxTotalBytes) break; + files.push({ path: rel, content }); + totalBytes += contentLength; + } catch { /* skip unreadable */ } + } + + // 3. Build the formatted summary string + const parts = []; + + if (tree) { + parts.push('## Workspace file structure\n\n```\n' + tree + '\n```'); + } + + for (const { path: filePath, content } of files) { + parts.push(`## File: ${filePath}\n\n\`\`\`\n${content}\n\`\`\``); + } + + return { + tree, + files, + summary: parts.join('\n\n'), + }; +} + +/** + * Build the system prompt text for thinking models (Kimi K2.5, DeepSeek R1). + * + * Unlike the standard system prompt (which instructs the model to call tools), + * this version acknowledges that no tools are available and instead points the + * model to the pre-injected workspace content below the prompt. + * + * @param {string} staticBase - the static prefix of the normal system prompt + * @param {string} workspaceSummary - output of buildWorkspaceContent().summary + * @returns {string} + */ +export function buildThinkingModelSystemPrompt(staticBase, workspaceSummary) { + const header = [ + `You are an AI coding assistant with access to a snapshot of the user's workspace.`, + ``, + `IMPORTANT: You are operating in thinking mode. Live tool calls (Read, Write, Bash, Grep, etc.)`, + `are NOT available in this session. Instead, a snapshot of the key project files and the`, + `complete workspace file tree has been embedded below. Use this snapshot to answer questions`, + `accurately and in full β€” never say you cannot see the project or ask the user to paste code.`, + ``, + `When the snapshot does not contain a file the user mentions, say so clearly and offer to`, + `reason from the available context.`, + ].join('\n'); + + const parts = [header]; + if (staticBase) parts.push(staticBase); + if (workspaceSummary) parts.push('---\n\n# Workspace snapshot (read-only)\n\n' + workspaceSummary); + return parts.join('\n\n'); +} + /** * Load all CLAUDE.md files and merge them in order. * @param {string} [cwd] - current working directory @@ -67,7 +248,24 @@ export function buildSystemPrompt({ cwd, tools, override, addDirs } = {}) { return { staticPrefix: override, dynamicSuffix: '', full: override }; } - const parts = ['You are an AI coding assistant.']; + const workspaceRoot = path.resolve(cwd || process.cwd()); + const basePreamble = [ + `You are an AI coding assistant with direct access to the user's workspace on disk.`, + `Current working directory: ${workspaceRoot}`, + ``, + `## Workspace exploration rules`, + ``, + `- When the user asks about their project, code, or files, ALWAYS use your tools to`, + ` explore the workspace first β€” do NOT ask the user to paste or share anything.`, + `- Use LS / Glob to discover files, Read to inspect their contents, Grep to search`, + ` for patterns, and Bash to run commands (tests, builds, linters, git log, etc.).`, + `- Start broad (list the root directory) then drill into relevant subdirectories.`, + `- Prefer reading the actual source over guessing from file names alone.`, + `- Never say "I don't see any files" or ask the user to share code β€” you can read`, + ` the workspace directly with your tools.`, + ].join('\n'); + + const parts = [basePreamble]; // Load CLAUDE.md files const mdFiles = loadClaudeMdFiles(cwd); diff --git a/v2/src/permissions/checker.mjs b/v2/src/permissions/checker.mjs index c34d67b..4aa1b2d 100644 --- a/v2/src/permissions/checker.mjs +++ b/v2/src/permissions/checker.mjs @@ -42,7 +42,7 @@ export function createPermissionChecker(config = {}) { return true; case 'auto': return true; // AI decides case 'dontAsk': return false; // deny everything not pre-approved - case 'plan': return toolName === 'Read' || toolName === 'Glob' || toolName === 'Grep'; + case 'plan': return ['Read', 'Glob', 'Grep', 'LS', 'WebFetch', 'WebSearch', 'ToolSearch'].includes(toolName); case 'default': default: // In default mode, safe tools pass through diff --git a/v2/test/test.mjs b/v2/test/test.mjs index ca1ff08..f4ce346 100644 --- a/v2/test/test.mjs +++ b/v2/test/test.mjs @@ -1148,10 +1148,13 @@ assert(foundTruncated, 'Micro-compaction truncates old tool results'); section('Phase 2: System Prompt'); -import { buildSystemPrompt, loadClaudeMdFiles, toCacheBlocks } from '../src/core/system-prompt.mjs'; +import { buildSystemPrompt, loadClaudeMdFiles, toCacheBlocks, buildWorkspaceSnapshot } from '../src/core/system-prompt.mjs'; const prompt = buildSystemPrompt({ cwd: '/tmp' }); assertIncludes(prompt.full, 'AI coding assistant', 'System prompt has base text'); +assertIncludes(prompt.full, 'Current working directory:', 'System prompt declares cwd'); +assertIncludes(prompt.full, 'Workspace exploration rules', 'System prompt has exploration rules'); +assertIncludes(prompt.full, 'do NOT ask the user to paste', 'System prompt forbids asking user to paste code'); assertType(prompt.staticPrefix, 'string', 'Has static prefix'); assertType(prompt.dynamicSuffix, 'string', 'Has dynamic suffix'); @@ -1169,6 +1172,20 @@ assertEqual(blocks.length, 2, 'Two cache blocks'); assertEqual(blocks[0].cache_control.type, 'ephemeral', 'Static block cached'); assert(blocks[1].cache_control === undefined, 'Dynamic block not cached'); +// buildWorkspaceSnapshot +assertType(buildWorkspaceSnapshot, 'function', 'buildWorkspaceSnapshot is exported'); +const snapshot = buildWorkspaceSnapshot('/tmp'); +assertType(snapshot, 'string', 'Snapshot returns a string'); +// Non-existent path returns empty string gracefully +const snapshotBad = buildWorkspaceSnapshot('/nonexistent_path_xyz_12345'); +assertEqual(snapshotBad, '', 'Non-existent path returns empty string'); +// Snapshot respects maxFiles cap: with maxFiles=1, we expect at most 1 entry +// plus a possible "… (truncated)" line = 2 non-blank lines maximum. Allow +// up to 3 to accommodate edge cases where /tmp itself is almost empty. +const snapshotCapped = buildWorkspaceSnapshot('/tmp', 1); +const snapshotLines = snapshotCapped.split('\n').filter(l => l.trim()); +assert(snapshotLines.length <= 3, 'Snapshot respects maxFiles cap (at most 1 entry + truncation line)'); + // ---------- Phase 3: CLI, UI, Commands Tests ---------- section('Phase 3: CLI Args (full flags)'); diff --git a/vscode-extension/.vscodeignore b/vscode-extension/.vscodeignore new file mode 100644 index 0000000..c45405b --- /dev/null +++ b/vscode-extension/.vscodeignore @@ -0,0 +1,4 @@ +.vscode/** +node_modules/** +.gitignore +*.vsix diff --git a/vscode-extension/LICENSE b/vscode-extension/LICENSE new file mode 100644 index 0000000..2dd524a --- /dev/null +++ b/vscode-extension/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 rUv + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vscode-extension/README.md b/vscode-extension/README.md new file mode 100644 index 0000000..59d6d03 --- /dev/null +++ b/vscode-extension/README.md @@ -0,0 +1,271 @@ +# Open Claude Code β€” VSCode Extension + +A **Cursor-style AI coding assistant** built directly into VSCode β€” no terminal required. + +--- + +## Features + +### πŸ—‚οΈ Proactive workspace analysis (new in v1.2) +- **Automatic workspace exploration** β€” before answering questions the agent scans your project with LS, Glob, Read, and Grep instead of asking you to paste code +- **Rich workspace injection for thinking models** β€” Kimi K2.5 and DeepSeek R1 receive a full workspace snapshot (file tree + key file contents: README, package.json, entry points, etc.) directly in their system prompt, so they have genuine project understanding even though NVIDIA NIM prevents live tool calls during thinking mode +- **Never "I can't see your files"** β€” the system prompt explicitly forbids asking you to share code; the agent reads files directly or uses the pre-injected snapshot + +### πŸ–₯️ Cursor-style Sidebar Panel (new in v1.1) +- **Dedicated activity bar icon** β€” opens a full chat panel in the VS Code sidebar +- **βš™ Settings button** β€” gear icon in the header opens the `openClaudeCode` settings section directly β€” no navigating through the Extensions marketplace +- **Chat history** β€” the **History** header button opens a Cursor-style panel listing all past sessions; sessions auto-save when you click **New** and persist across VS Code restarts (up to 30 sessions stored in `globalState`) +- **Copy whole answer** β€” ⎘ Copy button on every assistant reply copies the full response to the clipboard (visible on hover) +- **Rich markdown rendering** β€” headers, tables, bold/italic, blockquotes +- **Syntax-highlighted code blocks** β€” JavaScript, TypeScript, Python, Go, Rust, JSON, Bash and more +- **Copy button on every code block** β€” one click to copy to clipboard +- **Apply to file** β€” apply AI-suggested code directly to the active editor or pick a file +- **Streaming responses** β€” see tokens arrive in real-time with animated cursor +- **Tool visualization** β€” collapsible cards showing each tool execution and result +- **Extended thinking** β€” expandable thinking blocks when the model reasons +- **@file context** β€” type `@filename` in the input to inject file contents into the prompt +- **File picker** β€” add any workspace file to context with the πŸ“„ button +- **Model & mode selector** β€” switch model and permission mode directly from the UI +- **Session stats** β€” token count, cost estimate, and elapsed time always visible +- **Stop button** β€” cancel generation at any time +- **New conversation** β€” clear history with one click (auto-saves the current session) + +### πŸ’¬ `@claude` Chat Participant (VSCode built-in chat) +- Ask questions, request code changes, and run agentic tools without leaving the editor +- **Full tool access** β€” the same 25+ tools as the CLI (Read, Write, Edit, Bash, Glob, Grep, WebFetch, …) +- **Multi-provider** β€” Anthropic Claude, OpenAI GPT, Google Gemini, NVIDIA NIM +- **Conversation memory** β€” history is maintained across turns in the same VS Code session +- **Slash commands** β€” `/clear` to reset, `/model` to switch models mid-session +- **Configurable permission mode** β€” control how aggressively the agent modifies your files + +--- + +## Requirements + +- **VSCode 1.90+** (Chat API required) +- **Node.js 18+** on your PATH +- An **API key** for at least one supported provider (Anthropic, OpenAI, Google, or NVIDIA) + +--- + +## Quick Start (already have VSCode installed) + +1. **Install the extension** β€” load from VSIX or press F5 in the repo (see [Installation](#installation) below). +2. **Open the chat panel** β€” click the **✦** icon in the Activity Bar (left sidebar). +3. **Follow the setup guide** β€” the welcome screen walks you through getting and entering an API key. + + Or run **Open Claude Code: Set API Key** from the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`). + +4. **Start chatting** β€” type a message and press **Enter**. + +> The first time you send a message the extension starts a Node.js agent subprocess in your workspace directory. This may take a second or two. + +--- + +## Installation + +### Option A β€” from VSIX (recommended for local use) + +1. Install [`vsce`](https://github.com/microsoft/vscode-vsce) if you haven't already: + ```bash + npm install -g @vscode/vsce + ``` +2. From the `vscode-extension/` directory, build the VSIX: + ```bash + cd vscode-extension + npm install + npm run package + ``` + The `prepackage` step automatically copies the `v2/src` engine into the + extension bundle so all functionality is available after installation. +3. Install it in VSCode: + ```bash + code --install-extension open-claude-code-1.2.0.vsix + ``` + Or use **Extensions β†’ … β†’ Install from VSIX…** in the VSCode UI. + +### Option B β€” load as an unpacked extension (development) + +1. Open the repo in VSCode. +2. Press **F5** to launch a new Extension Development Host window. +3. In the new window, open any project folder and use `@claude` in the Chat panel. + +--- + +## Setup + +### API key β€” which provider? + +| Provider | Where to get a key | Environment variable | +|----------|--------------------|----------------------| +| **Anthropic** (recommended) | [console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys) | `ANTHROPIC_API_KEY` | +| **OpenAI** | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) | `OPENAI_API_KEY` | +| **Google** | [aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey) | `GOOGLE_API_KEY` or `GEMINI_API_KEY` | +| **NVIDIA NIM** | [integrate.api.nvidia.com](https://integrate.api.nvidia.com) | `NVIDIA_API_KEY` | + +### Option 1 β€” Command Palette (recommended) + +Run **Open Claude Code: Set API Key** (`Ctrl+Shift+P` / `Cmd+Shift+P`). + +The key is stored securely in VSCode's [SecretStorage](https://code.visualstudio.com/api/references/vscode-api#SecretStorage) β€” it is never written to disk in plaintext. + +### Option 2 β€” VS Code Settings (NVIDIA key only) + +Open Settings (`Ctrl+,`), search for `openClaudeCode.nvidiaApiKey`, and paste your `nvapi-...` key. + +### Option 3 β€” Environment variable + +Set the variable before launching VSCode: +```bash +export ANTHROPIC_API_KEY=sk-ant-... +code . +``` + +--- + +## Usage + +### Sidebar chat panel (recommended) + +Click the **✦ Claude Code** icon in the activity bar (left sidebar) to open the chat panel, then: +- Type your message and press **Enter** to send (Shift+Enter for a new line) +- Use `@filename` to inject file contents into the prompt +- Click **πŸ“„** to pick a file from the workspace +- Click **History** to browse and reopen past conversations (sessions auto-save when you click **New**) +- Click **New** to start a fresh conversation (saves the current session first) +- Click **βš™** (gear) to open the extension settings directly from the chat panel +- Use the **Model** and **Mode** dropdowns to configure the agent + +When Claude suggests code, every code block has: +- **Copy** β€” copy the code to clipboard +- **Apply to file…** β€” apply the code to the active editor (or pick a file) + +Every assistant message also has a **⎘ Copy** button (visible on hover) that copies the full response text. + +### @claude chat participant (VSCode built-in chat) + +Open the **Chat** panel (`Ctrl+Alt+I` / `Cmd+Alt+I`) and type: + +``` +@claude explain this codebase +@claude fix the bug in src/server.js +@claude write unit tests for utils.ts +@claude what does the `loadSettings` function do? +``` + +### Slash commands + +| Command | Description | +|---------|-------------| +| `@claude /clear` | Reset conversation history | +| `@claude /model claude-opus-4-6` | Switch model for this session | + +### Command Palette commands + +| Command | Description | +|---------|-------------| +| **Open Claude Code: Set API Key** | Store your API key securely | +| **Open Claude Code: Clear Session** | Reset conversation history | +| **Open Claude Code: Show Status** | Show bridge status, model, and key info | +| **Open Claude Code: Open Chat Panel** | Focus the sidebar chat panel | +| **Open Claude Code: Apply Code to Active File** | Paste code into the active editor | + +--- + +## Configuration + +Open **Settings** (`Ctrl+,`) and search for `openClaudeCode`: + +| Setting | Default | Description | +|---------|---------|-------------| +| `openClaudeCode.model` | `claude-sonnet-4-6` | AI model to use | +| `openClaudeCode.nvidiaApiKey` | _(empty)_ | NVIDIA NIM API key (`nvapi-...`) | +| `openClaudeCode.nvidiaThinkingMode` | `false` | Enable extended reasoning mode for Kimi K2.5 / DeepSeek R1 (disables live tools) | +| `openClaudeCode.permissionMode` | `default` | How the agent handles file/shell permissions | +| `openClaudeCode.maxTurns` | `20` | Maximum agentic tool-use turns per request | +| `openClaudeCode.showToolOutput` | `true` | Show tool progress and results in chat | +| `openClaudeCode.enableWebviewPanel` | `true` | Show the Cursor-style sidebar chat panel | + +### Permission modes + +| Mode | Description | +|------|-------------| +| `default` | Ask before each tool use (safest) | +| `auto` | Automatically approve safe operations | +| `plan` | Read-only β€” no file writes or shell commands | +| `acceptEdits` | Approve file edits without prompting | +| `bypassPermissions` | Skip all permission checks | + +--- + +## How it works + +The extension spawns **`agent-bridge.mjs`** as a long-lived Node.js subprocess in your workspace directory. The bridge imports the Open Claude Code agent loop from `../v2/src/` and speaks a simple newline-delimited JSON protocol over stdin/stdout. + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ VS Code Extension Host β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ ClaudeCodeViewProvider β”‚ β”‚ ChatParticipant β”‚ β”‚ +β”‚ β”‚ (Cursor-style sidebar) β”‚ β”‚ (@claude) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ postMessage / onMessage β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ child_process.spawn β”‚ +└──────────────────────────┼──────────────────────────-β”˜ + β–Ό + agent-bridge.mjs (ESM Node.js subprocess) + β”‚ createAgentLoop + 25 tools + β–Ό + Anthropic / OpenAI / Google API +``` + +The subprocess persists across chat turns so the agent's conversation history is maintained. Clicking **New** (or running the **Clear Session** command) resets the history. + +--- + +### NVIDIA models β€” Kimi K2.5 and DeepSeek R1 + +These models are supported in two modes. **Tool-calling mode is the default** and works exactly like Cursor or opencode β€” the model reads files, runs Bash, greps for patterns, and edits code like any other agent model. + +#### Default: full tool-calling mode + +Just select **moonshotai/kimi-k2.5** or **deepseek-ai/deepseek-r1**, enter your `NVIDIA_API_KEY` in Settings, and start chatting. The model has access to all tools: Read, Write, Edit, Bash, Glob, Grep, and more. + +#### Optional: extended thinking (reasoning) mode + +If you want the model to show its step-by-step reasoning, enable the **nvidiaThinkingMode** setting: + +1. Open Settings (`Ctrl+,`), search for `openClaudeCode.nvidiaThinkingMode`, and set it to **true**. +2. Run **Open Claude Code: Clear Session** so the bridge restarts with the new setting. + +In thinking mode the NVIDIA NIM API does not accept live tool calls alongside the thinking flag, so tools are replaced with a rich workspace snapshot injected into the system prompt: +- **File tree** β€” the full indented directory structure of your project +- **Key file contents** β€” README, package.json/Cargo.toml/pyproject.toml, main entry points, and other high-value project files (up to ~64 KB total) + +| Mode | Tools | Thinking trace | Best for | +|------|-------|---------------|---------| +| Tool-calling (default) | βœ… Full access | ❌ | Multi-step coding tasks, file edits, grep, bash | +| Thinking (`nvidiaThinkingMode: true`) | ❌ | βœ… | Deep analysis, architecture review, explanations | + +--- + +## Troubleshooting + +**"Failed to start agent"** +- Make sure you have set your API key (see Setup above). +- Check the **Output** panel β†’ **Open Claude Code** channel for subprocess stderr logs. + +**The agent hangs or doesn't respond** +- Run **Open Claude Code: Clear Session** to restart the bridge. +- Check that Node.js 18+ is on your PATH: `node --version`. + +**Tool calls fail with permission errors** +- Change `openClaudeCode.permissionMode` to `auto` or `acceptEdits` in settings. + +--- + +## License + +MIT diff --git a/vscode-extension/agent-bridge.mjs b/vscode-extension/agent-bridge.mjs new file mode 100644 index 0000000..260708e --- /dev/null +++ b/vscode-extension/agent-bridge.mjs @@ -0,0 +1,213 @@ +#!/usr/bin/env node +/** + * agent-bridge.mjs + * + * Long-lived subprocess that runs the Open Claude Code agent loop and + * communicates with the VSCode extension over stdin/stdout using + * newline-delimited JSON (ndjson). + * + * Protocol (stdin β†’ bridge): + * {"type":"run", "message":""} + * {"type":"reset"} + * {"type":"model", "model":""} + * + * Protocol (bridge β†’ stdout): + * {"type":"stream_event", "text":"..."} + * {"type":"assistant", "content":"..."} + * {"type":"tool_progress", "tool":"Bash","status":"running"} + * {"type":"result", "tool":"Bash","result":"..."} + * {"type":"thinking", "text":"..."} + * {"type":"compaction", "count":1} + * {"type":"hookPermissionResult", "tool":"...","allowed":false} + * {"type":"stop", "reason":"end_turn"} + * {"type":"error", "message":"..."} + * {"type":"ready"} + * + * Environment variables consumed: + * ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, GEMINI_API_KEY, + * NVIDIA_API_KEY + * ANTHROPIC_MODEL β€” initial model override + * CLAUDE_CODE_PERMISSION_MODE + * CLAUDE_CODE_MAX_TURNS + * + * v2/src resolution: + * When installed from a VSIX the v2 source is bundled inside the extension + * directory as ./v2/src (copied by the prepackage script). + * When running from source (development / F5) ../v2/src is used instead. + */ + +import { fileURLToPath, pathToFileURL } from 'url'; +import path from 'path'; +import { existsSync } from 'fs'; +import readline from 'readline'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// --------------------------------------------------------------------------- +// Locate v2/src at runtime so the bridge works in both packaged and dev modes. +// --------------------------------------------------------------------------- +function findV2Src() { + const candidates = [ + path.join(__dirname, 'v2', 'src'), // installed from VSIX (bundled) + path.join(__dirname, '..', 'v2', 'src'), // development β€” sibling directory + ]; + for (const candidate of candidates) { + if (existsSync(path.join(candidate, 'core', 'agent-loop.mjs'))) { + return candidate; + } + } + throw new Error( + 'Cannot locate v2/src. Checked:\n' + + candidates.map(c => ' ' + c).join('\n') + '\n' + + 'Run `npm run package` from the vscode-extension/ directory to bundle the source.' + ); +} + +const V2_SRC = findV2Src(); +const v2url = (rel) => pathToFileURL(path.join(V2_SRC, rel)).href; + +// Dynamic imports β€” resolved against the path found above. +const { createAgentLoop } = await import(v2url('core/agent-loop.mjs')); +const { createToolRegistry } = await import(v2url('tools/registry.mjs')); +const { createPermissionChecker } = await import(v2url('permissions/checker.mjs')); +const { loadSettings } = await import(v2url('config/settings.mjs')); +const { HookEngine } = await import(v2url('hooks/engine.mjs')); +const { AgentLoader } = await import(v2url('agents/loader.mjs')); +const { SkillsLoader } = await import(v2url('skills/loader.mjs')); + +// Redirect console.error/warn to stderr so we don't pollute the ndjson stream. +// (It already goes to stderr by default, but belt-and-suspenders.) +const originalStderr = process.stderr.write.bind(process.stderr); + +function emit(obj) { + process.stdout.write(JSON.stringify(obj) + '\n'); +} + +async function init() { + let settings; + try { + settings = await loadSettings(); + } catch (err) { + emit({ type: 'error', message: `Failed to load settings: ${err.message}` }); + process.exit(1); + } + + const tools = createToolRegistry(); + const permissions = createPermissionChecker(settings.permissions); + const hooks = new HookEngine(settings.hooks || {}); + + // Load agents and skills + const agentLoader = new AgentLoader(); + agentLoader.load(); + const skillsLoader = new SkillsLoader(); + skillsLoader.load(); + + const skillTool = tools.get('Skill'); + if (skillTool) skillTool._skillsLoader = skillsLoader; + + const loop = createAgentLoop({ + model: settings.model || 'claude-sonnet-4-6', + tools, + permissions, + settings, + hooks, + }); + + loop.state._agentLoader = agentLoader; + loop.state._skillsLoader = skillsLoader; + loop.state._hooks = settings.hooks; + loop.state._permissionMode = settings.permissions?.defaultMode || 'default'; + + emit({ type: 'ready' }); + + // ── Message loop ──────────────────────────────────────────────────────── + const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity }); + + // Serialize requests so they never interleave. + let queue = Promise.resolve(); + + rl.on('line', (line) => { + const trimmed = line.trim(); + if (!trimmed) return; + + let msg; + try { + msg = JSON.parse(trimmed); + } catch { + emit({ type: 'error', message: `Bad JSON from extension: ${trimmed}` }); + return; + } + + if (msg.type === 'reset') { + queue = queue.then(() => handleReset(loop)); + } else if (msg.type === 'run') { + queue = queue.then(() => handleRun(loop, msg.message)); + } else if (msg.type === 'model') { + queue = queue.then(() => handleModelSwitch(loop, msg.model)); + } else if (msg.type === 'resume') { + queue = queue.then(() => handleResume(loop, msg.messages || [])); + } + }); + + rl.on('close', () => process.exit(0)); +} + +async function handleRun(loop, message) { + if (!message || typeof message !== 'string') { + emit({ type: 'error', message: 'run message must have a non-empty "message" string' }); + emit({ type: 'stop', reason: 'error' }); + return; + } + try { + for await (const event of loop.run(message)) { + emit(event); + } + } catch (err) { + emit({ type: 'error', message: err.message }); + emit({ type: 'stop', reason: 'error' }); + } +} + +async function handleReset(loop) { + loop.state.messages = []; + loop.state.turnCount = 0; + loop.state.tokenUsage = { input: 0, output: 0 }; + emit({ type: 'ready' }); +} + +async function handleModelSwitch(loop, model) { + if (model && typeof model === 'string') { + loop.state.model = model; + } + emit({ type: 'ready' }); +} + +/** + * Restore conversation history into the agent loop so the model remembers + * the full session from the beginning (like Claude Premium session memory). + * + * UI messages (type:'user'/'assistant', text:'...') are converted to the + * API message format used by the agent loop. + */ +async function handleResume(loop, messages) { + loop.state.messages = messages + .filter(m => (m.type === 'user' || m.type === 'assistant') && m.text) + .map(m => { + if (m.type === 'user') { + return { role: 'user', content: m.text }; + } + // Assistant messages use content-block array format for API compatibility + return { role: 'assistant', content: [{ type: 'text', text: m.text }] }; + }); + loop.state.turnCount = messages.filter(m => m.type === 'user').length; + // Token usage is reset to zero because the stored messages contain only plain + // text (not the original API token counts). The stats bar will show usage for + // new turns going forward; this is correct and expected behaviour on resume. + loop.state.tokenUsage = { input: 0, output: 0 }; + emit({ type: 'ready' }); +} + +init().catch((err) => { + emit({ type: 'error', message: err.message }); + process.exit(1); +}); diff --git a/vscode-extension/extension.js b/vscode-extension/extension.js new file mode 100644 index 0000000..6f176f4 --- /dev/null +++ b/vscode-extension/extension.js @@ -0,0 +1,1167 @@ +'use strict'; +/** + * extension.js β€” Open Claude Code VSCode Extension + * + * Provides two interfaces: + * + * 1. Custom Webview Panel (Cursor-style sidebar) + * - Activity bar icon β†’ dedicated chat panel + * - Rich HTML/CSS/JS UI with markdown, syntax highlighting, code apply, @file mentions + * - Streaming token display, tool visualization, model/mode switching + * + * 2. Chat Participant (@claude) β€” kept for backwards compatibility + * - Forwards messages to the shared agent-bridge subprocess + * + * Commands: + * Open Claude Code: Set API Key + * Open Claude Code: Clear Session + * Open Claude Code: Show Status + * Open Claude Code: Open Chat Panel + * Open Claude Code: Apply Code to Active File + */ + +const vscode = require('vscode'); +const { spawn, execFile } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +const PARTICIPANT_ID = 'open-claude-code.claude'; +const BRIDGE_SCRIPT = path.join(__dirname, 'agent-bridge.mjs'); +// Maximum number of user/assistant messages kept in a persisted session. +// Applies to both the in-progress activeSession and updated history entries. +const MAX_SESSION_MESSAGES = 200; + +// ── Rate-limit retry ───────────────────────────────────────────────────────── +/** Backoff delays (ms) for successive retry attempts */ +const RETRY_DELAYS_MS = [3000, 8000, 20000]; + +/** + * Returns true when the error message looks like a transient rate-limit / + * server-overload error that is worth retrying automatically. + * @param {string} msg + */ +function isRateLimitError(msg) { + return /rate.?limit|overload|too.?many.?request|capacity|529|503|quota/i.test(msg || ''); +} + +// ── AgentBridge ───────────────────────────────────────────────────────────── + +/** + * Manages a single long-lived agent-bridge.mjs child process. + * Serializes requests so concurrent messages don't interleave. + */ +class AgentBridge { + constructor(cwd, env) { + this._cwd = cwd; + this._env = env; + this._proc = null; + this._lineBuffer = ''; + this._currentHandler = null; + this._queue = Promise.resolve(); + this._started = false; + } + + start() { + if (this._started) return; + this._started = true; + + this._proc = spawn(process.execPath, [BRIDGE_SCRIPT], { + cwd: this._cwd, + env: { ...process.env, ...this._env }, + stdio: ['pipe', 'pipe', 'pipe'], + }); + + this._proc.stdout.setEncoding('utf8'); + this._proc.stdout.on('data', (chunk) => { + this._lineBuffer += chunk; + const lines = this._lineBuffer.split('\n'); + this._lineBuffer = lines.pop(); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed) this._dispatch(trimmed); + } + }); + + this._proc.stderr.setEncoding('utf8'); + this._proc.stderr.on('data', (data) => { + console.error('[open-claude-code bridge]', data.trim()); + }); + + this._proc.on('exit', (code, signal) => { + this._started = false; + const handler = this._currentHandler; + this._currentHandler = null; + if (handler) { + const hint = 'Click Retry to continue β€” your session memory will be restored.'; + const reason = signal + ? `signal=${signal}` + : `exit code ${code}`; + handler({ + type: 'error', + message: `Agent process stopped unexpectedly (${reason}). ${hint}`, + }); + } + }); + } + + _dispatch(line) { + let event; + try { event = JSON.parse(line); } catch { + console.error('[open-claude-code bridge] bad JSON:', line); + return; + } + if (this._currentHandler) this._currentHandler(event); + } + + run(message, onEvent) { + // Use the two-arg form of .then() so a rejected queue resolves before + // attempting the new run (prevents a stuck queue after bridge errors). + this._queue = this._queue.then( + () => this._doRun(message, onEvent), + () => this._doRun(message, onEvent), + ); + return this._queue; + } + + _doRun(message, onEvent) { + return new Promise((resolve) => { + this._currentHandler = (event) => { + onEvent(event); + if (event.type === 'stop' || event.type === 'error') { + this._currentHandler = null; + resolve(); + } + }; + if (!this._send({ type: 'run', message })) { + this._currentHandler = null; + onEvent({ type: 'error', message: 'Agent bridge is not running.' }); + onEvent({ type: 'stop', reason: 'error' }); + resolve(); + } + }); + } + + reset() { + this._queue = this._queue.then( + () => new Promise((resolve) => { + this._currentHandler = (event) => { + if (event.type === 'ready' || event.type === 'error') { + this._currentHandler = null; + resolve(); + } + }; + if (!this._send({ type: 'reset' })) { + this._currentHandler = null; + resolve(); + } + }) + ); + return this._queue; + } + + switchModel(model) { + this._queue = this._queue.then( + () => new Promise((resolve) => { + this._currentHandler = (event) => { + if (event.type === 'ready' || event.type === 'error') { + this._currentHandler = null; + resolve(); + } + }; + if (!this._send({ type: 'model', model })) { + this._currentHandler = null; + resolve(); + } + }) + ); + return this._queue; + } + + /** + * Restore conversation history into the agent loop so the model remembers + * the full session from the beginning (Claude Premium-style session memory). + */ + resume(messages) { + this._queue = this._queue.then( + () => new Promise((resolve) => { + this._currentHandler = (event) => { + if (event.type === 'ready' || event.type === 'error') { + this._currentHandler = null; + resolve(); + } + }; + if (!this._send({ type: 'resume', messages })) { + // Bridge not running β€” skip gracefully; a new bridge will be + // created with auto-injected history by getBridge(). + this._currentHandler = null; + resolve(); + } + }) + ); + return this._queue; + } + + /** + * Write a JSON message to the bridge's stdin. + * Returns true on success, false if the bridge is not running or write fails. + * Never throws so callers can use a simple boolean check. + */ + _send(obj) { + if (!this._proc || !this._started) return false; + try { + this._proc.stdin.write(JSON.stringify(obj) + '\n'); + return true; + } catch { + return false; + } + } + + get isRunning() { return this._started && !!this._proc; } + + dispose() { + if (this._proc) { + this._proc.stdin.end(); + this._proc.kill(); + this._proc = null; + } + this._started = false; + } +} + +// ── Extension state ────────────────────────────────────────────────────────── + +/** @type {AgentBridge | null} */ +let bridge = null; + +/** @type {vscode.ExtensionContext | null} */ +let extensionContext = null; + +/** @type {ClaudeCodeViewProvider | null} */ +let viewProvider = null; + +async function getBridge() { + if (bridge && bridge.isRunning) return bridge; + + const config = vscode.workspace.getConfiguration('openClaudeCode'); + const model = config.get('model') || 'claude-sonnet-4-6'; + const permissionMode = config.get('permissionMode') || 'default'; + + const anthropicKey = + (await extensionContext.secrets.get('openClaudeCode.apiKey')) || + process.env.ANTHROPIC_API_KEY || ''; + const openaiKey = process.env.OPENAI_API_KEY || ''; + const googleKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY || ''; + const nvidiaKey = config.get('nvidiaApiKey') || process.env.NVIDIA_API_KEY || ''; + + const env = {}; + if (anthropicKey) env.ANTHROPIC_API_KEY = anthropicKey; + if (openaiKey) env.OPENAI_API_KEY = openaiKey; + if (googleKey) env.GOOGLE_API_KEY = googleKey; + if (nvidiaKey) env.NVIDIA_API_KEY = nvidiaKey; + env.ANTHROPIC_MODEL = model; + env.CLAUDE_CODE_PERMISSION_MODE = permissionMode; + env.CLAUDE_CODE_MAX_TURNS = String(config.get('maxTurns') || 20); + env.NVIDIA_THINKING_MODE = String(config.get('nvidiaThinkingMode') || false); + + const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd(); + + bridge = new AgentBridge(cwd, env); + bridge.start(); + + // ── Auto-restore session memory ───────────────────────────────────────── + // Whenever a fresh bridge is created (after a crash, settings change, or + // VS Code restart) inject the last active session so the model always + // remembers the full conversation without the user having to do anything. + // This is queued before any subsequent run() call so history is always + // loaded before the first user message is processed. + const savedSession = extensionContext + ? extensionContext.globalState.get('openClaudeCode.activeSession') + : null; + if (savedSession?.messages?.length > 0) { + bridge.resume(savedSession.messages); + } + + return bridge; +} + +// ── ClaudeCodeViewProvider (Webview sidebar) ───────────────────────────────── + +class ClaudeCodeViewProvider { + constructor(context) { + this._context = context; + this._view = null; + this._isCancelled = false; + this._tokenUsage = { input: 0, output: 0 }; + this._cost = 0; + } + + resolveWebviewView(webviewView) { + this._view = webviewView; + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [ + vscode.Uri.joinPath(this._context.extensionUri, 'media'), + ], + }; + + webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); + + webviewView.webview.onDidReceiveMessage( + (msg) => this._handleWebviewMessage(msg), + null, + this._context.subscriptions + ); + } + + postMessage(msg) { + if (this._view) { + this._view.webview.postMessage(msg); + } + } + + async _handleWebviewMessage(msg) { + switch (msg.type) { + case 'ready': { + const config = vscode.workspace.getConfiguration('openClaudeCode'); + const hasApiKey = !!( + (await extensionContext.secrets.get('openClaudeCode.apiKey')) || + process.env.ANTHROPIC_API_KEY || + process.env.OPENAI_API_KEY || + process.env.GOOGLE_API_KEY || + process.env.GEMINI_API_KEY || + process.env.NVIDIA_API_KEY || + config.get('nvidiaApiKey') + ); + // Restore any active session persisted before the last VS Code restart + const activeSession = this._context.globalState.get('openClaudeCode.activeSession'); + const activeMessages = (activeSession && Array.isArray(activeSession.messages) && activeSession.messages.length > 0) + ? activeSession.messages : null; + // Also restore the session ID so continued messages update the right history entry + const activeSessionId = (activeSession && activeSession.sessionId) || null; + this.postMessage({ + type: 'initialized', + model: config.get('model') || 'claude-sonnet-4-6', + mode: config.get('permissionMode') || 'default', + thinkingMode: !!config.get('nvidiaThinkingMode'), + autoAttachActiveFile: !!config.get('autoAttachActiveFile'), + hasApiKey, + activeSession: activeMessages, + activeSessionId, + }); + break; + } + + case 'runCommand': { + if (msg.command) { + vscode.commands.executeCommand(msg.command, ...(msg.args || [])); + } + break; + } + + case 'send': { + await this._runPrompt(msg.message, msg.contextFiles, msg.fileRefs); + break; + } + + case 'clear': { + if (bridge && bridge.isRunning) await bridge.reset(); + this._tokenUsage = { input: 0, output: 0 }; + this._cost = 0; + this.postMessage({ type: 'sessionCleared' }); + break; + } + + case 'cancel': { + this._isCancelled = true; + break; + } + + case 'model': { + const config = vscode.workspace.getConfiguration('openClaudeCode'); + await config.update('model', msg.model, vscode.ConfigurationTarget.Global); + if (bridge && bridge.isRunning) await bridge.switchModel(msg.model); + this.postMessage({ type: 'modelChanged', model: msg.model }); + break; + } + + case 'mode': { + const config = vscode.workspace.getConfiguration('openClaudeCode'); + await config.update('permissionMode', msg.mode, vscode.ConfigurationTarget.Global); + if (bridge) { bridge.dispose(); bridge = null; } + break; + } + + case 'thinkingMode': { + const config = vscode.workspace.getConfiguration('openClaudeCode'); + await config.update('nvidiaThinkingMode', !!msg.enabled, vscode.ConfigurationTarget.Global); + // Restart bridge so NVIDIA_THINKING_MODE env var is re-read + if (bridge) { bridge.dispose(); bridge = null; } + this.postMessage({ type: 'thinkingModeChanged', enabled: !!msg.enabled }); + break; + } + + case 'applyCode': { + await this._applyCodeToActiveEditor(msg.code, msg.language); + break; + } + + case 'applyCodeToFile': { + await this._applyCodeWithFilePicker(msg.code, msg.language); + break; + } + + case 'copyToClipboard': { + await vscode.env.clipboard.writeText(msg.text || ''); + break; + } + + case 'pickFile': { + const uris = await vscode.window.showOpenDialog({ + canSelectMany: false, + openLabel: 'Add to context', + }); + if (uris && uris[0]) { + await this._addFileToContext(uris[0].fsPath); + } + break; + } + + case 'addContextFile': { + if (msg.path) await this._addFileToContext(msg.path); + break; + } + + case 'fileSearch': { + const results = await this._searchFiles(msg.query || ''); + this.postMessage({ type: 'fileSearchResults', files: results }); + break; + } + + case 'saveSession': { + if (msg.messages && msg.messages.length > 0) { + const sessions = this._context.globalState.get('openClaudeCode.chatHistory', []); + const firstUser = msg.messages.find(m => m.type === 'user'); + const title = firstUser + ? firstUser.text.slice(0, 80).replace(/\n/g, ' ') + : 'Untitled conversation'; + sessions.unshift({ + id: Date.now().toString(), + title, + createdAt: Date.now(), + messages: msg.messages, + }); + // Keep the 30 most recent sessions + if (sessions.length > 30) sessions.length = 30; + await this._context.globalState.update('openClaudeCode.chatHistory', sessions); + } + break; + } + + case 'getHistory': { + const sessions = this._context.globalState.get('openClaudeCode.chatHistory', []); + const sessionList = sessions.map(s => ({ + id: s.id, + title: s.title, + createdAt: s.createdAt, + messageCount: s.messages ? s.messages.length : 0, + })); + this.postMessage({ type: 'historyData', sessions: sessionList }); + break; + } + + case 'loadSession': { + const sessions = this._context.globalState.get('openClaudeCode.chatHistory', []); + const session = sessions.find(s => s.id === msg.id); + if (session) { + // Include id so the webview can track which session is being viewed + this.postMessage({ type: 'sessionData', id: session.id, messages: session.messages || [] }); + } + break; + } + + case 'resumeFromHistory': { + // Direct-resume (Cursor-style): load and immediately switch to a history session. + const sessions = this._context.globalState.get('openClaudeCode.chatHistory', []); + const session = sessions.find(s => s.id === msg.id); + if (session) { + this.postMessage({ type: 'resumeFromHistoryData', id: session.id, messages: session.messages || [] }); + } + break; + } + + case 'updateSession': { + // Update an existing history entry (after adding new messages to a resumed session). + const sessions = this._context.globalState.get('openClaudeCode.chatHistory', []); + const sess = sessions.find(s => s.id === msg.id); + if (sess && Array.isArray(msg.messages) && msg.messages.length > 0) { + sess.messages = msg.messages.slice(-MAX_SESSION_MESSAGES); + sess.messageCount = sess.messages.length; + const firstUser = sess.messages.find(m => m.type === 'user'); + if (firstUser) sess.title = firstUser.text.slice(0, 80).replace(/\n/g, ' '); + await this._context.globalState.update('openClaudeCode.chatHistory', sessions); + } + break; + } + + case 'renameSession': { + const sessions = this._context.globalState.get('openClaudeCode.chatHistory', []); + const sess = sessions.find(s => s.id === msg.id); + if (sess && msg.title) { + sess.title = String(msg.title).slice(0, 120); + await this._context.globalState.update('openClaudeCode.chatHistory', sessions); + const sessionList = sessions.map(s => ({ + id: s.id, title: s.title, createdAt: s.createdAt, + messageCount: s.messages ? s.messages.length : 0, + })); + this.postMessage({ type: 'historyData', sessions: sessionList }); + } + break; + } + + case 'deleteSession': { + let sessions = this._context.globalState.get('openClaudeCode.chatHistory', []); + sessions = sessions.filter(s => s.id !== msg.id); + await this._context.globalState.update('openClaudeCode.chatHistory', sessions); + const sessionList = sessions.map(s => ({ + id: s.id, title: s.title, createdAt: s.createdAt, + messageCount: s.messages ? s.messages.length : 0, + })); + this.postMessage({ type: 'historyData', sessions: sessionList }); + break; + } + + case 'autoSaveSession': { + // Persist the current in-progress session so it survives VS Code restarts. + // Called by the webview after every completed response (stop event). + if (msg.messages && msg.messages.length > 0) { + // Cap at 200 messages (individual user/assistant entries) to avoid + // unbounded growth of the active session storage. This is separate + // from the 30-session cap on the chat history archive. + const capped = msg.messages.slice(-MAX_SESSION_MESSAGES); + await this._context.globalState.update('openClaudeCode.activeSession', { + messages: capped, + // Track which history session this is so it can be updated on restart + sessionId: msg.sessionId || null, + savedAt: Date.now(), + }); + } else { + await this._context.globalState.update('openClaudeCode.activeSession', null); + } + break; + } + + case 'resumeSession': { + // Restore conversation history into the agent bridge so the model + // remembers the full session (Claude Premium-style session memory). + if (msg.messages && msg.messages.length > 0) { + try { + const agentBridge = await getBridge(); + await agentBridge.resume(msg.messages); + } catch (err) { + this.postMessage({ type: 'error', message: 'Failed to resume session: ' + err.message }); + } + } + break; + } + + case 'exportConversation': { + const content = String(msg.markdown || ''); + const defaultName = 'conversation-' + new Date().toISOString().slice(0, 10) + '.md'; + const defaultUri = vscode.Uri.file( + path.join( + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd(), + defaultName + ) + ); + const uri = await vscode.window.showSaveDialog({ + defaultUri, + filters: { 'Markdown': ['md'] }, + title: 'Export conversation as Markdown', + }); + if (uri) { + await vscode.workspace.fs.writeFile(uri, Buffer.from(content, 'utf8')); + vscode.window.showInformationMessage('Exported to ' + path.basename(uri.fsPath)); + } + break; + } + + case 'getActiveFileContent': { + const editor = vscode.window.activeTextEditor; + if (editor) { + this.postMessage({ + type: 'activeFileContent', + content: editor.document.getText(), + fileName: path.basename(editor.document.fileName), + }); + } else { + this.postMessage({ type: 'activeFileContent', content: null, fileName: null }); + } + break; + } + + case 'getPinnedFiles': { + const config = vscode.workspace.getConfiguration('openClaudeCode'); + const pinned = (config.get('pinnedFiles') || []).filter(p => { + try { return fs.existsSync(p); } catch { return false; } + }); + this.postMessage({ + type: 'pinnedFiles', + files: pinned.map(p => ({ name: path.basename(p), path: p })), + }); + break; + } + + case 'pinFile': { + const config = vscode.workspace.getConfiguration('openClaudeCode'); + const pinned = [...(config.get('pinnedFiles') || [])]; + if (msg.path && !pinned.includes(msg.path)) { + pinned.push(msg.path); + await config.update('pinnedFiles', pinned, vscode.ConfigurationTarget.Global); + } + break; + } + + case 'unpinFile': { + const config = vscode.workspace.getConfiguration('openClaudeCode'); + const pinned = (config.get('pinnedFiles') || []).filter(p => p !== msg.path); + await config.update('pinnedFiles', pinned, vscode.ConfigurationTarget.Global); + break; + } + + case 'runInTerminal': { + // Send shell code directly to the integrated terminal. + // Reuse an existing "Claude Code" terminal if one is already open. + const code = String(msg.code || '').trim(); + if (!code) break; + let terminal = vscode.window.terminals.find(t => t.name === 'Claude Code'); + if (!terminal) { + terminal = vscode.window.createTerminal({ name: 'Claude Code' }); + } + terminal.show(true); // true = preserve editor focus + terminal.sendText(code); + break; + } + + case 'addActiveFile': { + // Add the currently active editor file to the webview context chips. + const editor = vscode.window.activeTextEditor; + if (editor) { + const filePath = editor.document.fileName; + const fileName = path.basename(filePath); + this.postMessage({ + type: 'fileContent', + name: fileName, + path: filePath, + }); + } else { + vscode.window.showInformationMessage('No active editor β€” open a file first.'); + } + break; + } + + case 'getGitContext': { + const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!cwd) { + this.postMessage({ type: 'gitContext', content: '(no workspace folder open)', branch: '' }); + break; + } + const run = (args) => new Promise((resolve) => { + execFile('git', args, { cwd, timeout: 5000 }, (err, stdout) => { + resolve(err ? '' : stdout.trim()); + }); + }); + const [branch, status, diffStat] = await Promise.all([ + run(['rev-parse', '--abbrev-ref', 'HEAD']), + run(['status', '--short']), + run(['diff', '--stat']), + ]); + const parts = []; + if (branch) parts.push(`Branch: ${branch}`); + parts.push(status ? `Changed files:\n${status}` : 'Working tree clean'); + if (diffStat) parts.push(`Diff summary:\n${diffStat}`); + this.postMessage({ type: 'gitContext', content: parts.join('\n\n'), branch: branch || '' }); + break; + } + + case 'getWorkspaceDiagnostics': { + const diags = []; + for (const [uri, ds] of vscode.languages.getDiagnostics()) { + const rel = vscode.workspace.asRelativePath(uri); + for (const d of ds) { + if (d.severity > 1) continue; // skip hints and info + diags.push({ + file: rel, + line: d.range.start.line + 1, + col: d.range.start.character + 1, + severity: d.severity === 0 ? 'error' : 'warning', + message: d.message, + source: d.source || '', + }); + } + } + diags.sort((a, b) => (a.severity === b.severity ? 0 : a.severity === 'error' ? -1 : 1)); + this.postMessage({ type: 'workspaceDiagnostics', diagnostics: diags }); + break; + } + + case 'getOpenEditors': { + const files = []; + const seen = new Set(); + for (const group of (vscode.window.tabGroups?.all || [])) { + for (const tab of group.tabs) { + const uri = tab.input?.uri; + if (uri && !seen.has(uri.fsPath)) { + seen.add(uri.fsPath); + files.push({ + name: path.basename(uri.fsPath), + path: uri.fsPath, + relativePath: vscode.workspace.asRelativePath(uri.fsPath), + }); + } + } + } + this.postMessage({ type: 'openEditors', files }); + break; + } + + case 'toggleAutoAttach': { + const config = vscode.workspace.getConfiguration('openClaudeCode'); + const next = !config.get('autoAttachActiveFile'); + await config.update('autoAttachActiveFile', next, vscode.ConfigurationTarget.Global); + this.postMessage({ type: 'autoAttachState', enabled: next }); + break; + } + + default: + break; + } + } + + async _runPrompt(message, contextFilePaths, fileRefs) { + this._isCancelled = false; + + let fullPrompt = message; + + // Inject context file contents + const allPaths = new Set(contextFilePaths || []); + if (fileRefs && fileRefs.length > 0) { + const ws = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (ws) { + for (const ref of fileRefs) { + const abs = path.resolve(ws, ref); + if (fs.existsSync(abs)) allPaths.add(abs); + } + } + } + + // Auto-attach active editor when the setting is on (works even if no + // other context files were explicitly added by the user). + const promptConfig = vscode.workspace.getConfiguration('openClaudeCode'); + if (promptConfig.get('autoAttachActiveFile')) { + const editor = vscode.window.activeTextEditor; + if (editor) allPaths.add(editor.document.fileName); + } + + if (allPaths.size > 0) { + const fileContents = []; + for (const fp of allPaths) { + try { + const content = fs.readFileSync(fp, 'utf8'); + const rel = vscode.workspace.asRelativePath(fp); + fileContents.push('\n\n--- File: ' + rel + ' ---\n' + content); + } catch { + // skip unreadable files + } + } + if (fileContents.length > 0) { + fullPrompt = message + '\n\n[Context files:]' + fileContents.join(''); + } + } + + let agentBridge; + try { + agentBridge = await getBridge(); + } catch (err) { + this.postMessage({ type: 'error', message: 'Failed to start agent: ' + err.message }); + this.postMessage({ type: 'stop' }); + return; + } + + // ── Retry loop (auto-recovers from rate-limit / overload errors) ──────── + for (let attempt = 0; ; attempt++) { + if (this._isCancelled) return; + + let retryErrorMsg = null; + + await agentBridge.run(fullPrompt, (event) => { + if (this._isCancelled) return; + // Intercept retryable errors β€” don't forward yet; we may recover + if (event.type === 'error' && isRateLimitError(event.message)) { + retryErrorMsg = event.message; + return; + } + this.postMessage(event); + }); + + if (this._isCancelled) return; + + if (!retryErrorMsg) { + // Normal completion β€” stop was already forwarded by onEvent + this.postMessage({ type: 'stop' }); + return; + } + + // Retryable error β€” decide whether to retry or give up + if (attempt >= RETRY_DELAYS_MS.length) { + // All retries exhausted + this.postMessage({ + type: 'error', + message: retryErrorMsg + ` (failed after ${attempt + 1} attempts)`, + }); + this.postMessage({ type: 'stop' }); + return; + } + + const delaySec = Math.ceil(RETRY_DELAYS_MS[attempt] / 1000); + this.postMessage({ + type: 'retrying', + attempt: attempt + 1, + delaySeconds: delaySec, + maxAttempts: RETRY_DELAYS_MS.length, + }); + + // Countdown ticks emitted every second so the UI can show a live timer + await new Promise((resolve) => { + let remaining = delaySec - 1; + const tick = setInterval(() => { + if (this._isCancelled) { clearInterval(tick); resolve(); return; } + if (remaining <= 0) { clearInterval(tick); resolve(); return; } + this.postMessage({ + type: 'retrying', + attempt: attempt + 1, + delaySeconds: remaining, + maxAttempts: RETRY_DELAYS_MS.length, + }); + remaining--; + }, 1000); + }); + + if (this._isCancelled) return; + + // Re-acquire bridge in case it restarted during the wait + try { + agentBridge = await getBridge(); + } catch (err) { + this.postMessage({ type: 'error', message: 'Failed to restart agent: ' + err.message }); + this.postMessage({ type: 'stop' }); + return; + } + } + } + + async _applyCodeToActiveEditor(code) { + const editor = vscode.window.activeTextEditor; + if (!editor) { + vscode.window.showWarningMessage('No active editor. Open a file first.'); + return; + } + await editor.edit((editBuilder) => { + if (!editor.selection.isEmpty) { + editBuilder.replace(editor.selection, code); + } else { + const lastLine = editor.document.lineCount - 1; + const lastChar = editor.document.lineAt(lastLine).text.length; + const end = new vscode.Position(lastLine, lastChar); + editBuilder.insert(end, '\n' + code); + } + }); + await vscode.commands.executeCommand('workbench.action.files.save'); + vscode.window.showInformationMessage('Code applied to ' + path.basename(editor.document.fileName)); + } + + async _applyCodeWithFilePicker(code, language) { + const ext = languageToExt(language); + const uris = await vscode.window.showOpenDialog({ + canSelectMany: false, + openLabel: 'Apply to this file', + filters: ext ? { [language || 'code']: [ext] } : undefined, + }); + if (!uris || !uris[0]) return; + + const doc = await vscode.workspace.openTextDocument(uris[0]); + const editor = await vscode.window.showTextDocument(doc); + await editor.edit((eb) => { + const fullRange = new vscode.Range( + doc.positionAt(0), + doc.positionAt(doc.getText().length) + ); + eb.replace(fullRange, code); + }); + await vscode.commands.executeCommand('workbench.action.files.save'); + } + + async _addFileToContext(filePath) { + const name = path.basename(filePath); + this.postMessage({ type: 'fileContent', path: filePath, name }); + } + + async _searchFiles(query) { + const pattern = query ? ('**/*' + query + '*') : '**/*'; + const uris = await vscode.workspace.findFiles(pattern, '**/node_modules/**', 20); + return uris.map((u) => ({ + name: path.basename(u.fsPath), + path: u.fsPath, + relativePath: vscode.workspace.asRelativePath(u.fsPath), + })); + } + + _getHtmlForWebview(webview) { + const cssUri = webview.asWebviewUri( + vscode.Uri.joinPath(this._context.extensionUri, 'media', 'chat.css') + ); + const jsUri = webview.asWebviewUri( + vscode.Uri.joinPath(this._context.extensionUri, 'media', 'chat.js') + ); + + const templatePath = path.join(this._context.extensionPath, 'media', 'chat.html'); + let html = fs.readFileSync(templatePath, 'utf8'); + + const nonce = generateNonce(); + const csp = [ + "default-src 'none'", + "style-src " + webview.cspSource + " 'unsafe-inline'", + "script-src 'nonce-" + nonce + "'", + "img-src " + webview.cspSource + " data: https:", + "font-src " + webview.cspSource, + ].join('; '); + + html = html + .replace('', '') + .replace('', cssUri.toString()) + .replace(//g, jsUri.toString()) + .replace(' + + diff --git a/vscode-extension/media/chat.js b/vscode-extension/media/chat.js new file mode 100644 index 0000000..cae24d0 --- /dev/null +++ b/vscode-extension/media/chat.js @@ -0,0 +1,2488 @@ +/* eslint-disable */ +/** + * chat.js β€” Claude Code VS Code Webview Client + * + * Responsibilities: + * - Render chat messages (markdown, code blocks with syntax highlighting) + * - Stream assistant replies token-by-token + * - Display tool execution cards (collapsible) + * - @file autocomplete for context injection + * - Apply-to-file button on code blocks + * - Model / permission-mode selectors + * - Stats bar (tokens, cost, elapsed time) + */ + +(function () { + 'use strict'; + + // ── VS Code API ────────────────────────────────────────────────────────── + const vscode = acquireVsCodeApi(); + + // ── State ──────────────────────────────────────────────────────────────── + let isLoading = false; + let currentStreamMsg = null; // DOM element being streamed into + let contextFiles = []; // { name, path, pinned?, isImage?, isCodebase? } + let tokenStats = { input: 0, output: 0 }; + let costTotal = 0; + let startTime = Date.now(); + let currentModel = ''; + let pendingApply = null; // { code, language } + let activeToolCards = {}; // toolName -> dom element + let sessionMessages = []; // tracked messages for history saving + let lastUserMessage = ''; // for ↑ recall and regenerate + let autoScroll = true; // auto-scroll while streaming + let pinnedFiles = []; // files pinned across sessions + let currentSessionId = null; // null for new sessions; history entry ID when continuing a saved session + let currentSessionTitle = ''; // first-message snippet shown in header + let allHistorySessions = []; // full session list for filtering + welcome screen + let historyFilterQuery = ''; // active search filter in history panel + let streamStartTime = 0; // timestamp when first stream token arrived + let streamOutputChars = 0; // approx output chars during current stream (for t/s) + let lastStreamTps = 0; // final t/s from last completed stream + let autoAttachActive = false; // whether to auto-attach the active file with every send + let msgSendTime = 0; // timestamp when the last user message was submitted + + /** Max characters shown in the header session-title indicator */ + const SESSION_TITLE_DISPLAY_LENGTH = 55; + + /** Rough approximation: characters per token for streaming speed calculation */ + const CHARS_PER_TOKEN_ESTIMATE = 4; + + /** Minimum input characters before the char-count hint is shown */ + const CHAR_COUNT_MIN_DISPLAY = 50; + + /** Input characters at which the count turns warning color */ + const CHAR_COUNT_WARNING_THRESHOLD = 3000; + + /** Interval (approx chars) between loading-text speed updates during streaming */ + const STREAM_UPDATE_THROTTLE_CHARS = 80; + + /** Plain-English descriptions for each permission mode (shown in mode-desc-bar) */ + const MODE_DESCRIPTIONS = { + default: 'Asks permission before making file edits or running commands β€” safest choice', + auto: 'Approves safe read operations automatically; asks for writes and commands', + plan: 'Read-only planning mode: analyzes code without making any changes', + acceptEdits: 'Automatically applies all file edits without asking β€” fast but careful', + bypassPermissions: '⚠ Skips all permission checks β€” full automation, use with care', + }; + + /** Keywords that indicate a retryable rate-limit/overload error in the UI */ + const RATE_LIMIT_PATTERN = /rate.?limit|overload|too.?many.?request|capacity|529|503|quota/i; + + // ── DOM refs ───────────────────────────────────────────────────────────── + const messagesEl = document.getElementById('messages'); + const welcomeEl = document.getElementById('welcome'); + const inputEl = document.getElementById('user-input'); + const sendBtn = document.getElementById('send-btn'); + const stopBtn = document.getElementById('stop-btn'); + const modelSelect = document.getElementById('model-select'); + const modeSelect = document.getElementById('mode-select'); + const addFileBtn = document.getElementById('add-file-btn'); + const newChatBtn = document.getElementById('new-chat-btn'); + const contextFilesEl = document.getElementById('context-files'); + const loadingEl = document.getElementById('loading-indicator'); + const loadingText = document.getElementById('loading-text'); + const statsModel = document.getElementById('stats-model'); + const statsTokens = document.getElementById('stats-tokens'); + const statsCost = document.getElementById('stats-cost'); + const statsTime = document.getElementById('stats-time'); + const autocompleteEl = document.getElementById('autocomplete'); + const applyModal = document.getElementById('apply-modal'); + const applyModalBody = document.getElementById('apply-modal-body'); + const applyConfirmBtn = document.getElementById('apply-confirm-btn'); + const applyPickBtn = document.getElementById('apply-pick-btn'); + const applyCancelBtn = document.getElementById('apply-cancel-btn'); + const thinkingToggleEl = document.getElementById('thinking-toggle'); + const thinkingToggleWrapper = document.getElementById('thinking-toggle-wrapper'); + const thinkingLabelEl = document.getElementById('thinking-label'); + const settingsBtn = document.getElementById('settings-btn'); + const historyBtn = document.getElementById('history-btn'); + const historyPanel = document.getElementById('history-panel'); + const historyList = document.getElementById('history-list'); + const historySessionView = document.getElementById('history-session-view'); + const historyCloseBtn = document.getElementById('history-close-btn'); + const historyBackBtn = document.getElementById('history-back-btn'); + const historyPanelTitle = document.getElementById('history-panel-title'); + + const autoscrollBtn = document.getElementById('autoscroll-btn'); + const exportBtn = document.getElementById('export-btn'); + const searchBar = document.getElementById('search-bar'); + const searchInput = document.getElementById('search-input'); + const searchCount = document.getElementById('search-count'); + const searchPrevBtn = document.getElementById('search-prev'); + const searchNextBtn = document.getElementById('search-next'); + const searchCloseBtn = document.getElementById('search-close'); + const contextBarEl = document.getElementById('context-bar'); + const contextUsedEl = document.getElementById('context-used'); + const contextMaxEl = document.getElementById('context-max'); + const contextFillEl = document.getElementById('context-bar-fill'); + const sessionIndicator = document.getElementById('session-indicator'); + const historySearch = document.getElementById('history-search'); + const historySearchBar = document.getElementById('history-search-bar'); + const welcomeRecentEl = document.getElementById('welcome-recent'); + const welcomeRecentList = document.getElementById('welcome-recent-list'); + const activeFileBtn = document.getElementById('active-file-btn'); + const statsSpeedItem = document.getElementById('stats-speed-item'); + const statsSpeedEl = document.getElementById('stats-speed'); + const statsMsgsEl = document.getElementById('stats-msgs'); + const charCountEl = document.getElementById('char-count'); + const actionsBtn = document.getElementById('actions-btn'); + const quickActionsPanel = document.getElementById('quick-actions'); + const modeDescBar = document.getElementById('mode-desc-bar'); + const modeDescText = document.getElementById('mode-desc-text'); + const gitBtn = document.getElementById('git-btn'); + const errorsBtn = document.getElementById('errors-btn'); + const autoAttachBtn = document.getElementById('auto-attach-btn'); + const contextWarningEl = document.getElementById('context-warning'); + const contextWarningTextEl = document.getElementById('context-warning-text'); + const contextWarningNewBtn = document.getElementById('context-warning-new'); + + /** Models that support NVIDIA thinking mode toggle */ + const THINKING_CAPABLE_MODELS = new Set([ + 'moonshotai/kimi-k2.5', + 'deepseek-ai/deepseek-r1', + ]); + + /** Approximate max context tokens per model */ + const MODEL_CONTEXT = { + 'claude-sonnet-4-6': 200000, + 'claude-opus-4-6': 200000, + 'claude-haiku-4-5': 200000, + 'gpt-4o': 128000, + 'gpt-4o-mini': 128000, + 'gemini-2.0-flash': 1000000, + 'moonshotai/kimi-k2.5': 128000, + 'deepseek-ai/deepseek-r1': 64000, + 'nvidia/llama-3.1-nemotron-70b-instruct': 128000, + 'meta/llama-3.1-405b-instruct': 128000, + 'meta/llama-3.3-70b-instruct': 128000, + 'mistralai/mistral-large-2-instruct': 128000, + 'mistralai/mixtral-8x22b-instruct-v0.1': 64000, + }; + + // ── Session indicator (header title) ───────────────────────────────────── + function updateSessionIndicator() { + if (!sessionIndicator) return; + if (currentSessionTitle) { + sessionIndicator.textContent = 'β€” ' + currentSessionTitle; + sessionIndicator.title = currentSessionTitle; + } else { + sessionIndicator.textContent = ''; + sessionIndicator.title = ''; + } + } + + // ── Tick elapsed time ──────────────────────────────────────────────────── + setInterval(() => { + if (!statsTime) return; + const elapsed = Math.floor((Date.now() - startTime) / 1000); + statsTime.textContent = elapsed < 60 + ? `${elapsed}s` + : `${Math.floor(elapsed/60)}m${elapsed%60}s`; + }, 1000); + + // ── Minimal Markdown β†’ HTML renderer ───────────────────────────────────── + function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function renderMarkdown(text) { + if (!text) return ''; + + // Collect fenced code blocks to prevent inner processing + const codeBlocks = []; + let md = text.replace(/```([\w+-]*)\n?([\s\S]*?)```/g, (_, lang, code) => { + const idx = codeBlocks.length; + codeBlocks.push({ lang: lang.trim(), code }); + return `\x00CODE${idx}\x00`; + }); + + // Inline code + const inlineCode = []; + md = md.replace(/`([^`]+)`/g, (_, code) => { + const idx = inlineCode.length; + inlineCode.push(code); + return `\x00INLINE${idx}\x00`; + }); + + // Escape HTML in the rest + md = escapeHtml(md); + + // Headers + md = md.replace(/^(#{1,6})\s+(.+)$/gm, (_, hashes, content) => { + const level = hashes.length; + return `${content}`; + }); + + // Bold + italic + md = md.replace(/\*\*\*(.+?)\*\*\*/g, '$1'); + md = md.replace(/\*\*(.+?)\*\*/g, '$1'); + md = md.replace(/\*(.+?)\*/g, '$1'); + md = md.replace(/__(.+?)__/g, '$1'); + md = md.replace(/_(.+?)_/g, '$1'); + + // Strikethrough + md = md.replace(/~~(.+?)~~/g, '$1'); + + // Links + md = md.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + // Horizontal rules + md = md.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '
'); + + // Blockquotes (simple, single-level) + md = md.replace(/^>\s?(.*)$/gm, '
$1
'); + + // Unordered lists (simple) + md = md.replace(/^[\*\-]\s+(.+)$/gm, '
  • $1
  • '); + md = md.replace(/(
  • [\s\S]*?<\/li>)+/g, (m) => `
      ${m}
    `); + + // Ordered lists + md = md.replace(/^\d+\.\s+(.+)$/gm, '
  • $1
  • '); + + // Tables + md = md.replace(/^\|(.+)\|\s*\n\|[-| :]+\|\s*\n((?:\|.+\|\s*\n)*)/gm, (_, header, rows) => { + const th = header.split('|').map(c => `${c.trim()}`).join(''); + const trs = rows.trim().split('\n').map(row => { + const tds = row.slice(1, -1).split('|').map(c => `${c.trim()}`).join(''); + return `${tds}`; + }).join(''); + return `${th}${trs}
    `; + }); + + // Paragraphs: wrap consecutive non-empty lines + md = md.replace(/\n\n+/g, '\n\n'); + const paragraphs = md.split('\n\n').map(chunk => { + chunk = chunk.trim(); + if (!chunk) return ''; + if (/^<(h\d|ul|ol|table|blockquote|hr)/.test(chunk)) return chunk; + return `

    ${chunk.replace(/\n/g, '
    ')}

    `; + }); + md = paragraphs.join('\n'); + + // Restore inline code + md = md.replace(/\x00INLINE(\d+)\x00/g, (_, i) => { + return `${escapeHtml(inlineCode[+i])}`; + }); + + // Restore code blocks β€” rendered as interactive elements + md = md.replace(/\x00CODE(\d+)\x00/g, (_, i) => { + const { lang, code } = codeBlocks[+i]; + return buildCodeBlockHtml(code, lang); + }); + + return md; + } + + // ── Syntax Highlighter ──────────────────────────────────────────────────── + + const keywords = { + js: ['const','let','var','function','return','if','else','for','while','do','break','continue', + 'switch','case','default','class','extends','new','this','super','import','export','from', + 'async','await','try','catch','finally','throw','typeof','instanceof','in','of','delete', + 'void','yield','static','get','set','null','undefined','true','false'], + ts: ['const','let','var','function','return','if','else','for','while','do','break','continue', + 'switch','case','default','class','extends','new','this','super','import','export','from', + 'async','await','try','catch','finally','throw','typeof','instanceof','in','of','delete', + 'void','yield','static','get','set','null','undefined','true','false', + 'interface','type','enum','namespace','declare','abstract','implements','readonly', + 'public','private','protected','as','keyof','infer','never','any','string','number','boolean'], + py: ['def','class','return','if','elif','else','for','while','break','continue','pass','import', + 'from','as','try','except','finally','raise','with','lambda','yield','async','await', + 'True','False','None','and','or','not','in','is','del','global','nonlocal','print'], + go: ['func','var','const','type','struct','interface','map','chan','package','import','return', + 'if','else','for','switch','case','default','break','continue','go','defer','select', + 'nil','true','false','string','int','float64','bool','error'], + rust: ['fn','let','mut','const','struct','enum','impl','trait','mod','use','pub','crate','super', + 'self','return','if','else','for','while','loop','match','break','continue','async','await', + 'true','false','None','Some','Ok','Err'], + java: ['class','interface','extends','implements','new','return','if','else','for','while','do', + 'switch','case','default','break','continue','try','catch','finally','throw','throws', + 'public','private','protected','static','final','abstract','import','package', + 'null','true','false','void','int','long','double','float','boolean','String'], + sh: ['if','then','else','elif','fi','for','do','done','while','case','esac','function', + 'return','exit','echo','export','local','readonly','shift','source', + 'true','false','null'], + }; + + function highlightCode(code, lang) { + const l = (lang || '').toLowerCase().replace(/[^a-z0-9#+-]/g, ''); + + // Map aliases + const langMap = { + javascript: 'js', jsx: 'js', mjs: 'js', cjs: 'js', + typescript: 'ts', tsx: 'ts', + python: 'py', py3: 'py', + golang: 'go', + shell: 'sh', bash: 'sh', zsh: 'sh', cmd: 'sh', + java: 'java', + rust: 'rust', rs: 'rust', + }; + const normalLang = langMap[l] || l; + + const kws = keywords[normalLang] || keywords.js; + const kwSet = new Set(kws); + + // For JSON, use a simple formatter + if (normalLang === 'json') return highlightJson(code); + + let result = ''; + let i = 0; + const len = code.length; + + while (i < len) { + // Single-line comment + if ((normalLang === 'js' || normalLang === 'ts' || normalLang === 'go' || + normalLang === 'java' || normalLang === 'rust') && + code[i] === '/' && code[i+1] === '/') { + const end = code.indexOf('\n', i); + const comment = end === -1 ? code.slice(i) : code.slice(i, end); + result += `${escapeHtml(comment)}`; + i += comment.length; + continue; + } + // Python/Bash comment + if ((normalLang === 'py' || normalLang === 'sh') && code[i] === '#') { + const end = code.indexOf('\n', i); + const comment = end === -1 ? code.slice(i) : code.slice(i, end); + result += `${escapeHtml(comment)}`; + i += comment.length; + continue; + } + // Block comment + if ((normalLang === 'js' || normalLang === 'ts' || normalLang === 'go' || + normalLang === 'java' || normalLang === 'rust') && + code[i] === '/' && code[i+1] === '*') { + const end = code.indexOf('*/', i + 2); + const comment = end === -1 ? code.slice(i) : code.slice(i, end + 2); + result += `${escapeHtml(comment)}`; + i += comment.length; + continue; + } + // String (double quote) + if (code[i] === '"') { + let j = i + 1; + while (j < len && !(code[j] === '"' && code[j-1] !== '\\')) j++; + const str = code.slice(i, j + 1); + result += `${escapeHtml(str)}`; + i = j + 1; + continue; + } + // String (single quote) + if (code[i] === "'") { + let j = i + 1; + while (j < len && !(code[j] === "'" && code[j-1] !== '\\')) j++; + const str = code.slice(i, j + 1); + result += `${escapeHtml(str)}`; + i = j + 1; + continue; + } + // Template literal + if (code[i] === '`' && (normalLang === 'js' || normalLang === 'ts')) { + let j = i + 1; + while (j < len && !(code[j] === '`' && code[j-1] !== '\\')) j++; + const str = code.slice(i, j + 1); + result += `${escapeHtml(str)}`; + i = j + 1; + continue; + } + // Number + if (/\d/.test(code[i]) && (i === 0 || /\W/.test(code[i-1]))) { + let j = i; + while (j < len && /[\d._xXbBoO]/.test(code[j])) j++; + result += `${escapeHtml(code.slice(i, j))}`; + i = j; + continue; + } + // Identifier or keyword + if (/[a-zA-Z_$]/.test(code[i])) { + let j = i; + while (j < len && /[\w$]/.test(code[j])) j++; + const word = code.slice(i, j); + if (kwSet.has(word)) { + result += `${escapeHtml(word)}`; + } else if (/^[A-Z]/.test(word)) { + result += `${escapeHtml(word)}`; + } else if (code[j] === '(') { + result += `${escapeHtml(word)}`; + } else { + result += `${escapeHtml(word)}`; + } + i = j; + continue; + } + // Operator + if (/[=>${escapeHtml(code[i])}`; + i++; + continue; + } + // Everything else + result += escapeHtml(code[i]); + i++; + } + return result; + } + + function highlightJson(code) { + return escapeHtml(code).replace( + /("(?:\\.|[^"\\])*")\s*:|("(?:\\.|[^"\\])*")|(true|false|null)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g, + (m, key, str, bool, num) => { + if (key) return `${key}:`; + if (str) return `${str}`; + if (bool) return `${bool}`; + if (num) return `${num}`; + return m; + } + ); + } + + // ── Code block HTML builder ─────────────────────────────────────────────── + let codeBlockIdCounter = 0; + // Store code by ID to avoid large data attributes and XSS risks + const codeStore = new Map(); + + /** Languages whose code can be sent directly to the integrated terminal */ + const RUNNABLE_LANGS = new Set(['sh', 'bash', 'shell', 'zsh', 'cmd', 'batch', 'powershell', 'ps1']); + + function buildCodeBlockHtml(code, lang) { + const id = `cb-${++codeBlockIdCounter}`; + const highlighted = highlightCode(code, lang); + const displayLang = lang || 'code'; + // Store code in JS Map, not in DOM attribute + codeStore.set(id, { code, language: lang || '' }); + const isRunnable = RUNNABLE_LANGS.has((lang || '').toLowerCase()); + const runBtnHtml = isRunnable + ? `` + : ''; + + // Wrap each line in a for CSS line numbers + const rawLines = highlighted.split('\n'); + // Remove a single trailing empty line artifact from split + if (rawLines.length > 1 && rawLines[rawLines.length - 1] === '') rawLines.pop(); + const numberedCode = rawLines.map(l => `${l}`).join('\n'); + + return `
    +
    + ${escapeHtml(displayLang)} +
    + ${runBtnHtml} + + +
    +
    +
    ${numberedCode}
    +
    `; + } + + // ── Event delegation for code block buttons ─────────────────────────────── + // (replaces window.copyCode / window.applyCode inline onclick handlers) + messagesEl.addEventListener('click', (e) => { + const btn = e.target.closest('[data-action]'); + if (!btn) return; + const action = btn.dataset.action; + const blockId = btn.dataset.blockId; + if (!blockId) return; + if (action === 'copy') { + const entry = codeStore.get(blockId); + if (!entry) return; + vscode.postMessage({ type: 'copyToClipboard', text: entry.code }); + btn.textContent = 'Copied!'; + setTimeout(() => { btn.textContent = 'Copy'; }, 1500); + } else if (action === 'apply') { + const entry = codeStore.get(blockId); + if (!entry) return; + pendingApply = { code: entry.code, language: entry.language }; + // Show preview immediately; diff will arrive when extension reads active file + showApplyModal(entry.code, null, null); + vscode.postMessage({ type: 'getActiveFileContent' }); + } else if (action === 'run') { + const entry = codeStore.get(blockId); + if (!entry) return; + vscode.postMessage({ type: 'runInTerminal', code: entry.code }); + btn.textContent = 'βœ“ Sent'; + setTimeout(() => { btn.textContent = 'β–· Run'; }, 1500); + } else if (action === 'wrap') { + const block = document.getElementById(blockId); + if (!block) return; + const pre = block.querySelector('pre'); + if (!pre) return; + const isWrapped = pre.classList.toggle('wrapped'); + btn.classList.toggle('active', isWrapped); + btn.title = isWrapped ? 'Word wrap: on (click to disable)' : 'Toggle word wrap'; + } + }); + + function decodeHtmlEntities(str) { + // Manually reverse only the escapes produced by escapeHtml() + return str + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&'); + } + + // ── Apply Modal ─────────────────────────────────────────────────────────── + function cancelApply() { + applyModal.classList.remove('visible'); + pendingApply = null; + } + + /** + * LCS-based line diff. Returns array of {type:'equal'|'add'|'remove', line}. + * Falls back to showing all new lines when files are too large. + */ + function computeDiff(aLines, bLines) { + if (aLines.length * bLines.length > 400000) { + return bLines.map(l => ({ type: 'add', line: l })); + } + const n = aLines.length, m = bLines.length; + const dp = Array.from({ length: n + 1 }, () => new Int32Array(m + 1)); + for (let i = 1; i <= n; i++) { + for (let j = 1; j <= m; j++) { + dp[i][j] = aLines[i - 1] === bLines[j - 1] + ? dp[i - 1][j - 1] + 1 + : Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + const result = []; + let i = n, j = m; + while (i > 0 || j > 0) { + if (i > 0 && j > 0 && aLines[i - 1] === bLines[j - 1]) { + result.unshift({ type: 'equal', line: aLines[i - 1] }); i--; j--; + } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { + result.unshift({ type: 'add', line: bLines[j - 1] }); j--; + } else { + result.unshift({ type: 'remove', line: aLines[i - 1] }); i--; + } + } + return result; + } + + function buildDiffView(diff) { + const container = document.createElement('div'); + container.className = 'diff-view'; + const pre = document.createElement('pre'); + for (const { type, line } of diff) { + const span = document.createElement('span'); + span.className = 'diff-line diff-' + type; + span.textContent = (type === 'add' ? '+ ' : type === 'remove' ? '- ' : ' ') + line; + pre.appendChild(span); + pre.appendChild(document.createTextNode('\n')); + } + container.appendChild(pre); + return container; + } + + function showApplyModal(newCode, oldContent, fileName) { + const titleEl = document.getElementById('apply-modal-title'); + if (titleEl) { + titleEl.textContent = (oldContent !== null && oldContent !== undefined) + ? `Review changes β€” ${fileName || 'active file'}` + : 'Apply code to file'; + } + applyModalBody.innerHTML = ''; + if (oldContent !== null && oldContent !== undefined) { + const diff = computeDiff(oldContent.split('\n'), newCode.split('\n')); + applyModalBody.appendChild(buildDiffView(diff)); + } else { + const preview = newCode.length > 2000 ? newCode.slice(0, 2000) + '\n…' : newCode; + applyModalBody.innerHTML = buildCodeBlockHtml(preview, pendingApply ? pendingApply.language : ''); + } + applyModal.classList.add('visible'); + } + + const applyCancelTopBtn = document.getElementById('apply-cancel-top'); + if (applyCancelTopBtn) { + applyCancelTopBtn.addEventListener('click', cancelApply); + } + + if (applyCancelBtn) { + applyCancelBtn.addEventListener('click', cancelApply); + } + + if (applyConfirmBtn) { + applyConfirmBtn.addEventListener('click', () => { + if (!pendingApply) return; + vscode.postMessage({ + type: 'applyCode', + code: pendingApply.code, + language: pendingApply.language, + }); + cancelApply(); + }); + } + + if (applyPickBtn) { + applyPickBtn.addEventListener('click', () => { + if (!pendingApply) return; + vscode.postMessage({ + type: 'applyCodeToFile', + code: pendingApply.code, + language: pendingApply.language, + }); + cancelApply(); + }); + } + + // ── Copy button helper ──────────────────────────────────────────────────── + function addCopyButtonToMessage(msgDiv, rawText, userPrompt) { + const header = msgDiv.querySelector('.msg-header'); + if (!header || header.querySelector('.msg-copy-btn')) return; + const btn = document.createElement('button'); + btn.className = 'msg-copy-btn'; + btn.title = 'Copy answer'; + btn.textContent = '⎘ Copy'; + btn.addEventListener('click', () => { + vscode.postMessage({ type: 'copyToClipboard', text: rawText }); + btn.textContent = 'βœ“ Copied'; + btn.classList.add('copied'); + setTimeout(() => { + btn.textContent = '⎘ Copy'; + btn.classList.remove('copied'); + }, 1500); + }); + header.appendChild(btn); + + if (userPrompt) { + const regenBtn = document.createElement('button'); + regenBtn.className = 'msg-regen-btn'; + regenBtn.title = 'Regenerate response'; + regenBtn.textContent = 'β†Ί'; + regenBtn.addEventListener('click', () => { + if (isLoading) return; + regenerateFrom(msgDiv, userPrompt); + }); + header.appendChild(regenBtn); + } + + // "Continue" button β€” sends a follow-up that asks the model to keep going. + // Useful when the model stops mid-task (e.g. max turns reached). + const continueBtn = document.createElement('button'); + continueBtn.className = 'msg-continue-btn'; + continueBtn.title = 'Ask the model to continue from here'; + continueBtn.textContent = 'β–Ά Continue'; + continueBtn.addEventListener('click', () => { + if (isLoading) return; + setSending(true); + setLoading(true, 'Thinking…'); + vscode.postMessage({ type: 'send', message: 'Please continue from where you left off.', contextFiles: [], fileRefs: [] }); + }); + header.appendChild(continueBtn); + } + + // ── Message rendering ───────────────────────────────────────────────────── + function hideWelcome() { + if (welcomeEl && !welcomeEl.classList.contains('hidden')) { + welcomeEl.classList.add('hidden'); + } + } + + function scrollToBottom() { + if (!autoScroll) return; + requestAnimationFrame(() => { + messagesEl.scrollTop = messagesEl.scrollHeight; + }); + } + + function addUserMessage(text) { + msgSendTime = Date.now(); // track when message was sent for per-message timing + hideWelcome(); + currentStreamMsg = null; + lastUserMessage = text; + // Derive session title from the first user message + if (sessionMessages.length === 0 && text.trim()) { + currentSessionTitle = text.trim().slice(0, SESSION_TITLE_DISPLAY_LENGTH).replace(/\n/g, ' '); + updateSessionIndicator(); + } + sessionMessages.push({ type: 'user', text }); + + const div = document.createElement('div'); + div.className = 'msg msg-user'; + div._sessionIdx = sessionMessages.length - 1; + + const meta = document.createElement('div'); + meta.className = 'msg-meta'; + meta.appendChild(document.createTextNode('You')); + + const editBtn = document.createElement('button'); + editBtn.className = 'msg-edit-btn'; + editBtn.title = 'Edit message'; + editBtn.textContent = '✏'; + editBtn.addEventListener('click', () => editUserMessage(div, text)); + meta.appendChild(editBtn); + + const copyUserBtn = document.createElement('button'); + copyUserBtn.className = 'msg-user-copy-btn'; + copyUserBtn.title = 'Copy message'; + copyUserBtn.textContent = '⎘'; + copyUserBtn.addEventListener('click', () => { + vscode.postMessage({ type: 'copyToClipboard', text }); + copyUserBtn.textContent = 'βœ“'; + setTimeout(() => { copyUserBtn.textContent = '⎘'; }, 1500); + }); + meta.appendChild(copyUserBtn); + + const timeSpan = document.createElement('span'); + timeSpan.className = 'msg-time'; + timeSpan.textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + meta.appendChild(timeSpan); + + const bubble = document.createElement('div'); + bubble.className = 'msg-bubble'; + bubble.textContent = text; // safe β€” textContent + + div.appendChild(meta); + div.appendChild(bubble); + messagesEl.appendChild(div); + scrollToBottom(); + } + + function getOrCreateAssistantMessage() { + if (currentStreamMsg) return currentStreamMsg; + hideWelcome(); + const div = document.createElement('div'); + div.className = 'msg msg-assistant'; + div._regenPrompt = lastUserMessage; // capture for regenerate + const ts = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + div.innerHTML = ` +
    +
    ✦
    + Claude + ${escapeHtml(ts)} +
    +
    + `; + messagesEl.appendChild(div); + currentStreamMsg = div.querySelector('.msg-content'); + scrollToBottom(); + return currentStreamMsg; + } + + function appendStreamText(text) { + const el = getOrCreateAssistantMessage(); + // Accumulate raw text on element, re-render markdown periodically + el._rawText = (el._rawText || '') + text; + + // Track streaming speed + if (!streamStartTime) streamStartTime = Date.now(); + streamOutputChars += text.length; + // Update loading label roughly every STREAM_UPDATE_THROTTLE_CHARS to avoid too many DOM writes + if (streamOutputChars % STREAM_UPDATE_THROTTLE_CHARS < text.length) { + const elapsed = (Date.now() - streamStartTime) / 1000; + const tps = elapsed > 0.5 ? Math.round(streamOutputChars / CHARS_PER_TOKEN_ESTIMATE / elapsed) : 0; + if (tps > 0) setLoading(true, `Generating… (${tps} t/s)`); + } + + // Throttle rendering to avoid layout thrashing + if (!el._renderPending) { + el._renderPending = true; + requestAnimationFrame(() => { + el._renderPending = false; + if (el._rawText) { + el.innerHTML = renderMarkdown(el._rawText); + el.classList.add('streaming-cursor'); + } + scrollToBottom(); + }); + } + } + + function finalizeAssistantMessage(content) { + // Compute and record final streaming speed + if (streamStartTime && streamOutputChars > 0) { + const elapsed = (Date.now() - streamStartTime) / 1000; + lastStreamTps = elapsed > 0.1 ? Math.round(streamOutputChars / CHARS_PER_TOKEN_ESTIMATE / elapsed) : 0; + } + streamStartTime = 0; + streamOutputChars = 0; + // Show speed in stats bar + if (statsSpeedItem && statsSpeedEl && lastStreamTps > 0) { + statsSpeedEl.textContent = lastStreamTps; + statsSpeedItem.style.display = ''; + } + + // Compute per-message response time + let responseTimeLabel = ''; + if (msgSendTime > 0) { + const elapsed = (Date.now() - msgSendTime) / 1000; + responseTimeLabel = elapsed < 60 + ? `${elapsed.toFixed(1)}s` + : `${Math.floor(elapsed / 60)}m${Math.round(elapsed % 60)}s`; + msgSendTime = 0; + } + + if (currentStreamMsg) { + const raw = currentStreamMsg._rawText || content || ''; + currentStreamMsg.innerHTML = raw ? renderMarkdown(raw) : ''; + currentStreamMsg.classList.remove('streaming-cursor'); + currentStreamMsg._rawText = ''; + // Show response time in header + if (responseTimeLabel) { + const msgDiv = currentStreamMsg.closest('.msg-assistant'); + const timeEl = msgDiv?.querySelector('.msg-time'); + if (timeEl) timeEl.textContent = responseTimeLabel; + } + // Add copy + regenerate buttons + track in history + if (raw) { + const msgDiv = currentStreamMsg.closest('.msg-assistant'); + if (msgDiv) addCopyButtonToMessage(msgDiv, raw, msgDiv._regenPrompt); + sessionMessages.push({ type: 'assistant', text: raw }); + } + currentStreamMsg = null; + } else if (content) { + hideWelcome(); + const div = document.createElement('div'); + div.className = 'msg msg-assistant'; + div.innerHTML = ` +
    +
    ✦
    + Claude + ${responseTimeLabel ? `${escapeHtml(responseTimeLabel)}` : ''} +
    +
    ${renderMarkdown(content)}
    + `; + addCopyButtonToMessage(div, content); + sessionMessages.push({ type: 'assistant', text: content }); + messagesEl.appendChild(div); + } + scrollToBottom(); + } + + let toolCardCounter = 0; + + function addToolCard(toolName) { + hideWelcome(); + const id = `tool-${++toolCardCounter}`; + + // Build card using DOM API so toolName is safely set via textContent + const msgDiv = document.createElement('div'); + msgDiv.className = 'msg'; + + const card = document.createElement('div'); + card.className = 'tool-card'; + card.id = id; + + const header = document.createElement('div'); + header.className = 'tool-card-header'; + header.addEventListener('click', () => card.classList.toggle('expanded')); + + const iconSpan = document.createElement('span'); + iconSpan.className = 'tool-icon'; + iconSpan.textContent = 'βš™'; + + const nameSpan = document.createElement('span'); + nameSpan.className = 'tool-name'; + nameSpan.textContent = toolName; // safe β€” textContent, not innerHTML + + const statusSpan = document.createElement('span'); + statusSpan.className = 'tool-status'; + const spinnerSpan = document.createElement('span'); + spinnerSpan.className = 'tool-spinner'; + spinnerSpan.textContent = '⟳'; + statusSpan.appendChild(spinnerSpan); + statusSpan.appendChild(document.createTextNode(' running…')); + + const chevron = document.createElement('span'); + chevron.className = 'tool-chevron'; + chevron.textContent = 'β–Ά'; + + header.appendChild(iconSpan); + header.appendChild(nameSpan); + header.appendChild(statusSpan); + header.appendChild(chevron); + + const body = document.createElement('div'); + body.className = 'tool-card-body'; + + const resultDiv = document.createElement('div'); + resultDiv.className = 'tool-result'; + resultDiv.id = `${id}-result`; + const em = document.createElement('em'); + em.textContent = 'Waiting for result…'; + resultDiv.appendChild(em); + body.appendChild(resultDiv); + + card.appendChild(header); + card.appendChild(body); + msgDiv.appendChild(card); + messagesEl.appendChild(msgDiv); + + activeToolCards[toolName] = id; + scrollToBottom(); + return id; + } + + function updateToolCard(toolName, result) { + const id = activeToolCards[toolName]; + if (!id) return; + const card = document.getElementById(id); + if (!card) return; + const statusEl = card.querySelector('.tool-status'); + if (statusEl) { + statusEl.textContent = ''; + const doneSpan = document.createElement('span'); + doneSpan.style.color = 'var(--success)'; + doneSpan.textContent = 'βœ“ done'; + statusEl.appendChild(doneSpan); + } + const resultEl = document.getElementById(`${id}-result`); + if (resultEl) { + const preview = result && result.length > 800 + ? result.slice(0, 800) + '\n…' + : (result || ''); + resultEl.textContent = preview; // safe β€” textContent only + } + delete activeToolCards[toolName]; + } + + let thinkingCounter = 0; + + function addThinkingBlock(text) { + hideWelcome(); + const id = `think-${++thinkingCounter}`; + const msgDiv = document.createElement('div'); + msgDiv.className = 'msg'; + + const thinkEl = document.createElement('div'); + thinkEl.className = 'msg-thinking'; + thinkEl.id = id; + + const headerEl = document.createElement('div'); + headerEl.className = 'msg-thinking-header'; + headerEl.addEventListener('click', () => thinkEl.classList.toggle('expanded')); + + const bubbleIcon = document.createElement('span'); + bubbleIcon.textContent = 'πŸ’­'; + const label = document.createElement('span'); + label.textContent = 'Extended thinking'; + const hint = document.createElement('span'); + hint.style.cssText = 'margin-left:auto;font-size:10px'; + hint.textContent = 'click to expand'; + + headerEl.appendChild(bubbleIcon); + headerEl.appendChild(label); + headerEl.appendChild(hint); + + const bodyEl = document.createElement('div'); + bodyEl.className = 'msg-thinking-body'; + bodyEl.textContent = text || ''; // safe β€” textContent + + thinkEl.appendChild(headerEl); + thinkEl.appendChild(bodyEl); + msgDiv.appendChild(thinkEl); + messagesEl.appendChild(msgDiv); + scrollToBottom(); + } + + function addSystemMessage(text) { + hideWelcome(); + const div = document.createElement('div'); + div.className = 'msg msg-system'; + div.innerHTML = `
    ${escapeHtml(text)}
    `; + messagesEl.appendChild(div); + scrollToBottom(); + } + + function addErrorMessage(text) { + hideWelcome(); + currentStreamMsg = null; + const div = document.createElement('div'); + div.className = 'msg msg-error'; + + const bubble = document.createElement('div'); + bubble.className = 'msg-bubble'; + bubble.textContent = '⚠ ' + text; // safe β€” textContent + + div.appendChild(bubble); + + // Always show a retry row so the user can manually retry after any error + const actionsRow = document.createElement('div'); + actionsRow.className = 'msg-error-actions'; + + const retryBtn = document.createElement('button'); + retryBtn.className = 'msg-retry-btn'; + retryBtn.textContent = '↩ Retry'; + retryBtn.title = 'Re-send the last message'; + retryBtn.addEventListener('click', () => { + if (isLoading || !lastUserMessage) return; + div.remove(); + setSending(true); + setLoading(true, 'Thinking…'); + vscode.postMessage({ type: 'send', message: lastUserMessage, contextFiles: [], fileRefs: [] }); + }); + actionsRow.appendChild(retryBtn); + div.appendChild(actionsRow); + + messagesEl.appendChild(div); + scrollToBottom(); + } + + // ── Edit user message ───────────────────────────────────────────────────── + function editUserMessage(msgDiv, originalText) { + if (isLoading) return; + const bubble = msgDiv.querySelector('.msg-bubble'); + if (!bubble) return; + + const ta = document.createElement('textarea'); + ta.className = 'msg-user-edit-area'; + ta.value = originalText; + ta.rows = 3; + + const actionsRow = document.createElement('div'); + actionsRow.className = 'msg-user-edit-actions'; + + const cancelBtn = document.createElement('button'); + cancelBtn.className = 'modal-btn'; + cancelBtn.textContent = 'Cancel'; + + const confirmBtn = document.createElement('button'); + confirmBtn.className = 'modal-btn primary'; + confirmBtn.textContent = '↡ Resend'; + + actionsRow.appendChild(cancelBtn); + actionsRow.appendChild(confirmBtn); + + bubble.replaceWith(ta); + msgDiv.appendChild(actionsRow); + ta.focus(); + ta.setSelectionRange(ta.value.length, ta.value.length); + + cancelBtn.addEventListener('click', () => { + ta.replaceWith(bubble); + actionsRow.remove(); + }); + confirmBtn.addEventListener('click', confirmEdit); + ta.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); confirmEdit(); } + if (e.key === 'Escape') { ta.replaceWith(bubble); actionsRow.remove(); } + }); + + function confirmEdit() { + const newText = ta.value.trim(); + if (!newText) return; + + bubble.textContent = newText; + ta.replaceWith(bubble); + actionsRow.remove(); + msgDiv._originalText = newText; + // Update edit button closure + const eb = msgDiv.querySelector('.msg-edit-btn'); + if (eb) eb.onclick = null; + if (eb) eb.addEventListener('click', () => editUserMessage(msgDiv, newText)); + + // Remove all DOM messages after this one + let el = msgDiv.nextElementSibling; + while (el) { const nx = el.nextElementSibling; el.remove(); el = nx; } + + // Truncate sessionMessages to this user message (inclusive) + const idx = typeof msgDiv._sessionIdx === 'number' ? msgDiv._sessionIdx : -1; + if (idx >= 0) sessionMessages.splice(idx); + sessionMessages.push({ type: 'user', text: newText }); + msgDiv._sessionIdx = sessionMessages.length - 1; + + lastUserMessage = newText; + setSending(true); + setLoading(true, 'Thinking…'); + vscode.postMessage({ type: 'send', message: newText, contextFiles: [], fileRefs: [] }); + } + } + + // ── Regenerate response ─────────────────────────────────────────────────── + function regenerateFrom(assistantMsgDiv, userPrompt) { + // Remove this assistant message and everything after it from the DOM + let el = assistantMsgDiv; + while (el) { const nx = el.nextElementSibling; el.remove(); el = nx; } + + // Truncate sessionMessages: remove the last assistant entry(s) for this prompt + const lastUserIdx = [...sessionMessages].reduceRight((found, m, i) => + found === -1 && m.type === 'user' && m.text === userPrompt ? i : found, -1); + if (lastUserIdx >= 0) sessionMessages.splice(lastUserIdx + 1); + + lastUserMessage = userPrompt; + setSending(true); + setLoading(true, 'Thinking…'); + vscode.postMessage({ type: 'send', message: userPrompt, contextFiles: [], fileRefs: [] }); + } + + // ── @codebase chip ──────────────────────────────────────────────────────── + function addCodebaseContext() { + if (contextFiles.find(f => f.isCodebase)) return; + contextFiles.push({ name: '@codebase', path: '__codebase__', isCodebase: true }); + renderContextFiles(); + } + + // ── @git chip β€” inject current git status ───────────────────────────────── + function requestGitContext() { + vscode.postMessage({ type: 'getGitContext' }); + } + function addGitContext(content, branch) { + if (contextFiles.find(f => f.isGit)) { + // Replace existing git chip with fresh one + contextFiles = contextFiles.filter(f => !f.isGit); + } + contextFiles.push({ + name: branch ? `βŽ‡ ${branch}` : 'βŽ‡ git', + path: '__git__', + isGit: true, + content, + }); + renderContextFiles(); + } + + // ── @errors chip β€” inject workspace diagnostics ─────────────────────────── + function requestErrorsContext() { + vscode.postMessage({ type: 'getWorkspaceDiagnostics' }); + } + function addErrorsContext(diagnostics) { + if (contextFiles.find(f => f.isErrors)) { + contextFiles = contextFiles.filter(f => !f.isErrors); + } + const errCount = diagnostics.filter(d => d.severity === 'error').length; + const warnCount = diagnostics.filter(d => d.severity === 'warning').length; + const content = diagnostics.length > 0 + ? diagnostics.map(d => + `[${d.severity.toUpperCase()}] ${d.file}:${d.line}:${d.col} β€” ${d.message}${d.source ? ` (${d.source})` : ''}` + ).join('\n') + : '(no errors or warnings found)'; + contextFiles.push({ + name: diagnostics.length > 0 ? `⚠ ${errCount}E ${warnCount}W` : '⚠ no errors', + title: diagnostics.length > 0 ? `${errCount} error(s), ${warnCount} warning(s)` : 'No errors or warnings', + path: '__errors__', + isErrors: true, + content, + }); + renderContextFiles(); + } + + // ── @openfiles β€” show open editor tabs in autocomplete ──────────────────── + function requestOpenFiles() { + vscode.postMessage({ type: 'getOpenEditors' }); + } + + // ── Image paste chip ────────────────────────────────────────────────────── + function addImageContext(name, dataUrl) { + if (!contextFilesEl) return; + contextFiles.push({ name, path: `__img__:${name}`, isImage: true, dataUrl }); + renderContextFiles(); + } + + // ── Toggle pin on a context file ───────────────────────────────────────── + function togglePinFile(file) { + file.pinned = !file.pinned; + if (file.pinned) { + if (!pinnedFiles.find(p => p.path === file.path)) + pinnedFiles.push({ name: file.name, path: file.path }); + vscode.postMessage({ type: 'pinFile', path: file.path }); + } else { + pinnedFiles = pinnedFiles.filter(p => p.path !== file.path); + vscode.postMessage({ type: 'unpinFile', path: file.path }); + } + renderContextFiles(); + } + + // ── Context window usage bar ────────────────────────────────────────────── + function updateContextBar() { + if (!contextBarEl || !contextUsedEl || !contextMaxEl || !contextFillEl) return; + const used = (tokenStats.input || 0) + (tokenStats.output || 0); + const max = MODEL_CONTEXT[currentModel] || 200000; + const pct = Math.min((used / max) * 100, 100); + contextUsedEl.textContent = used >= 1000 ? `${(used / 1000).toFixed(1)}k` : String(used); + contextMaxEl.textContent = `${(max / 1000).toFixed(0)}k`; + contextFillEl.style.width = pct + '%'; + contextFillEl.style.background = pct > 95 ? 'var(--error-text)' + : pct > 80 ? 'var(--warning)' + : 'var(--success)'; + contextBarEl.style.display = used > 0 ? '' : 'none'; + + // Context-full warning banner + if (contextWarningEl) { + if (pct > 85) { + if (contextWarningTextEl) { + contextWarningTextEl.textContent = + `Context ${Math.round(pct)}% full β€” responses may degrade soon.`; + } + contextWarningEl.style.display = ''; + } else { + contextWarningEl.style.display = 'none'; + } + } + } + + // ── Conversation search ─────────────────────────────────────────────────── + let searchMatches = []; + let searchCurrentIdx = -1; + + function showSearchBar() { + if (!searchBar) return; + searchBar.style.display = ''; + if (searchInput) { searchInput.focus(); searchInput.select(); } + } + + function hideSearchBar() { + if (!searchBar) return; + searchBar.style.display = 'none'; + clearSearchHighlights(); + if (searchInput) searchInput.value = ''; + if (searchCount) searchCount.textContent = ''; + } + + function clearSearchHighlights() { + messagesEl.querySelectorAll('.search-match, .search-match-current').forEach(el => { + el.classList.remove('search-match', 'search-match-current'); + }); + searchMatches = []; + searchCurrentIdx = -1; + } + + function performSearch(query) { + clearSearchHighlights(); + if (!query || query.length < 2) { + if (searchCount) searchCount.textContent = ''; + return; + } + const lq = query.toLowerCase(); + messagesEl.querySelectorAll('.msg-user, .msg-assistant').forEach(el => { + if ((el.textContent || '').toLowerCase().includes(lq)) { + el.classList.add('search-match'); + searchMatches.push(el); + } + }); + if (searchMatches.length > 0) { + searchCurrentIdx = 0; + searchMatches[0].classList.add('search-match-current'); + searchMatches[0].scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + if (searchCount) { + searchCount.textContent = searchMatches.length > 0 + ? `${searchCurrentIdx + 1}/${searchMatches.length}` + : 'No results'; + } + } + + function searchNav(dir) { + if (searchMatches.length === 0) return; + searchMatches[searchCurrentIdx]?.classList.remove('search-match-current'); + searchCurrentIdx = (searchCurrentIdx + dir + searchMatches.length) % searchMatches.length; + searchMatches[searchCurrentIdx].classList.add('search-match-current'); + searchMatches[searchCurrentIdx].scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + if (searchCount) searchCount.textContent = `${searchCurrentIdx + 1}/${searchMatches.length}`; + } + + if (searchInput) { + searchInput.addEventListener('input', () => performSearch(searchInput.value.trim())); + searchInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { e.preventDefault(); searchNav(e.shiftKey ? -1 : 1); } + if (e.key === 'Escape') hideSearchBar(); + }); + } + if (searchPrevBtn) searchPrevBtn.addEventListener('click', () => searchNav(-1)); + if (searchNextBtn) searchNextBtn.addEventListener('click', () => searchNav(1)); + if (searchCloseBtn) searchCloseBtn.addEventListener('click', hideSearchBar); + + document.addEventListener('keydown', (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'f') { + e.preventDefault(); + showSearchBar(); + } + // Ctrl+L β€” focus the chat input (standard AI IDE shortcut) + if ((e.ctrlKey || e.metaKey) && e.key === 'l') { + e.preventDefault(); + if (inputEl) { + inputEl.focus(); + inputEl.select(); + } + } + }); + window.addEventListener('message', (event) => { + const msg = event.data; + switch (msg.type) { + case 'stream_event': + appendStreamText(msg.text || ''); + break; + + case 'assistant': + if (msg.content && !msg._streamed) { + finalizeAssistantMessage(msg.content); + } else { + finalizeAssistantMessage(null); + } + break; + + case 'thinking': + addThinkingBlock(msg.text); + break; + + case 'tool_progress': + addToolCard(msg.tool); + setLoading(true, `Running ${msg.tool}…`); + break; + + case 'result': + updateToolCard(msg.tool, typeof msg.result === 'string' ? msg.result : JSON.stringify(msg.result, null, 2)); + break; + + case 'compaction': + addSystemMessage(`⟳ Context compacted (pass ${msg.count})`); + break; + + case 'hookPermissionResult': + if (!msg.allowed) { + addSystemMessage(`β›” Tool blocked by hook: ${msg.tool}`); + } + break; + + case 'error': + finalizeAssistantMessage(null); + addErrorMessage(msg.message); + setLoading(false); + setSending(false); + break; + + case 'retrying': + // Rate-limit auto-retry: show countdown in loading indicator + setLoading(true, `⏳ Rate limited β€” retrying in ${msg.delaySeconds}s (attempt ${msg.attempt}/${msg.maxAttempts})…`); + break; + + case 'stop': + finalizeAssistantMessage(null); + setLoading(false); + setSending(false); + // When the agent loop hits its turn limit, nudge the user to + // click the β–Ά Continue button so the task can keep going. + if (msg.reason === 'max_turns') { + addSystemMessage( + 'βš™ Max tool-use turns reached. Use β–Ά Continue on the last reply to keep going.' + ); + } + // Auto-save current session after every response so VS Code restarts + // don't lose the conversation (Cursor/Claude-style session memory). + if (sessionMessages.length > 0) { + vscode.postMessage({ type: 'autoSaveSession', messages: [...sessionMessages], sessionId: currentSessionId }); + if (currentSessionId) { + // Also update the history entry so it stays current when the + // user opens the history panel without clicking "New" first. + vscode.postMessage({ type: 'updateSession', id: currentSessionId, messages: [...sessionMessages] }); + } + } + break; + + case 'tokenUpdate': + tokenStats = msg.tokens || tokenStats; + costTotal = msg.cost || costTotal; + updateStats(); + updateContextBar(); + break; + + case 'modelChanged': + currentModel = msg.model || currentModel; + if (modelSelect) modelSelect.value = msg.model || ''; + syncThinkingToggleVisibility(currentModel); + updateStats(); + updateContextBar(); + break; + + case 'sessionCleared': + messagesEl.innerHTML = ''; + if (welcomeEl) welcomeEl.classList.remove('hidden'); + currentStreamMsg = null; + activeToolCards = {}; + sessionMessages = []; + currentSessionId = null; + currentSessionTitle = ''; + updateSessionIndicator(); + tokenStats = { input: 0, output: 0 }; + costTotal = 0; + startTime = Date.now(); + streamStartTime = 0; + streamOutputChars = 0; + lastStreamTps = 0; + if (statsSpeedItem) statsSpeedItem.style.display = 'none'; + // Restore pinned files into context + contextFiles = pinnedFiles.map(f => ({ ...f, pinned: true })); + renderContextFiles(); + updateStats(); + updateContextBar(); + // Clear the auto-saved active session + vscode.postMessage({ type: 'autoSaveSession', messages: [] }); + // Refresh recent sessions for the newly-visible welcome screen + if (allHistorySessions.length > 0) { + updateWelcomeRecentSessions(allHistorySessions); + } else { + vscode.postMessage({ type: 'getHistory' }); + } + break; + + case 'historyData': + allHistorySessions = msg.sessions || []; + renderHistoryList(allHistorySessions, historyFilterQuery); + updateWelcomeRecentSessions(allHistorySessions); + break; + + case 'sessionData': + renderHistorySession(msg.messages || [], msg.id || null); + break; + + case 'resumeFromHistoryData': + // Direct-resume (Cursor-style): triggered when user clicks a history item + restoreSessionMessages(msg.messages || [], msg.id || null); + break; + + case 'fileContent': + addContextFile(msg.name, msg.path); + break; + + case 'activeFileContent': + // Arrives after user clicks "Apply to file…" + if (pendingApply && applyModal.classList.contains('visible')) { + showApplyModal(pendingApply.code, msg.content, msg.fileName); + } + break; + + case 'pinnedFiles': + pinnedFiles = (msg.files || []); + for (const f of pinnedFiles) { + addContextFile(f.name, f.path, true); + } + break; + + case 'inlineEditRequest': + if (inputEl && msg.selectedText) { + const ctx = msg.hasSelection + ? `Edit this code from \`${msg.fileName}\`:\n\`\`\`\n${msg.selectedText}\n\`\`\`\n\nInstruction: ` + : `Regarding \`${msg.fileName}\`: `; + inputEl.value = ctx; + inputEl.style.height = 'auto'; + inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px'; + inputEl.focus(); + inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length); + } + break; + + case 'initialized': + currentModel = msg.model || 'claude-sonnet-4-6'; + if (modelSelect && msg.model) modelSelect.value = msg.model; + if (modeSelect && msg.mode) modeSelect.value = msg.mode; + if (thinkingToggleEl) { + thinkingToggleEl.checked = !!msg.thinkingMode; + if (thinkingLabelEl) thinkingLabelEl.classList.toggle('active', !!msg.thinkingMode); + } + syncThinkingToggleVisibility(currentModel); + // Restore auto-attach state from settings + autoAttachActive = !!msg.autoAttachActiveFile; + if (autoAttachBtn) autoAttachBtn.classList.toggle('active', autoAttachActive); + updateStats(); + updateContextBar(); + // Restore active session if one was persisted (survives VS Code restarts) + if (msg.activeSession && msg.activeSession.length > 0) { + restoreSessionMessages(msg.activeSession, msg.activeSessionId || null); + } else { + showWelcome(!!msg.hasApiKey); + } + // Load pinned files from settings + vscode.postMessage({ type: 'getPinnedFiles' }); + break; + + case 'apiKeySet': + showWelcome(true); + break; + + case 'gitContext': + addGitContext(msg.content || '', msg.branch || ''); + break; + + case 'workspaceDiagnostics': + addErrorsContext(msg.diagnostics || []); + break; + + case 'openEditors': + renderAutocomplete(msg.files || []); + break; + + case 'autoAttachState': + autoAttachActive = !!msg.enabled; + if (autoAttachBtn) autoAttachBtn.classList.toggle('active', autoAttachActive); + break; + + default: + break; + } + }); + + // ── Welcome / onboarding ────────────────────────────────────────────────── + const setupGuideEl = document.getElementById('setup-guide'); + const welcomeNormalEl = document.getElementById('welcome-normal'); + + function showWelcome(hasKey) { + if (!setupGuideEl || !welcomeNormalEl) return; + if (hasKey) { + setupGuideEl.style.display = 'none'; + welcomeNormalEl.style.display = 'flex'; + // Pre-populate recent sessions if already loaded; otherwise fetch + if (allHistorySessions.length > 0) { + updateWelcomeRecentSessions(allHistorySessions); + } else { + vscode.postMessage({ type: 'getHistory' }); + } + } else { + setupGuideEl.style.display = 'flex'; + welcomeNormalEl.style.display = 'none'; + if (welcomeRecentEl) welcomeRecentEl.style.display = 'none'; + } + } + + /** + * Render the top-3 most recent sessions on the welcome screen so users can + * instantly resume a conversation without opening the history panel. + */ + function updateWelcomeRecentSessions(sessions) { + if (!welcomeRecentEl || !welcomeRecentList) return; + // Only show when the welcome screen is actually visible + if (!welcomeEl || welcomeEl.classList.contains('hidden')) { + welcomeRecentEl.style.display = 'none'; + return; + } + const recent = sessions.slice(0, 3); + if (recent.length === 0) { + welcomeRecentEl.style.display = 'none'; + return; + } + welcomeRecentEl.style.display = ''; + welcomeRecentList.innerHTML = ''; + for (const s of recent) { + const item = document.createElement('button'); + item.className = 'welcome-recent-item'; + + const titleDiv = document.createElement('div'); + titleDiv.className = 'welcome-recent-title'; + titleDiv.textContent = s.title || 'Untitled conversation'; + + const metaDiv = document.createElement('div'); + metaDiv.className = 'welcome-recent-meta'; + const date = new Date(s.createdAt); + const msgCount = s.messageCount || 0; + metaDiv.textContent = `${date.toLocaleDateString()} Β· ${msgCount} msg${msgCount !== 1 ? 's' : ''}`; + + item.appendChild(titleDiv); + item.appendChild(metaDiv); + item.addEventListener('click', () => { + vscode.postMessage({ type: 'resumeFromHistory', id: s.id }); + }); + welcomeRecentList.appendChild(item); + } + } + + // Wire provider links to open in browser via extension + ['link-anthropic', 'link-openai', 'link-google', 'link-nvidia'].forEach(id => { + const el = document.getElementById(id); + if (!el) return; + el.addEventListener('click', (e) => { + e.preventDefault(); + vscode.postMessage({ type: 'runCommand', command: 'vscode.open', args: [el.href] }); + }); + }); + + // "Set API Key" button in setup guide + const btnSetKey = document.getElementById('btn-set-key'); + if (btnSetKey) { + btnSetKey.addEventListener('click', () => { + vscode.postMessage({ type: 'runCommand', command: 'openClaudeCode.setApiKey' }); + }); + } + + // "Open Settings" button + const btnOpenSettings = document.getElementById('btn-open-settings'); + if (btnOpenSettings) { + btnOpenSettings.addEventListener('click', () => { + vscode.postMessage({ type: 'runCommand', command: 'workbench.action.openSettings', args: ['openClaudeCode'] }); + }); + } + + // Example prompts (setup guide + normal welcome) β€” click to fill input + ['ex-1','ex-2','ex-3','ex-w-1','ex-w-2','ex-w-3'].forEach(id => { + const el = document.getElementById(id); + if (!el) return; + el.addEventListener('click', () => { + if (inputEl) { + inputEl.value = el.textContent.trim(); + inputEl.focus(); + inputEl.dispatchEvent(new Event('input')); + } + }); + }); + + // ── Loading / sending state ─────────────────────────────────────────────── + function setLoading(on, text) { + isLoading = on; + if (loadingEl) loadingEl.classList.toggle('visible', on); + if (loadingText && text) loadingText.textContent = text; + else if (loadingText && !on) loadingText.textContent = 'Thinking…'; + } + + function setSending(on) { + if (sendBtn) sendBtn.disabled = on; + if (stopBtn) stopBtn.classList.toggle('visible', on); + if (!on) setLoading(false); + } + + // ── Stats bar ───────────────────────────────────────────────────────────── + function updateStats() { + if (statsModel) statsModel.textContent = currentModel || 'β€”'; + if (statsTokens) { + const total = (tokenStats.input || 0) + (tokenStats.output || 0); + statsTokens.textContent = total >= 1000 ? `${(total/1000).toFixed(1)}K` : String(total); + } + if (statsCost) { + statsCost.textContent = costTotal < 0.01 + ? `$${costTotal.toFixed(4)}` + : `$${costTotal.toFixed(3)}`; + } + if (statsMsgsEl) { + // Count completed user-assistant exchange pairs (floor division) + const msgs = Math.floor(sessionMessages.length / 2); + statsMsgsEl.textContent = msgs > 0 ? String(msgs) : '0'; + } + } + + // ── Context files ───────────────────────────────────────────────────────── + function addContextFile(name, filePath, pinned = false) { + if (contextFiles.find(f => f.path === filePath)) return; + contextFiles.push({ name, path: filePath, pinned }); + renderContextFiles(); + } + + function removeContextFile(filePath) { + const file = contextFiles.find(f => f.path === filePath); + if (file && file.pinned) { + // Unpin when removed + pinnedFiles = pinnedFiles.filter(p => p.path !== filePath); + vscode.postMessage({ type: 'unpinFile', path: filePath }); + } + contextFiles = contextFiles.filter(f => f.path !== filePath); + renderContextFiles(); + vscode.postMessage({ type: 'removeContextFile', path: filePath }); + } + + function renderContextFiles() { + if (!contextFilesEl) return; + contextFilesEl.innerHTML = ''; + for (const f of contextFiles) { + const chip = document.createElement('div'); + chip.className = 'context-chip' + + (f.pinned ? ' pinned' : '') + + (f.isGit ? ' chip-git' : '') + + (f.isErrors ? ' chip-errors' : ''); + if (f.title) chip.title = f.title; + + const nameSpan = document.createElement('span'); + // Special chips carry their icon in the name; files get an icon prefix + const icon = f.isImage ? 'πŸ–Ό ' : (f.isCodebase || f.isGit || f.isErrors) ? '' : (f.pinned ? 'πŸ“Œ ' : 'πŸ“„ '); + nameSpan.textContent = icon + f.name; + + if (f.isImage && f.dataUrl) { + const img = document.createElement('img'); + // dataUrl is always a data: URI from FileReader.readAsDataURL β€” validate before use + if (/^data:image\/[a-z]+;base64,/.test(f.dataUrl)) { + img.src = f.dataUrl; + } + img.alt = f.name; + chip.appendChild(img); + } + + if (!f.isCodebase && !f.isImage && !f.isGit && !f.isErrors) { + const pinBtn = document.createElement('button'); + pinBtn.className = 'pin-btn'; + pinBtn.title = f.pinned ? 'Unpin file' : 'Pin to all sessions'; + pinBtn.textContent = 'πŸ“Ž'; + pinBtn.addEventListener('click', (e) => { e.stopPropagation(); togglePinFile(f); }); + chip.appendChild(nameSpan); + chip.appendChild(pinBtn); + } else { + chip.appendChild(nameSpan); + } + + const removeBtn = document.createElement('button'); + removeBtn.textContent = 'Γ—'; + removeBtn.addEventListener('click', () => removeContextFile(f.path)); + chip.appendChild(removeBtn); + contextFilesEl.appendChild(chip); + } + contextFilesEl.style.display = contextFiles.length ? 'flex' : 'none'; + } + + // ── Input handling ──────────────────────────────────────────────────────── + if (inputEl) { + inputEl.addEventListener('input', () => { + // Auto-resize + inputEl.style.height = 'auto'; + inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px'; + + // Character count hint + if (charCountEl) { + const len = inputEl.value.length; + if (len > CHAR_COUNT_MIN_DISPLAY) { + charCountEl.textContent = len >= 1000 ? `${(len/1000).toFixed(1)}k chars` : `${len} chars`; + charCountEl.classList.toggle('warn', len > CHAR_COUNT_WARNING_THRESHOLD); + } else { + charCountEl.textContent = ''; + charCountEl.classList.remove('warn'); + } + } + + const val = inputEl.value; + const cursorPos = inputEl.selectionStart; + const before = val.slice(0, cursorPos); + + // @codebase special mention β€” replace with chip immediately + if (val.includes('@codebase')) { + inputEl.value = val.replace(/@codebase\b/g, '').trim(); + inputEl.style.height = 'auto'; + inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px'; + addCodebaseContext(); + hideAutocomplete(); + return; + } + + // @git β€” inject git status as context chip + if (/@git\b/.test(val)) { + inputEl.value = val.replace(/@git\b/g, '').trim(); + inputEl.style.height = 'auto'; + inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px'; + requestGitContext(); + hideAutocomplete(); + return; + } + + // @errors β€” inject workspace diagnostics as context chip + if (/@errors\b/.test(val)) { + inputEl.value = val.replace(/@errors\b/g, '').trim(); + inputEl.style.height = 'auto'; + inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px'; + requestErrorsContext(); + hideAutocomplete(); + return; + } + + // @openfiles β€” show open editor tabs in autocomplete + if (/@openfiles\b/.test(val)) { + inputEl.value = val.replace(/@openfiles\b/g, '').trim(); + inputEl.style.height = 'auto'; + inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px'; + requestOpenFiles(); + // keep autocomplete open β€” it will be populated when response arrives + return; + } + + // Slash command autocomplete (when first char is /) + const slashMatch = val.match(/^(\/[\w]*)$/); + if (slashMatch) { + showSlashCommands(slashMatch[1].slice(1)); + return; + } + + // @mention autocomplete + const atMatch = before.match(/@([\w./\\-]*)$/); + if (atMatch) { + showFileAutocomplete(atMatch[1]); + } else { + hideAutocomplete(); + } + }); + + inputEl.addEventListener('keydown', (e) => { + // Submit on Enter (not Shift+Enter) or Ctrl/Cmd+Enter + if (e.key === 'Enter' && (!e.shiftKey || e.ctrlKey || e.metaKey) && !autocompleteEl.classList.contains('visible')) { + e.preventDefault(); + submitMessage(); + return; + } + // Autocomplete navigation + if (autocompleteEl.classList.contains('visible')) { + if (e.key === 'ArrowDown') { e.preventDefault(); moveAcSelection(1); return; } + if (e.key === 'ArrowUp') { e.preventDefault(); moveAcSelection(-1); return; } + if (e.key === 'Enter' || e.key === 'Tab') { + e.preventDefault(); + selectAcItem(); + return; + } + if (e.key === 'Escape') { hideAutocomplete(); return; } + } + // Up arrow in empty input β†’ recall last message + if (e.key === 'ArrowUp' && !inputEl.value && !autocompleteEl.classList.contains('visible')) { + if (lastUserMessage) { + e.preventDefault(); + inputEl.value = lastUserMessage; + inputEl.style.height = 'auto'; + inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px'; + inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length); + } + return; + } + // Escape cancels loading + if (e.key === 'Escape' && isLoading) { + vscode.postMessage({ type: 'cancel' }); + setSending(false); + } + }); + + // Image paste + inputEl.addEventListener('paste', (e) => { + const items = [...(e.clipboardData?.items || [])]; + const imgItem = items.find(it => it.type.startsWith('image/')); + if (!imgItem) return; + e.preventDefault(); + const file = imgItem.getAsFile(); + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => addImageContext(file.name || 'screenshot.png', ev.target.result); + reader.readAsDataURL(file); + }); + } + + // ── Drag-and-drop files onto input ──────────────────────────────────────── + const inputWrapper = document.getElementById('input-wrapper'); + if (inputWrapper) { + const CODE_EXTS = new Set([ + 'js','ts','jsx','tsx','py','go','rs','java','cpp','c','h','cs','rb', + 'php','swift','kt','scala','sh','bash','zsh','md','json','yaml','yml', + 'toml','html','css','scss','less','vue','svelte','sql','graphql','tf','txt', + ]); + inputWrapper.addEventListener('dragover', (e) => { + e.preventDefault(); + inputWrapper.classList.add('drag-over'); + }); + inputWrapper.addEventListener('dragleave', () => inputWrapper.classList.remove('drag-over')); + inputWrapper.addEventListener('drop', (e) => { + e.preventDefault(); + inputWrapper.classList.remove('drag-over'); + const files = [...(e.dataTransfer?.files || [])]; + for (const file of files) { + const ext = (file.name.split('.').pop() || '').toLowerCase(); + if (CODE_EXTS.has(ext)) { + const filePath = file.path || file.name; + vscode.postMessage({ type: 'addContextFile', path: filePath, name: file.name }); + } + } + }); + } + + if (sendBtn) { + sendBtn.addEventListener('click', submitMessage); + } + + if (stopBtn) { + stopBtn.addEventListener('click', () => { + vscode.postMessage({ type: 'cancel' }); + setSending(false); + }); + } + + // ── Quick Actions toggle ─────────────────────────────────────────────────── + if (actionsBtn && quickActionsPanel) { + actionsBtn.addEventListener('click', () => { + const visible = quickActionsPanel.style.display !== 'none'; + quickActionsPanel.style.display = visible ? 'none' : ''; + actionsBtn.classList.toggle('active', !visible); + }); + // Wire each quick-action button β†’ fill input with template + quickActionsPanel.querySelectorAll('.qa-btn').forEach((btn) => { + btn.addEventListener('click', () => { + const template = btn.dataset.template || ''; + if (!template || !inputEl) return; + inputEl.value = template + '\n'; + inputEl.style.height = 'auto'; + inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px'; + inputEl.focus(); + inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length); + // Collapse the panel after selecting to give the user more space + quickActionsPanel.style.display = 'none'; + actionsBtn.classList.remove('active'); + }); + }); + } + + // ── Mode description bar ───────────────────────────────────────────────── + let modeDescTimeout = null; + function showModeDesc(mode) { + const desc = MODE_DESCRIPTIONS[mode]; + if (!modeDescBar || !modeDescText || !desc) return; + modeDescText.textContent = desc; + modeDescBar.style.display = ''; + clearTimeout(modeDescTimeout); + modeDescTimeout = setTimeout(() => { + if (modeDescBar) modeDescBar.style.display = 'none'; + }, 5000); + } + if (modeSelect) { + modeSelect.addEventListener('change', () => showModeDesc(modeSelect.value)); + } + + function submitMessage() { + if (!inputEl) return; + const rawText = inputEl.value.trim(); + if (!rawText || isLoading) return; + + // Extract @file references from text before sending + const fileRefs = []; + rawText.replace(/@([\w./\\-]+)/g, (_, p) => fileRefs.push(p)); + + addUserMessage(rawText); + inputEl.value = ''; + inputEl.style.height = 'auto'; + hideAutocomplete(); + setSending(true); + setLoading(true, 'Thinking…'); + + // Append inline content from special context chips (@git, @errors) + const specialParts = []; + for (const f of contextFiles) { + if (f.isGit && f.content) specialParts.push('\n\n[Git Context]\n' + f.content); + if (f.isErrors && f.content) specialParts.push('\n\n[Workspace Problems]\n' + f.content); + } + const finalMessage = specialParts.length > 0 ? rawText + specialParts.join('') : rawText; + + // Separate file paths from image/codebase/special context entries + const sendContextFiles = contextFiles + .filter(f => !f.isImage && !f.isCodebase && !f.isGit && !f.isErrors) + .map(f => f.path); + const hasCodebase = contextFiles.some(f => f.isCodebase); + + vscode.postMessage({ + type: 'send', + message: finalMessage, + contextFiles: sendContextFiles, + fileRefs, + useCodebase: hasCodebase, + }); + + // Clear non-pinned context after send + contextFiles = contextFiles.filter(f => f.pinned); + renderContextFiles(); + } + + // ── File autocomplete ───────────────────────────────────────────────────── + let acItems = []; + let acSelectedIdx = -1; + + function showFileAutocomplete(query) { + vscode.postMessage({ type: 'fileSearch', query }); + } + + // ── Slash command autocomplete ───────────────────────────────────────────── + const SLASH_COMMANDS = [ + { cmd: '/clear', desc: 'Clear conversation and start fresh' }, + { cmd: '/new', desc: 'Start a new conversation (alias for /clear)' }, + { cmd: '/model', desc: 'Switch AI model' }, + { cmd: '/export', desc: 'Export conversation as Markdown' }, + { cmd: '/help', desc: 'Show keyboard shortcuts' }, + { cmd: '/pin', desc: 'Pin all current context files to every session' }, + { cmd: '/explain', desc: 'Explain the code/file in context', template: 'Explain what this code does in simple terms:' }, + { cmd: '/fix', desc: 'Find and fix bugs', template: 'Find and fix the bugs in this code. Explain what was wrong:' }, + { cmd: '/refactor', desc: 'Refactor for clarity and maintainability', template: 'Refactor this code to be cleaner and more maintainable:' }, + { cmd: '/test', desc: 'Write unit tests', template: 'Write comprehensive unit tests for this code:' }, + { cmd: '/review', desc: 'Code review for bugs and improvements', template: 'Review this code for potential bugs, security issues, and improvements:' }, + { cmd: '/docs', desc: 'Generate documentation comments', template: 'Generate clear documentation/JSDoc comments for this code:' }, + { cmd: '/commit', desc: 'Generate a git commit message', template: 'Generate a concise git commit message (conventional commits style) for these changes:' }, + { cmd: '/optimize', desc: 'Optimize code for performance', template: 'Optimize this code for better performance and explain the changes:' }, + ]; + + function showSlashCommands(prefix) { + const filtered = SLASH_COMMANDS.filter(c => c.cmd.slice(1).startsWith(prefix)); + if (!filtered.length) { hideAutocomplete(); return; } + acItems = filtered.map(c => ({ isSlash: true, cmd: c.cmd, desc: c.desc })); + acSelectedIdx = 0; + autocompleteEl.innerHTML = ''; + for (let i = 0; i < filtered.length; i++) { + const c = filtered[i]; + const item = document.createElement('div'); + item.className = 'ac-item' + (i === 0 ? ' selected' : ''); + const cmdSpan = document.createElement('span'); + cmdSpan.className = 'ac-name'; + cmdSpan.style.fontWeight = '600'; + cmdSpan.textContent = c.cmd; + const descSpan = document.createElement('span'); + descSpan.className = 'ac-desc'; + descSpan.textContent = c.desc; + item.appendChild(cmdSpan); + item.appendChild(descSpan); + item.addEventListener('click', () => { acSelectedIdx = i; selectAcItem(); }); + autocompleteEl.appendChild(item); + } + autocompleteEl.classList.add('visible'); + } + + function executeSlashCommand(cmd) { + // Check for template commands first + const slashEntry = SLASH_COMMANDS.find(c => c.cmd === cmd); + if (slashEntry && slashEntry.template) { + if (inputEl) { + inputEl.value = slashEntry.template + '\n'; + inputEl.style.height = 'auto'; + inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px'; + inputEl.focus(); + inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length); + } + return; + } + switch (cmd) { + case '/clear': + case '/new': newChatBtn && newChatBtn.click(); break; + case '/model': modelSelect && modelSelect.focus(); break; + case '/export': exportBtn && exportBtn.click(); break; + case '/pin': contextFiles.filter(f => !f.pinned).forEach(f => togglePinFile(f)); break; + case '/help': + addSystemMessage( + 'Keyboard shortcuts: Enter=send Β· Shift+Enter=newline Β· @=add file Β· ' + + '@codebase=full codebase Β· /=commands Β· ↑=recall last message Β· ' + + 'Ctrl+L=focus input Β· Ctrl+F=search Β· Ctrl+K=inline edit Β· Esc=stop\n\n' + + 'Template commands: /explain Β· /fix Β· /refactor Β· /test Β· /review Β· /docs Β· /commit Β· /optimize' + ); + break; + } + } + + function renderAutocomplete(files) { + if (!files || files.length === 0) { hideAutocomplete(); return; } + acItems = files; + acSelectedIdx = 0; + autocompleteEl.innerHTML = ''; + for (let i = 0; i < files.length; i++) { + const f = files[i]; + const item = document.createElement('div'); + item.className = 'ac-item' + (i === 0 ? ' selected' : ''); + item.innerHTML = ` + πŸ“„ + ${escapeHtml(f.name)} + ${escapeHtml(f.relativePath || '')} + `; + item.addEventListener('click', () => { + acSelectedIdx = i; + selectAcItem(); + }); + autocompleteEl.appendChild(item); + } + autocompleteEl.classList.add('visible'); + } + + function hideAutocomplete() { + autocompleteEl.classList.remove('visible'); + acItems = []; + acSelectedIdx = -1; + } + + function moveAcSelection(dir) { + const items = autocompleteEl.querySelectorAll('.ac-item'); + if (items.length === 0) return; + items[acSelectedIdx]?.classList.remove('selected'); + acSelectedIdx = (acSelectedIdx + dir + items.length) % items.length; + items[acSelectedIdx]?.classList.add('selected'); + } + + function selectAcItem() { + if (acSelectedIdx < 0 || acSelectedIdx >= acItems.length) return; + const item = acItems[acSelectedIdx]; + + // Slash command selection + if (item.isSlash) { + inputEl.value = ''; + inputEl.style.height = 'auto'; + hideAutocomplete(); + executeSlashCommand(item.cmd); + return; + } + + // File selection β€” replace @query in input + const file = item; + const val = inputEl.value; + const cursorPos = inputEl.selectionStart; + const before = val.slice(0, cursorPos); + const replaced = before.replace(/@[\w./\\-]*$/, '') + `@${file.name} `; + inputEl.value = replaced + val.slice(cursorPos); + inputEl.setSelectionRange(replaced.length, replaced.length); + hideAutocomplete(); + vscode.postMessage({ type: 'addContextFile', path: file.path, name: file.name }); + } + + // ── History Panel ───────────────────────────────────────────────────────── + /** + * Persist the current in-progress session before switching to another one. + * - If we're continuing a history session, update that entry (no duplicate). + * - If this is a fresh session, create a new history entry. + */ + function saveCurrentSessionIfNeeded() { + if (sessionMessages.length === 0) return; + if (currentSessionId) { + vscode.postMessage({ type: 'updateSession', id: currentSessionId, messages: [...sessionMessages] }); + } else { + vscode.postMessage({ type: 'saveSession', messages: [...sessionMessages] }); + } + } + + /** + * Restore a set of saved messages into the main chat panel and re-inject them + * into the agent bridge, so the model remembers the full conversation context + * (Cursor/Claude-style session memory). + * + * @param {Array} messages - saved user/assistant message objects + * @param {string|null} sessionId - history entry ID being continued; null for a new session + */ + function restoreSessionMessages(messages, sessionId) { + // Track which history session we are editing so we update it (not duplicate it) + currentSessionId = sessionId !== undefined ? sessionId : null; + + // Derive header title from the first user message in this session + const firstUser = messages.find(m => m.type === 'user'); + currentSessionTitle = firstUser ? firstUser.text.trim().slice(0, SESSION_TITLE_DISPLAY_LENGTH).replace(/\n/g, ' ') : ''; + updateSessionIndicator(); + + // Clear current UI state + messagesEl.innerHTML = ''; + sessionMessages = []; + activeToolCards = {}; + tokenStats = { input: 0, output: 0 }; + costTotal = 0; + startTime = Date.now(); + updateStats(); + updateContextBar(); + + if (messages.length === 0) { + showWelcome(true); + return; + } + + // Replay messages into the DOM (addUserMessage / finalizeAssistantMessage + // only touch the DOM and sessionMessages β€” they do not send to the bridge) + for (const m of messages) { + if (m.type === 'user') { + addUserMessage(m.text); + } else if (m.type === 'assistant') { + finalizeAssistantMessage(m.text); + } + } + + // Re-inject the conversation history into the agent bridge so the next + // user message is answered with full knowledge of the entire session. + vscode.postMessage({ type: 'resumeSession', messages }); + } + + function openHistoryPanel() { + if (historyPanel) historyPanel.classList.add('visible'); + if (historySessionView) historySessionView.classList.remove('visible'); + if (historyList) historyList.style.display = ''; + if (historyBackBtn) historyBackBtn.style.display = 'none'; + if (historyPanelTitle) historyPanelTitle.textContent = 'Chat History'; + if (historySearchBar) historySearchBar.classList.remove('hidden'); + // Reset search when panel opens + if (historySearch) historySearch.value = ''; + historyFilterQuery = ''; + vscode.postMessage({ type: 'getHistory' }); + } + + function closeHistoryPanel() { + if (historyPanel) historyPanel.classList.remove('visible'); + } + + function renderHistoryList(sessions, filter) { + if (!historyList) return; + historyList.innerHTML = ''; + const lFilter = (filter || '').toLowerCase().trim(); + const displayed = lFilter + ? sessions.filter(s => (s.title || '').toLowerCase().includes(lFilter)) + : sessions; + if (displayed.length === 0) { + const empty = document.createElement('div'); + empty.className = 'history-empty'; + empty.textContent = lFilter + ? 'No conversations match your search.' + : 'No saved conversations yet.\nStart a new chat and click "New" to save it to history.'; + historyList.appendChild(empty); + return; + } + for (const session of displayed) { + const item = document.createElement('div'); + item.className = 'history-item' + (session.id === currentSessionId ? ' active' : ''); + + const titleEl = document.createElement('div'); + titleEl.className = 'history-item-title'; + titleEl.textContent = session.title || 'Untitled conversation'; + + const metaEl = document.createElement('div'); + metaEl.className = 'history-item-meta'; + const dateEl = document.createElement('span'); + dateEl.textContent = new Date(session.createdAt).toLocaleString(); + const countEl = document.createElement('span'); + const msgCount = session.messageCount || 0; + countEl.textContent = `${msgCount} msg${msgCount !== 1 ? 's' : ''}`; + metaEl.appendChild(dateEl); + metaEl.appendChild(countEl); + + const actionsEl = document.createElement('div'); + actionsEl.className = 'history-item-actions'; + + const renameBtn = document.createElement('button'); + renameBtn.className = 'history-action-btn'; + renameBtn.title = 'Rename'; + renameBtn.textContent = '✏'; + renameBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const newTitle = window.prompt('Rename conversation:', session.title || ''); + if (newTitle !== null && newTitle.trim()) { + vscode.postMessage({ type: 'renameSession', id: session.id, title: newTitle.trim() }); + } + }); + + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'history-action-btn history-delete-btn'; + deleteBtn.title = 'Delete'; + deleteBtn.textContent = 'πŸ—‘'; + deleteBtn.addEventListener('click', (e) => { + e.stopPropagation(); + if (window.confirm('Delete this conversation?')) { + vscode.postMessage({ type: 'deleteSession', id: session.id }); + } + }); + + actionsEl.appendChild(renameBtn); + actionsEl.appendChild(deleteBtn); + + item.appendChild(titleEl); + item.appendChild(metaEl); + item.appendChild(actionsEl); + // Cursor/Claude-style: clicking a session immediately switches to it. + // The current session is auto-saved before switching so nothing is lost. + item.addEventListener('click', () => { + saveCurrentSessionIfNeeded(); + closeHistoryPanel(); + vscode.postMessage({ type: 'resumeFromHistory', id: session.id }); + }); + historyList.appendChild(item); + } + } + + let historyViewMessages = []; // messages of the session currently shown in history panel + let historyViewSessionId = null; // ID of that session + + function renderHistorySession(messages, sessionId) { + if (!historySessionView) return; + historyViewMessages = messages; + historyViewSessionId = sessionId || null; + historySessionView.innerHTML = ''; + + // ── Resume button ──────────────────────────────────────────────────── + if (messages.length > 0) { + const resumeBar = document.createElement('div'); + resumeBar.className = 'history-resume-bar'; + + const resumeBtn = document.createElement('button'); + resumeBtn.className = 'history-resume-btn'; + resumeBtn.textContent = 'β–Ά Continue this conversation'; + resumeBtn.title = 'Switch to this conversation β€” the model will remember everything from the beginning'; + resumeBtn.addEventListener('click', () => { + saveCurrentSessionIfNeeded(); + closeHistoryPanel(); + restoreSessionMessages(historyViewMessages, historyViewSessionId); + }); + + resumeBar.appendChild(resumeBtn); + historySessionView.appendChild(resumeBar); + } + + for (const m of messages) { + if (m.type === 'user') { + const div = document.createElement('div'); + div.className = 'msg msg-user'; + div.innerHTML = ` +
    You
    +
    ${escapeHtml(m.text)}
    + `; + historySessionView.appendChild(div); + } else if (m.type === 'assistant') { + const div = document.createElement('div'); + div.className = 'msg msg-assistant'; + div.innerHTML = ` +
    +
    ✦
    + Claude +
    +
    ${renderMarkdown(m.text)}
    + `; + addCopyButtonToMessage(div, m.text); + historySessionView.appendChild(div); + } + } + + // Show session view, hide list + if (historyList) historyList.style.display = 'none'; + historySessionView.classList.add('visible'); + if (historyBackBtn) historyBackBtn.style.display = ''; + if (historyPanelTitle) historyPanelTitle.textContent = 'Past Conversation'; + if (historySearchBar) historySearchBar.classList.add('hidden'); + historySessionView.scrollTop = 0; + } + + if (historyBtn) historyBtn.addEventListener('click', openHistoryPanel); + if (historyCloseBtn) historyCloseBtn.addEventListener('click', closeHistoryPanel); + + // Filter history list as user types + if (historySearch) { + historySearch.addEventListener('input', () => { + historyFilterQuery = historySearch.value.trim(); + renderHistoryList(allHistorySessions, historyFilterQuery); + }); + historySearch.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + historySearch.value = ''; + historyFilterQuery = ''; + renderHistoryList(allHistorySessions, ''); + } + }); + } + if (historyBackBtn) { + historyBackBtn.addEventListener('click', () => { + if (historySessionView) historySessionView.classList.remove('visible'); + if (historyList) historyList.style.display = ''; + if (historyBackBtn) historyBackBtn.style.display = 'none'; + if (historyPanelTitle) historyPanelTitle.textContent = 'Chat History'; + if (historySearchBar) historySearchBar.classList.remove('hidden'); + }); + } + + // ── Toolbar buttons ─────────────────────────────────────────────────────── + if (newChatBtn) { + newChatBtn.addEventListener('click', () => { + saveCurrentSessionIfNeeded(); + sessionMessages = []; + currentSessionId = null; + vscode.postMessage({ type: 'clear' }); + // Immediately restore pinned files for the new session + contextFiles = pinnedFiles.map(f => ({ ...f, pinned: true })); + renderContextFiles(); + }); + } + + if (addFileBtn) { + addFileBtn.addEventListener('click', () => { + vscode.postMessage({ type: 'pickFile' }); + }); + } + + if (activeFileBtn) { + activeFileBtn.addEventListener('click', () => { + vscode.postMessage({ type: 'addActiveFile' }); + }); + } + + if (settingsBtn) { + settingsBtn.addEventListener('click', () => { + vscode.postMessage({ type: 'runCommand', command: 'workbench.action.openSettings', args: ['openClaudeCode'] }); + }); + } + + // Export conversation as Markdown + if (exportBtn) { + exportBtn.addEventListener('click', () => { + if (sessionMessages.length === 0) return; + const lines = sessionMessages.map(m => + m.type === 'user' + ? `## You\n\n${m.text}` + : `## Claude\n\n${m.text}` + ); + const markdown = lines.join('\n\n---\n\n'); + vscode.postMessage({ type: 'exportConversation', markdown }); + }); + } + + // Auto-scroll toggle + if (autoscrollBtn) { + autoscrollBtn.addEventListener('click', () => { + autoScroll = !autoScroll; + autoscrollBtn.classList.toggle('locked', !autoScroll); + autoscrollBtn.title = autoScroll ? 'Auto-scroll: On (click to lock)' : 'Auto-scroll: Off (click to unlock)'; + if (autoScroll) scrollToBottom(); + }); + } + + // Re-enable auto-scroll when user scrolls to the bottom + messagesEl.addEventListener('scroll', () => { + const atBottom = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < 60; + if (atBottom && !autoScroll) { + autoScroll = true; + if (autoscrollBtn) { + autoscrollBtn.classList.remove('locked'); + autoscrollBtn.title = 'Auto-scroll: On (click to lock)'; + } + } else if (!atBottom && autoScroll && isLoading) { + // User scrolled up while response is streaming β€” pause auto-scroll + autoScroll = false; + if (autoscrollBtn) { + autoscrollBtn.classList.add('locked'); + autoscrollBtn.title = 'Auto-scroll: Off (click to unlock)'; + } + } + }); + + if (modelSelect) { + modelSelect.addEventListener('change', () => { + vscode.postMessage({ type: 'model', model: modelSelect.value }); + currentModel = modelSelect.value; + updateStats(); + syncThinkingToggleVisibility(modelSelect.value); + }); + } + + if (modeSelect) { + modeSelect.addEventListener('change', () => { + vscode.postMessage({ type: 'mode', mode: modeSelect.value }); + }); + } + + // ── Thinking mode toggle (NVIDIA capable models only) ───────────────────── + function syncThinkingToggleVisibility(model) { + const visible = THINKING_CAPABLE_MODELS.has(model); + const display = visible ? '' : 'none'; + if (thinkingToggleWrapper) thinkingToggleWrapper.style.display = display; + if (thinkingLabelEl) thinkingLabelEl.style.display = display; + } + + if (thinkingToggleEl) { + thinkingToggleEl.addEventListener('change', () => { + const enabled = thinkingToggleEl.checked; + if (thinkingLabelEl) thinkingLabelEl.classList.toggle('active', enabled); + vscode.postMessage({ type: 'thinkingMode', enabled }); + }); + } + + // ── Message from extension: fileSearchResults ───────────────────────────── + window.addEventListener('message', (event) => { + if (event.data.type === 'fileSearchResults') { + renderAutocomplete(event.data.files || []); + } + }); + + // ── Context inject buttons ───────────────────────────────────────────────── + if (gitBtn) { + gitBtn.addEventListener('click', () => requestGitContext()); + } + if (errorsBtn) { + errorsBtn.addEventListener('click', () => requestErrorsContext()); + } + if (autoAttachBtn) { + autoAttachBtn.addEventListener('click', () => { + vscode.postMessage({ type: 'toggleAutoAttach' }); + }); + } + if (contextWarningNewBtn) { + contextWarningNewBtn.addEventListener('click', () => { + newChatBtn && newChatBtn.click(); + }); + } + + // ── Signal ready ────────────────────────────────────────────────────────── + vscode.postMessage({ type: 'ready' }); + +}()); diff --git a/vscode-extension/media/icon.svg b/vscode-extension/media/icon.svg new file mode 100644 index 0000000..af03488 --- /dev/null +++ b/vscode-extension/media/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/vscode-extension/open-claude-code-1.4.0.vsix b/vscode-extension/open-claude-code-1.4.0.vsix new file mode 100644 index 0000000..36d89c1 Binary files /dev/null and b/vscode-extension/open-claude-code-1.4.0.vsix differ diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json new file mode 100644 index 0000000..bda3a49 --- /dev/null +++ b/vscode-extension/package-lock.json @@ -0,0 +1,2448 @@ +{ + "name": "open-claude-code", + "version": "1.4.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "open-claude-code", + "version": "1.4.0", + "license": "MIT", + "devDependencies": { + "@types/vscode": "^1.90.0", + "@vscode/vsce": "^2.32.0" + }, + "engines": { + "vscode": "^1.90.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.7.0.tgz", + "integrity": "sha512-uYbJ0YarxkVGWEq814BysJry/IPvpDNkVKmc2bMZp4G+igUQkJ5nlFirycwPGUeA9ICLQqCxqExCA1Z1E07bPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.5.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.5.0.tgz", + "integrity": "sha512-i3eS/5pmxDbIU/mLMENs88Qg3k6XxqJytJy6PpB7L1tCBjdXHJDadCD3Hu1TyTooe7iQo7CYqbocgL/l/8u90g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.1.3.tgz", + "integrity": "sha512-LqT8mRZpEils9zGR9eW+Ljqifh2aMA99UF/X0jxIKDYZeHr6onlHwhVP4xHCeLhh55BI63JCbdf1iWJbMh1mPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.5.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@types/vscode": { + "version": "1.116.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.116.0.tgz", + "integrity": "sha512-sYHp4MO6BqJ2PD7Hjt0hlIS3tMaYsVPJrd0RUjDJ8HtOYnyJIEej0bLSccM8rE77WrC+Xox/kdBwEFDO8MsxNA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@vscode/vsce": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.32.0.tgz", + "integrity": "sha512-3EFJfsgrSftIqt3EtdRcAygy/OJ3hstyI1cDmIgkU9CFZW5C+3djr6mfosndCUqcVYuyjmxOK1xmFp/Bq7+NIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^6.2.1", + "form-data": "^4.0.0", + "glob": "^7.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^12.3.2", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^7.5.2", + "tmp": "^0.2.1", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 16" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", + "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + } + } +} diff --git a/vscode-extension/package.json b/vscode-extension/package.json new file mode 100644 index 0000000..b0ba7c6 --- /dev/null +++ b/vscode-extension/package.json @@ -0,0 +1,216 @@ +{ + "name": "open-claude-code", + "displayName": "Open Claude Code", + "description": "AI coding assistant powered by Claude β€” Cursor-style chat panel with file context, code apply, tool visualization and more", + "version": "1.4.0", + "publisher": "open-claude-code", + "engines": { + "vscode": "^1.90.0" + }, + "categories": [ + "AI", + "Chat" + ], + "keywords": [ + "claude", + "ai", + "chat", + "coding assistant", + "anthropic", + "cursor" + ], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./extension.js", + "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "claude-code-sidebar", + "title": "Claude Code", + "icon": "media/icon.svg" + } + ] + }, + "views": { + "claude-code-sidebar": [ + { + "type": "webview", + "id": "claudeCode.chatView", + "name": "Chat", + "retainContextWhenHidden": true + } + ] + }, + "chatParticipants": [ + { + "id": "open-claude-code.claude", + "fullName": "Open Claude Code", + "name": "claude", + "description": "AI coding assistant powered by Claude. Ask about your code, run tools, edit files.", + "isSticky": true, + "commands": [ + { + "name": "clear", + "description": "Clear conversation history and start a new session" + }, + { + "name": "model", + "description": "Switch the model (e.g. @claude /model claude-opus-4-6)" + } + ] + } + ], + "commands": [ + { + "command": "openClaudeCode.setApiKey", + "title": "Open Claude Code: Set API Key" + }, + { + "command": "openClaudeCode.clearSession", + "title": "Open Claude Code: Clear Session" + }, + { + "command": "openClaudeCode.showStatus", + "title": "Open Claude Code: Show Status" + }, + { + "command": "openClaudeCode.openChat", + "title": "Open Claude Code: Open Chat Panel" + }, + { + "command": "openClaudeCode.applyCode", + "title": "Open Claude Code: Apply Code to Active File" + }, + { + "command": "openClaudeCode.inlineEdit", + "title": "Open Claude Code: Inline Edit Selection (Ctrl+K)" + } + ], + "keybindings": [ + { + "command": "openClaudeCode.openChat", + "key": "ctrl+alt+c", + "mac": "cmd+alt+c" + }, + { + "command": "openClaudeCode.inlineEdit", + "key": "ctrl+k", + "when": "editorTextFocus" + } + ], + "configuration": { + "title": "Open Claude Code", + "properties": { + "openClaudeCode.model": { + "type": "string", + "default": "claude-sonnet-4-6", + "enum": [ + "claude-sonnet-4-6", + "claude-haiku-4-5", + "claude-opus-4-6", + "gpt-4o", + "gpt-4o-mini", + "gemini-2.0-flash", + "moonshotai/kimi-k2.5", + "nvidia/llama-3.1-nemotron-70b-instruct", + "meta/llama-3.1-405b-instruct", + "meta/llama-3.3-70b-instruct", + "mistralai/mistral-large-2-instruct", + "mistralai/mixtral-8x22b-instruct-v0.1", + "deepseek-ai/deepseek-r1" + ], + "enumDescriptions": [ + "Anthropic Claude Sonnet 4.6", + "Anthropic Claude Haiku 4.5", + "Anthropic Claude Opus 4.6", + "OpenAI GPT-4o", + "OpenAI GPT-4o Mini", + "Google Gemini 2.0 Flash", + "NVIDIA NIM β€” Moonshot AI Kimi K2.5 (full tool access; enable nvidiaThinkingMode for reasoning mode)", + "NVIDIA NIM β€” Llama 3.1 Nemotron 70B Instruct", + "NVIDIA NIM β€” Meta Llama 3.1 405B Instruct", + "NVIDIA NIM β€” Meta Llama 3.3 70B Instruct", + "NVIDIA NIM β€” Mistral Large 2 Instruct", + "NVIDIA NIM β€” Mixtral 8x22B Instruct", + "NVIDIA NIM β€” DeepSeek R1 (full tool access; enable nvidiaThinkingMode for reasoning mode)" + ], + "description": "AI model to use for the chat participant" + }, + "openClaudeCode.nvidiaApiKey": { + "type": "string", + "default": "", + "description": "NVIDIA NIM API key (from integrate.api.nvidia.com). Required when using any NVIDIA-hosted model." + }, + "openClaudeCode.nvidiaThinkingMode": { + "type": "boolean", + "default": false, + "description": "Enable extended thinking (reasoning) mode for capable NVIDIA NIM models (kimi-k2.5, deepseek-r1). When enabled, the model reasons step-by-step but cannot use live tools (Read, Bash, Grep, etc.) β€” a workspace snapshot is injected instead. When disabled (default), these models use full tool-calling mode just like any other provider." + }, + "openClaudeCode.permissionMode": { + "type": "string", + "enum": [ + "default", + "auto", + "plan", + "acceptEdits", + "bypassPermissions" + ], + "default": "default", + "enumDescriptions": [ + "Ask for permission before each tool use", + "Automatically approve safe operations", + "Plan mode β€” read only, no file edits", + "Automatically accept file edits without asking", + "Bypass all permission checks" + ], + "description": "Permission mode for file and shell operations" + }, + "openClaudeCode.maxTurns": { + "type": "number", + "default": 20, + "minimum": 1, + "maximum": 100, + "description": "Maximum agentic tool-use turns per chat request" + }, + "openClaudeCode.showToolOutput": { + "type": "boolean", + "default": true, + "description": "Show tool progress and results inline in chat" + }, + "openClaudeCode.enableWebviewPanel": { + "type": "boolean", + "default": true, + "description": "Show the Cursor-style sidebar chat panel (requires reload)" + }, + "openClaudeCode.autoAttachActiveFile": { + "type": "boolean", + "default": false, + "description": "Automatically include the currently active editor file as context with every message sent" + }, + "openClaudeCode.pinnedFiles": { + "type": "array", + "default": [], + "items": { "type": "string" }, + "description": "File paths pinned to every new conversation as context" + } + } + } + }, + "scripts": { + "prepackage": "node scripts/prepare-bundle.js", + "package": "vsce package --no-dependencies", + "postpackage": "node scripts/cleanup-bundle.js", + "install-vsix": "code --install-extension open-claude-code-1.4.0.vsix" + }, + "devDependencies": { + "@types/vscode": "^1.90.0", + "@vscode/vsce": "^2.32.0" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/codomium/CODE" + } +} diff --git a/vscode-extension/scripts/cleanup-bundle.js b/vscode-extension/scripts/cleanup-bundle.js new file mode 100644 index 0000000..0efddba --- /dev/null +++ b/vscode-extension/scripts/cleanup-bundle.js @@ -0,0 +1,23 @@ +/** + * cleanup-bundle.js + * + * Post-package script: removes the v2/src copy that was created by + * prepare-bundle.js, keeping the vscode-extension directory clean. + * + * Run automatically via the `postpackage` npm script. + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const DEST = path.resolve(__dirname, '..', 'v2'); + +if (fs.existsSync(DEST)) { + console.log(`Removing bundled copy: ${DEST}`); + fs.rmSync(DEST, { recursive: true, force: true }); + console.log('Done.'); +} else { + console.log('Nothing to clean up.'); +} diff --git a/vscode-extension/scripts/prepare-bundle.js b/vscode-extension/scripts/prepare-bundle.js new file mode 100644 index 0000000..4db8135 --- /dev/null +++ b/vscode-extension/scripts/prepare-bundle.js @@ -0,0 +1,42 @@ +/** + * prepare-bundle.js + * + * Pre-package script: copies the v2/src tree into the extension directory so + * that `vsce package` can include it in the VSIX. + * + * The agent-bridge.mjs subprocess needs v2/src at runtime. When the + * extension is run from source (F5 / development), ../v2/src is available + * via the sibling directory. When installed from a VSIX the extension lives + * in a self-contained folder, so v2/src must be bundled inside it. + * + * Run automatically via the `prepackage` npm script. + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const SRC = path.resolve(__dirname, '..', '..', 'v2', 'src'); +const DEST = path.resolve(__dirname, '..', 'v2', 'src'); + +if (!fs.existsSync(SRC)) { + console.error(`ERROR: Source not found: ${SRC}`); + process.exit(1); +} + +console.log(`Bundling v2/src into extension…`); +console.log(` from: ${SRC}`); +console.log(` to: ${DEST}`); + +if (fs.existsSync(DEST)) { + fs.rmSync(DEST, { recursive: true, force: true }); +} + +// Ensure parent directory exists +fs.mkdirSync(path.dirname(DEST), { recursive: true }); + +// fs.cpSync requires Node β‰₯ 16.7 (satisfied by current VS Code engine requirements). +fs.cpSync(SRC, DEST, { recursive: true }); + +console.log('Done β€” v2/src bundled.');