From 0384076b523f4f3ab3919ed332e8f4f49a2df14b Mon Sep 17 00:00:00 2001 From: Jefferson Raylan Date: Mon, 22 Dec 2025 03:36:44 -0300 Subject: [PATCH 01/18] feat!: add extensible addon system for third-party integrations BREAKING CHANGE: Extension architecture refactored to support addons - Add public API (ISeamlessAgentAPI) for addon extensions - Add addon registry system with lifecycle management - Add UI integration: custom tabs, history providers, settings sections - Add AI tools integration: addons can register LLM tools - Add event system for addon communication - Add storage integration for addon data persistence - Add types-only npm package generation (seamless-agent-addon-api) - Refactor extension entry points for modular architecture - Update CI/CD workflow to publish addon typedefs to npm --- .github/workflows/release-please.yml | 9 + .hintrc | 15 + .vscodeignore | 8 + TODO | 7 + build-addon-typedefs.js | 156 +++++ media/main.css | 269 ++++++++ media/webview.html | 56 ++ package-lock.json | 9 +- package.json | 15 +- package.nls.json | 10 +- package.nls.pt-br.json | 10 +- package.nls.pt.json | 10 +- src/addon-typedefs/index.ts | 71 ++ src/addons/README.md | 22 + src/addons/index.ts | 24 + src/addons/registry.ts | 412 ++++++++++++ src/addons/types.ts | 40 ++ src/agent/index.ts | 79 +++ src/api/README.md | 46 ++ src/api/SeamlessAgentAPI.ts | 485 ++++++++++++++ src/api/events.ts | 199 ++++++ src/api/index.ts | 86 +++ src/api/types.ts | 679 ++++++++++++++++++++ src/core/index.ts | 180 ++++++ src/core/types.ts | 44 ++ src/extension.antigravity.ts | 145 +---- src/extension.ts | 208 +----- src/localization.ts | 10 + src/mcp/apiService.ts | 11 +- src/mcp/mcpServer.ts | 7 +- src/storage/chatHistoryStorage.ts | 13 +- src/tools/askUser.ts | 1 - src/tools/index.ts | 44 +- src/tools/planReview.ts | 13 +- src/types.d.ts | 6 + src/webview/main.ts | 312 ++++++++- src/webview/types.ts | 53 +- src/webview/webviewProvider.ts | 120 +++- tsconfig.addon-typedefs.json | 22 + tsconfig.json | 6 +- typings/addon-typedefs/codiconsLibrary.d.ts | 13 + 41 files changed, 3563 insertions(+), 362 deletions(-) create mode 100644 .hintrc create mode 100644 TODO create mode 100644 build-addon-typedefs.js create mode 100644 src/addon-typedefs/index.ts create mode 100644 src/addons/README.md create mode 100644 src/addons/index.ts create mode 100644 src/addons/registry.ts create mode 100644 src/addons/types.ts create mode 100644 src/agent/index.ts create mode 100644 src/api/README.md create mode 100644 src/api/SeamlessAgentAPI.ts create mode 100644 src/api/events.ts create mode 100644 src/api/index.ts create mode 100644 src/api/types.ts create mode 100644 src/core/index.ts create mode 100644 src/core/types.ts create mode 100644 src/types.d.ts create mode 100644 tsconfig.addon-typedefs.json create mode 100644 typings/addon-typedefs/codiconsLibrary.d.ts diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index bf4468e..100265c 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -95,3 +95,12 @@ jobs: [Provenance attestations](${{ steps.attestation.outputs.attestation-url }}) env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Build addon typedefs package + run: npm run build:addon-typedefs + + - name: Publish addon typedefs to npm + working-directory: ./dist-addon-api + run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.hintrc b/.hintrc new file mode 100644 index 0000000..53b8f54 --- /dev/null +++ b/.hintrc @@ -0,0 +1,15 @@ +{ + "extends": [ + "development" + ], + "hints": { + "compat-api/css": [ + "default", + { + "ignore": [ + "min-height: auto" + ] + } + ] + } +} \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore index c4f2425..a83ef64 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -17,6 +17,14 @@ vsc-extension-quickstart.md **/*.map **/*.ts +# Addon typedefs package output +dist-addon-api/** + +# Typedef build inputs +typings/** +tsconfig.addon-typedefs.json +build-addon-typedefs.js + # All node_modules (bundled with esbuild) node_modules/** diff --git a/TODO b/TODO new file mode 100644 index 0000000..03b7fca --- /dev/null +++ b/TODO @@ -0,0 +1,7 @@ +- Adicionar pause no tool getNextTask +- Resume task +- Usar o markdown do próprio vscode +- Gravar gif +- Atualizar o README.md +- Remover botão de remover de mensagens de tasks que já enviadas ou cuja lista já foi encerrada +- Criar contextos de funcionalidades para poder ser referenciados diff --git a/build-addon-typedefs.js b/build-addon-typedefs.js new file mode 100644 index 0000000..b41af5e --- /dev/null +++ b/build-addon-typedefs.js @@ -0,0 +1,156 @@ +/* eslint-disable no-console */ + +/** + * Gera um pacote "types-only" para autores de addons. + * + * Saída padrão: ./dist-addon-api + * + * O pacote gerado contém: + * - .d.ts (emitDeclarationOnly) + * - package.json minimal + * - README.md (composto a partir de src/api/README.md e src/addons/README.md) + * - LICENSE + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +function parseArgs(argv) { + const args = new Map(); + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a.startsWith('--')) { + const next = argv[i + 1]; + if (!next || next.startsWith('--')) { + args.set(a, true); + } else { + args.set(a, next); + i++; + } + } + } + return args; +} + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function safeReadText(filePath) { + if (!fs.existsSync(filePath)) return ''; + return fs.readFileSync(filePath, 'utf8'); +} + +function ensureDir(dir) { + fs.mkdirSync(dir, { recursive: true }); +} + +function writeFile(filePath, content) { + ensureDir(path.dirname(filePath)); + fs.writeFileSync(filePath, content); +} + +function copyFile(src, dest) { + ensureDir(path.dirname(dest)); + fs.copyFileSync(src, dest); +} + +function rmDir(dir) { + if (fs.existsSync(dir)) { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +function getLocalTscBin(repoRoot) { + const bin = process.platform === 'win32' + ? path.join(repoRoot, 'node_modules', '.bin', 'tsc.cmd') + : path.join(repoRoot, 'node_modules', '.bin', 'tsc'); + + if (fs.existsSync(bin)) return bin; + return null; +} + +function main() { + const repoRoot = __dirname; + const args = parseArgs(process.argv.slice(2)); + + const outDir = path.resolve(repoRoot, args.get('--outDir') || 'dist-addon-api'); + const pkgName = String(args.get('--name') || 'seamless-agent-addon-api'); + + const rootPkg = readJson(path.join(repoRoot, 'package.json')); + const version = String(args.get('--version') || rootPkg.version); + + console.log('[addon-typedefs] output:', outDir); + console.log('[addon-typedefs] package name:', pkgName); + console.log('[addon-typedefs] version:', version); + + rmDir(outDir); + + const tscBin = getLocalTscBin(repoRoot); + const tscCmd = tscBin ? `"${tscBin}"` : 'npx tsc'; + + console.log('[addon-typedefs] generating declarations...'); + execSync(`${tscCmd} -p tsconfig.addon-typedefs.json`, { + cwd: repoRoot, + stdio: 'inherit', + }); + + // Criar index.d.ts na raiz do pacote para facilitar imports + // (reexporta o entrypoint gerado em dist-addon-api/addon-typedefs/index.d.ts) + const indexDts = `/**\n * Seamless Agent Addon API (types-only)\n *\n * Use apenas em contexto de tipos: \`import type { ... }\`.\n */\n\nexport * from './addon-typedefs';\n`; + writeFile(path.join(outDir, 'index.d.ts'), indexDts); + + // README: compor a partir dos READMEs dos módulos + const apiReadme = safeReadText(path.join(repoRoot, 'src', 'api', 'README.md')); + const addonsReadme = safeReadText(path.join(repoRoot, 'src', 'addons', 'README.md')); + + const readmeParts = [ + '# Seamless Agent — Addon API (Types Only)\n', + 'Este pacote contém **apenas definições de tipos** (TypeScript) para autores de addons integrarem com a extensão **Seamless Agent**.\n', + '\n> Dica: use sempre `import type { ... }` para garantir que nada seja importado em runtime.\n', + apiReadme ? `\n---\n\n## API\n\n${apiReadme.trim()}\n` : '', + addonsReadme ? `\n---\n\n## Addons\n\n${addonsReadme.trim()}\n` : '', + ].filter(Boolean); + + writeFile(path.join(outDir, 'README.md'), readmeParts.join('\n')); + + // LICENSE + const licenseSrc = path.join(repoRoot, 'LICENSE.md'); + if (fs.existsSync(licenseSrc)) { + copyFile(licenseSrc, path.join(outDir, 'LICENSE.md')); + } + + // package.json do pacote types-only + const typesPkg = { + name: pkgName, + version, + description: 'Type definitions for Seamless Agent addon extensions', + license: rootPkg.license || 'MIT', + repository: rootPkg.repository, + keywords: ['vscode', 'seamless-agent', 'addon', 'types', 'typings'], + sideEffects: false, + types: './index.d.ts', + exports: { + '.': { + types: './index.d.ts' + } + }, + // Dependências apenas de tipagem/compilação do consumidor + peerDependencies: { + '@types/vscode': '^1.104.0', + '@vscode/codicons': '^0.0.44' + }, + files: [ + '**/*.d.ts', + 'README.md', + 'LICENSE.md' + ] + }; + + writeFile(path.join(outDir, 'package.json'), JSON.stringify(typesPkg, null, 2)); + + console.log('[addon-typedefs] done'); +} + +main(); diff --git a/media/main.css b/media/main.css index fd65b48..9acd587 100644 --- a/media/main.css +++ b/media/main.css @@ -1560,4 +1560,273 @@ button:disabled { height: 2px; border-radius: 2px; background-color: var(--vscode-panelTitle-activeBorder) !important; +} + +/* ================================ + Settings Tab Styles + ================================ */ +.settings-container { + padding: 8px 0; +} + +.settings-header { + margin-bottom: 16px; +} + +.settings-header h3 { + font-size: 14px; + font-weight: 600; + margin: 0 0 4px 0; + color: var(--vscode-foreground); +} + +.settings-description { + font-size: 12px; + color: var(--vscode-descriptionForeground); + margin: 0; +} + +.settings-section { + margin-bottom: 12px; + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + overflow: hidden; +} + +/* Settings Link Section (for VS Code Settings) - styled like a section header */ +.settings-link-section { + margin-bottom: 12px; + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + overflow: hidden; +} + +.settings-link-btn { + width: 100%; + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background-color: var(--vscode-sideBarSectionHeader-background); + border: none; + cursor: pointer; + text-align: left; + color: var(--vscode-foreground); + font-family: inherit; + -webkit-user-select: none; + user-select: none; +} + +.settings-link-btn:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.settings-link-btn:focus { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + +.settings-link-btn .codicon:first-child { + font-size: 12px; +} + +.settings-link-text { + font-size: 13px; + font-weight: 600; + flex: 1; +} + +.settings-link-btn .codicon:last-child { + font-size: 10px; + color: var(--vscode-descriptionForeground); +} + +.settings-section-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background-color: var(--vscode-sideBarSectionHeader-background); + cursor: pointer; + -webkit-user-select: none; + user-select: none; +} + +.settings-section-header:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.settings-section-header h4 { + font-size: 13px; + font-weight: 600; + margin: 0; + flex: 1; +} + +.settings-section-header .codicon { + font-size: 12px; + transition: transform 0.2s ease; +} + +.settings-section.collapsed .settings-section-header .codicon { + transform: rotate(-90deg); +} + +.settings-section-content { + padding: 12px; + border-top: 1px solid var(--vscode-panel-border); +} + +.settings-section.collapsed .settings-section-content { + display: none; +} + +/* Settings Items */ +.setting-item { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 12px; +} + +.setting-item:last-child { + margin-bottom: 0; +} + +.setting-item-row { + display: flex; + align-items: center; + gap: 8px; +} + +.setting-label { + font-size: 13px; + font-weight: 500; + color: var(--vscode-foreground); +} + +.setting-description { + font-size: 12px; + color: var(--vscode-descriptionForeground); + margin: 0; +} + +/* Checkbox setting */ +.setting-checkbox { + display: flex; + align-items: center; + gap: 8px; +} + +.setting-checkbox input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--vscode-checkbox-background); +} + +/* Text/Number input setting */ +.setting-input { + width: 100%; + padding: 6px 8px; + font-size: 13px; + font-family: var(--vscode-font-family); + color: var(--vscode-input-foreground); + background-color: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border); + border-radius: 2px; + outline: none; +} + +.setting-input:focus { + border-color: var(--vscode-focusBorder); +} + +/* Select setting */ +.setting-select { + width: 100%; + padding: 6px 8px; + font-size: 13px; + font-family: var(--vscode-font-family); + color: var(--vscode-dropdown-foreground); + background-color: var(--vscode-dropdown-background); + border: 1px solid var(--vscode-dropdown-border); + border-radius: 2px; + outline: none; + cursor: pointer; +} + +.setting-select:focus { + border-color: var(--vscode-focusBorder); +} + +/* Addon Info Card */ +.addon-card { + display: flex; + flex-direction: column; + gap: 4px; + padding: 8px 12px; + margin-bottom: 8px; + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; +} + +.addon-card:last-child { + margin-bottom: 0; +} + +.addon-card-header { + display: flex; + align-items: center; + gap: 8px; +} + +.addon-card-name { + font-size: 13px; + font-weight: 600; + color: var(--vscode-foreground); +} + +.addon-card-version { + font-size: 11px; + color: var(--vscode-descriptionForeground); + background-color: var(--vscode-badge-background); + padding: 1px 6px; + border-radius: 10px; +} + +.addon-card-description { + font-size: 12px; + color: var(--vscode-descriptionForeground); + margin: 0; +} + +.addon-card-meta { + font-size: 11px; + color: var(--vscode-descriptionForeground); + display: flex; + gap: 12px; +} + +.addon-card-meta span { + display: flex; + align-items: center; + gap: 4px; +} + +/* Active/Inactive badge */ +.addon-status { + font-size: 11px; + padding: 1px 6px; + border-radius: 10px; +} + +.addon-status.active { + background-color: var(--vscode-testing-iconPassed); + color: var(--vscode-editor-background); +} + +.addon-status.inactive { + background-color: var(--vscode-testing-iconSkipped); + color: var(--vscode-editor-background); } \ No newline at end of file diff --git a/media/webview.html b/media/webview.html index 4ccfd45..ad596e8 100644 --- a/media/webview.html +++ b/media/webview.html @@ -65,6 +65,17 @@

data-badge-for="history" > + + + + +
+ + +
+
+ +

{{registeredAddons}}

+
+
+

{{noAddonsRegistered}}

+
+
+ + + @@ -256,6 +304,14 @@

chatHistory: "{{chatHistory}}", clearHistory: "{{clearHistory}}", pastedImage: "{{pastedImage}}", + // Settings labels + settings: "{{settings}}", + settingsDescription: "{{settingsDescription}}", + loadingSettings: "{{loadingSettings}}", + registeredAddons: "{{registeredAddons}}", + noAddonsRegistered: "{{noAddonsRegistered}}", + addonVersion: "{{addonVersion}}", + addonAuthor: "{{addonAuthor}}", }; diff --git a/package-lock.json b/package-lock.json index 15a7842..953fe95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.24.3", - "@vscode/codicons": "^0.0.43", "highlight.js": "^11.11.1", "markdown-it": "^14.1.0", "zod": "^4.1.13" @@ -19,6 +18,7 @@ "@types/markdown-it": "^14.1.2", "@types/node": "^24.10.1", "@types/vscode": "^1.104.0", + "@vscode/codicons": "^0.0.44", "esbuild": "^0.27.1", "npm-run-all": "^4.1.5", "typescript": "^5.9.3" @@ -549,9 +549,10 @@ "license": "MIT" }, "node_modules/@vscode/codicons": { - "version": "0.0.43", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.43.tgz", - "integrity": "sha512-8sf8WOBoZkyUi8ogCm5ycHJJGhwOEG3E9b64+JIx+m6bCExdkc30VwCwr94cXUU1opmRD0CTCWLcN46I8WLJIg==", + "version": "0.0.44", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.44.tgz", + "integrity": "sha512-F7qPRumUK3EHjNdopfICLGRf3iNPoZQt+McTHAn4AlOWPB3W2kL4H0S7uqEqbyZ6rCxaeDjpAn3MCUnwTu/VJQ==", + "dev": true, "license": "CC-BY-4.0" }, "node_modules/accepts": { diff --git a/package.json b/package.json index aaadb87..49aec86 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ ], "license": "MIT", "main": "./dist/extension.js", + "types": "./dist/extension.d.ts", "engines": { "vscode": "^1.104.0" }, @@ -38,6 +39,14 @@ "activationEvents": [ "onStartupFinished" ], + "extensionKind": [ + "workspace" + ], + "capabilities": { + "untrustedWorkspaces": { + "supported": true + } + }, "repository": { "type": "git", "url": "git+https://github.com/jraylan/seamless-agent.git" @@ -50,7 +59,8 @@ "ia-tool", "vibe-coding", "ai", - "copilot" + "copilot", + "extensible" ], "contributes": { "viewsContainers": { @@ -280,6 +290,7 @@ "compile": "npm run compile:vscode", "compile:vscode": "npm run check-types && node esbuild.js", "compile:antigravity": "npm run check-types && node esbuild.js --target=antigravity", + "build:addon-typedefs": "node build-addon-typedefs.js", "watch": "npm-run-all -p watch:*", "watch:esbuild": "node esbuild.js --watch", "watch:antigravity": "node esbuild.js --watch --target=antigravity", @@ -294,13 +305,13 @@ "@types/markdown-it": "^14.1.2", "@types/node": "^24.10.1", "@types/vscode": "^1.104.0", + "@vscode/codicons": "^0.0.44", "esbuild": "^0.27.1", "npm-run-all": "^4.1.5", "typescript": "^5.9.3" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.24.3", - "@vscode/codicons": "^0.0.43", "highlight.js": "^11.11.1", "markdown-it": "^14.1.0", "zod": "^4.1.13" diff --git a/package.nls.json b/package.nls.json index 0e7a6d1..2312ad5 100644 --- a/package.nls.json +++ b/package.nls.json @@ -97,5 +97,13 @@ "command.showHistory.title": "Show History", "command.clearHistory.title": "Clear History", "status.closed": "Closed", - "status.active": "Active" + "status.active": "Active", + "settings.title": "Settings", + "settings.description": "Configure Seamless Agent and installed addons", + "settings.loading": "Loading settings...", + "settings.registeredAddons": "Registered Addons", + "settings.noAddonsRegistered": "No addons registered", + "settings.openInVSCodeSettings": "Open in VS Code Settings", + "settings.addonVersion": "Version", + "settings.addonAuthor": "Author" } diff --git a/package.nls.pt-br.json b/package.nls.pt-br.json index f48920e..925b0fd 100644 --- a/package.nls.pt-br.json +++ b/package.nls.pt-br.json @@ -97,5 +97,13 @@ "command.showHistory.title": "Ver Histórico", "command.clearHistory.title": "Limpar Histórico", "status.closed": "Fechado", - "status.active": "Ativo" + "status.active": "Ativo", + "settings.title": "Configurações", + "settings.description": "Configure o Seamless Agent e os addons instalados", + "settings.loading": "Carregando configurações...", + "settings.registeredAddons": "Addons Registrados", + "settings.noAddonsRegistered": "Nenhum addon registrado", + "settings.openInVSCodeSettings": "Abrir nas Configurações do VS Code", + "settings.addonVersion": "Versão", + "settings.addonAuthor": "Autor" } diff --git a/package.nls.pt.json b/package.nls.pt.json index 66148cd..62ceaa6 100644 --- a/package.nls.pt.json +++ b/package.nls.pt.json @@ -97,5 +97,13 @@ "command.showHistory.title": "Ver Histórico", "command.clearHistory.title": "Limpar Histórico", "status.closed": "Fechado", - "status.active": "Ativo" + "status.active": "Ativo", + "settings.title": "Definições", + "settings.description": "Configurar Seamless Agent e addons instalados", + "settings.loading": "A carregar definições...", + "settings.registeredAddons": "Addons Registados", + "settings.noAddonsRegistered": "Nenhum addon registado", + "settings.openInVSCodeSettings": "Abrir nas Definições do VS Code", + "settings.addonVersion": "Versão", + "settings.addonAuthor": "Autor" } diff --git a/src/addon-typedefs/index.ts b/src/addon-typedefs/index.ts new file mode 100644 index 0000000..143f567 --- /dev/null +++ b/src/addon-typedefs/index.ts @@ -0,0 +1,71 @@ +/** + * Seamless Agent — Addon API (somente tipos) + * + * Este entrypoint existe para que extensões "addon" consigam tipar a integração + * com o Seamless Agent sem depender de implementações (runtime). + * + * IMPORTANTE: + * - Este módulo deve ser usado apenas em contexto de tipos. + * - Prefira sempre `import type { ... }`. + */ + +export type { + // Core API + ISeamlessAgentAPI, + + // Addon definition + IAddon, + IAddonRegistration, + IAddonLifecycle, + + // UI + IUIIntegration, + IAddonUICapabilities, + ICustomTab, + IUIContent, + IHistoryType, + IHistoryItemProvider, + IHistoryItem, + + // Settings + ISettingsSection, + IAddonSettingSection, + IAddonSettingDefinition, + ISettingItem, + + // Tools + IToolsIntegration, + IAddonAICapabilities, + IAddonTool, + IToolExecutionContext, + IAskUserParams, + IUserResponse, + IPlanReviewParams, + IPlanReviewResult, + + // Events + IEventEmitter, + + // Storage + IStorageIntegration, + + // Convenience aliases + AskUserInput, + AskUserToolResult, + PlanReviewInput, + PlanReviewToolResult, +} from '../api/types'; + +/** + * Nomes de eventos padrão emitidos pelo Seamless Agent. + * + * Observação: esta é uma UNIÃO de strings (tipo), não um `const`. + * Isso evita que addons tentem usar este pacote em runtime. + */ +export type SeamlessAgentEventName = + | 'addon:registered' + | 'addon:unregistered' + | 'settings:changed' + | 'ui:refresh' + | 'tool:executed' + | 'tab:changed'; diff --git a/src/addons/README.md b/src/addons/README.md new file mode 100644 index 0000000..4d3065d --- /dev/null +++ b/src/addons/README.md @@ -0,0 +1,22 @@ +Este diretório contém o **sistema de addons** do Seamless Agent (registro, lifecycle e utilitários). + +## Para autores de addons + +Em geral, você **não** precisa importar nada daqui no seu addon. +Use a **API pública** exposta pela extensão Seamless Agent (ver `src/api`). + +## Para contribuidores do Seamless Agent + +- `registry.ts`: Registro central de addons (`AddonRegistry`) que mantém estado de ativação, tabs, settings e tools. +- `types.ts`: Aliases/re-exports de tipos do contrato público, para manter compatibilidade interna. + +## Responsabilidades + +O `AddonRegistry` agrega e organiza: + +- **Tabs** registradas pelos addons +- **Seções de Settings** registradas pelos addons +- **Providers de histórico** +- **Tools** expostas pelos addons + +O runtime do Seamless Agent usa o registry para alimentar a UI e a execução de ferramentas. diff --git a/src/addons/index.ts b/src/addons/index.ts new file mode 100644 index 0000000..beb7912 --- /dev/null +++ b/src/addons/index.ts @@ -0,0 +1,24 @@ +/** + * Addons Module + * + * This module provides addon management functionality for the Seamless Agent extension. + * The main addon registry is now part of the public API, but this module provides + * legacy compatibility and internal utilities. + */ + +// Re-export registry +export { AddonRegistry } from './registry'; + +// Re-export types for backward compatibility +export * from './types'; + +// Re-export API types that addons need +export type { + ISeamlessAgentAPI, + IAddonRegistration, + IEventEmitter, + IStorageIntegration, + IUIIntegration, + IToolsIntegration, +} from '../api/types'; + diff --git a/src/addons/registry.ts b/src/addons/registry.ts new file mode 100644 index 0000000..f23895a --- /dev/null +++ b/src/addons/registry.ts @@ -0,0 +1,412 @@ +/** + * Addon Registry + * + * Centralized registry for managing addon registrations. + * Implements the Registry Pattern for addon lifecycle management. + */ + +import type * as vscode from 'vscode'; +import type { + IAddon, + IAddonRegistration, + IEventEmitter, + ICustomTab, + ISettingsSection, + IAddonTool, +} from '../api/types'; +import { SeamlessAgentEvents } from '../api/types'; + +/** + * Internal registration data structure + */ +interface RegistrationData { + addon: IAddon; + isActive: boolean; + disposables: vscode.Disposable[]; + registeredTabs: ICustomTab[]; + registeredTools: IAddonTool[]; + registeredSettingsSections: ISettingsSection[]; +} + +/** + * Addon Registration implementation + */ +class AddonRegistrationImpl implements IAddonRegistration { + constructor( + private readonly registry: AddonRegistry, + private readonly data: RegistrationData + ) { } + + get addon(): IAddon { + return this.data.addon; + } + + get id(): string { + return this.data.addon.id; + } + + get isActive(): boolean { + return this.data.isActive; + } + + /** + * Deactivate the addon without unregistering + */ + deactivate(): void { + if (!this.data.isActive) return; + + this.data.isActive = false; + + // Call lifecycle hook + if (this.data.addon.lifecycle?.onDeactivate) { + try { + const result = this.data.addon.lifecycle.onDeactivate(); + if (result instanceof Promise) { + result.catch(err => { + console.error(`[AddonRegistry] Error in onDeactivate for ${this.id}:`, err); + }); + } + } catch (err) { + console.error(`[AddonRegistry] Error in onDeactivate for ${this.id}:`, err); + } + } + } + + /** + * Reactivate a deactivated addon + */ + activate(): void { + if (this.data.isActive) return; + + this.data.isActive = true; + + // Call lifecycle hook + if (this.data.addon.lifecycle?.onActivate) { + try { + const result = this.data.addon.lifecycle.onActivate(); + if (result instanceof Promise) { + result.catch(err => { + console.error(`[AddonRegistry] Error in onActivate for ${this.id}:`, err); + }); + } + } catch (err) { + console.error(`[AddonRegistry] Error in onActivate for ${this.id}:`, err); + } + } + } + + /** + * Dispose and unregister the addon + */ + dispose(): void { + this.registry.unregister(this.id); + } +} + +/** + * Centralized registry for addon management. + * Implements the Registry Pattern for tracking and querying addons. + */ +export class AddonRegistry implements vscode.Disposable { + private readonly registrations: Map = new Map(); + private readonly eventEmitter: IEventEmitter; + + constructor(eventEmitter: IEventEmitter) { + this.eventEmitter = eventEmitter; + } + + /** + * Register an addon + * @param addon - The addon to register + * @returns Registration handle + */ + register(addon: IAddon): IAddonRegistration { + // Check for duplicate registration + if (this.registrations.has(addon.id)) { + throw new Error(`Addon with ID '${addon.id}' is already registered`); + } + + // Validate addon + this.validateAddon(addon); + + // Create registration data + const data: RegistrationData = { + addon, + isActive: true, + disposables: [], + registeredTabs: [], + registeredTools: [], + registeredSettingsSections: [], + }; + + // Extract and store capabilities + if (addon.ui?.tabs) { + data.registeredTabs = [...addon.ui.tabs]; + } + + if (addon.ai?.tools) { + data.registeredTools = [...addon.ai.tools]; + } + + if (addon.settings) { + data.registeredSettingsSections = addon.settings.map(section => ({ + id: `${addon.id}.${section.key}`, + title: section.label, + description: section.description, + settings: section.settings.map(setting => ({ + key: `${addon.id}.${section.key}.${setting.key}`, + label: setting.label, + description: setting.description, + type: setting.type, + value: setting.defaultValue, + defaultValue: setting.defaultValue, + options: setting.options, + })), + })); + } + + // Store registration + this.registrations.set(addon.id, data); + + // Create registration handle + const registration = new AddonRegistrationImpl(this, data); + + // Call lifecycle hook + if (addon.lifecycle?.onActivate) { + try { + const result = addon.lifecycle.onActivate(); + if (result instanceof Promise) { + result.catch(err => { + console.error(`[AddonRegistry] Error in onActivate for ${addon.id}:`, err); + }); + } + } catch (err) { + console.error(`[AddonRegistry] Error in onActivate for ${addon.id}:`, err); + } + } + + // Emit registration event + this.eventEmitter.emit(SeamlessAgentEvents.ADDON_REGISTERED, { + addonId: addon.id, + addon, + }); + + console.log(`[AddonRegistry] Registered addon: ${addon.id} (${addon.name} v${addon.version})`); + + return registration; + } + + /** + * Unregister an addon by ID + * @param addonId - The addon ID to unregister + */ + unregister(addonId: string): void { + const data = this.registrations.get(addonId); + if (!data) { + console.warn(`[AddonRegistry] Addon '${addonId}' not found for unregistration`); + return; + } + + // Call lifecycle hook + if (data.addon.lifecycle?.onDeactivate) { + try { + const result = data.addon.lifecycle.onDeactivate(); + if (result instanceof Promise) { + result.catch(err => { + console.error(`[AddonRegistry] Error in onDeactivate for ${addonId}:`, err); + }); + } + } catch (err) { + console.error(`[AddonRegistry] Error in onDeactivate for ${addonId}:`, err); + } + } + + // Dispose all registered disposables + for (const disposable of data.disposables) { + try { + disposable.dispose(); + } catch (err) { + console.error(`[AddonRegistry] Error disposing resource for ${addonId}:`, err); + } + } + + // Remove from registry + this.registrations.delete(addonId); + + // Emit unregistration event + this.eventEmitter.emit(SeamlessAgentEvents.ADDON_UNREGISTERED, { + addonId, + }); + + console.log(`[AddonRegistry] Unregistered addon: ${addonId}`); + } + + /** + * Get a registration by addon ID + * @param addonId - The addon ID + * @returns Registration data or undefined + */ + get(addonId: string): IAddonRegistration | undefined { + const data = this.registrations.get(addonId); + if (!data) return undefined; + + return new AddonRegistrationImpl(this, data); + } + + /** + * Get all registered addons + * @returns Array of addon registrations + */ + getAll(): IAddonRegistration[] { + return Array.from(this.registrations.values()).map( + data => new AddonRegistrationImpl(this, data) + ); + } + + /** + * Get all active addons + * @returns Array of active addon registrations + */ + getActive(): IAddonRegistration[] { + return Array.from(this.registrations.values()) + .filter(data => data.isActive) + .map(data => new AddonRegistrationImpl(this, data)); + } + + /** + * Get all registered tabs from all active addons + * @returns Array of custom tabs + */ + getAllTabs(): ICustomTab[] { + const tabs: ICustomTab[] = []; + + for (const data of this.registrations.values()) { + if (data.isActive) { + tabs.push(...data.registeredTabs); + } + } + + // Sort by priority + return tabs.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100)); + } + + /** + * Get all registered tools from all active addons + * @returns Array of addon tools + */ + getAllTools(): IAddonTool[] { + const tools: IAddonTool[] = []; + + for (const data of this.registrations.values()) { + if (data.isActive) { + tools.push(...data.registeredTools); + } + } + + return tools; + } + + /** + * Get all registered settings sections from all active addons + * @returns Array of settings sections + */ + getAllSettingsSections(): ISettingsSection[] { + const sections: ISettingsSection[] = []; + + for (const data of this.registrations.values()) { + if (data.isActive) { + sections.push(...data.registeredSettingsSections); + } + } + + // Sort by priority + return sections.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100)); + } + + /** + * Add a disposable to an addon's registration + * @param addonId - The addon ID + * @param disposable - The disposable to add + */ + addDisposable(addonId: string, disposable: vscode.Disposable): void { + const data = this.registrations.get(addonId); + if (data) { + data.disposables.push(disposable); + } + } + + /** + * Get the count of registered addons + * @returns Number of registered addons + */ + get count(): number { + return this.registrations.size; + } + + /** + * Check if an addon is registered + * @param addonId - The addon ID + * @returns True if registered + */ + has(addonId: string): boolean { + return this.registrations.has(addonId); + } + + /** + * Validate an addon definition + * @param addon - The addon to validate + * @throws Error if validation fails + */ + private validateAddon(addon: IAddon): void { + if (!addon.id || typeof addon.id !== 'string') { + throw new Error('Addon must have a valid ID string'); + } + + if (!addon.name || typeof addon.name !== 'string') { + throw new Error('Addon must have a valid name string'); + } + + if (!addon.version || typeof addon.version !== 'string') { + throw new Error('Addon must have a valid version string'); + } + + // Validate tool names are unique + if (addon.ai?.tools) { + const toolNames = new Set(); + for (const tool of addon.ai.tools) { + if (!tool.name) { + throw new Error(`Tool in addon '${addon.id}' must have a name`); + } + if (toolNames.has(tool.name)) { + throw new Error(`Duplicate tool name '${tool.name}' in addon '${addon.id}'`); + } + toolNames.add(tool.name); + } + } + + // Validate tab IDs are unique + if (addon.ui?.tabs) { + const tabIds = new Set(); + for (const tab of addon.ui.tabs) { + if (!tab.id) { + throw new Error(`Tab in addon '${addon.id}' must have an ID`); + } + if (tabIds.has(tab.id)) { + throw new Error(`Duplicate tab ID '${tab.id}' in addon '${addon.id}'`); + } + tabIds.add(tab.id); + } + } + } + + /** + * Dispose the registry and all addons + */ + dispose(): void { + // Unregister all addons + const addonIds = [...this.registrations.keys()]; + for (const addonId of addonIds) { + this.unregister(addonId); + } + } +} diff --git a/src/addons/types.ts b/src/addons/types.ts new file mode 100644 index 0000000..156f812 --- /dev/null +++ b/src/addons/types.ts @@ -0,0 +1,40 @@ +/** + * Addon Types + * + * This module re-exports the addon-related types from the public API + * for internal use and backward compatibility. + * + * For new code, prefer importing directly from '../api/types'. + */ + +// Re-export all addon-related types from the API +export type { + IAddon, + IAddonRegistration, + IAddonLifecycle, + IAddonUICapabilities, + IAddonAICapabilities, + IAddonSettingSection, + IAddonSettingDefinition, + IAddonTool, + IToolExecutionContext, + ICustomTab, + IUIContent, + IHistoryType, + ISettingsSection, + ISettingItem, +} from '../api/types'; + +// Legacy type aliases for backward compatibility +export type { IAddonSettingSection as AddonSettingSection } from '../api/types'; +export type { IAddonSettingDefinition as AddonSetting } from '../api/types'; +export type { IAddonTool as AITool } from '../api/types'; + +/** + * Tab type identifiers + */ +export type TabType = + | 'notification' + | 'history' + | 'settings' + | string; // Allow custom tab types from addons diff --git a/src/agent/index.ts b/src/agent/index.ts new file mode 100644 index 0000000..5382796 --- /dev/null +++ b/src/agent/index.ts @@ -0,0 +1,79 @@ +import * as vscode from 'vscode'; + + + + +export class OrchestrationAgent { + + + public dispose() { + + } + + public handler: vscode.ChatRequestHandler = async ( + request: vscode.ChatRequest, + chatContext: vscode.ChatContext, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken + ) => { + // Get our ask_user tool + const askUserTool = vscode.lm.tools.find(t => t.name === 'ask_user'); + + // Get all available tools including ours + const tools = askUserTool ? [askUserTool] : []; + + // Build the prompt with instruction to use the tool + const systemPrompt = `You are a helpful assistant. + IMPORTANT: You have access to an "ask_user" tool. You MUST use this tool before completing any task to confirm with the user that their request was fulfilled correctly. + Never finish a response without first calling the ask_user tool to verify with the user.`; + + const messages = [ + vscode.LanguageModelChatMessage.User(systemPrompt), + vscode.LanguageModelChatMessage.User(request.prompt) + ]; + + // Get the model + let model = request.model; + + const options: vscode.LanguageModelChatRequestOptions = { + tools: tools.map(t => ({ + name: t.name, + description: t.description, + inputSchema: t.inputSchema + })), + }; + + try { + const response = await model.sendRequest(messages, options, token); + + for await (const part of response.stream) { + if (part instanceof vscode.LanguageModelTextPart) { + stream.markdown(part.value); + } else if (part instanceof vscode.LanguageModelToolCallPart) { + // Handle tool calls + stream.progress(`Calling ${part.name}...`); + const toolResult = await vscode.lm.invokeTool(part.name, { + input: part.input, + toolInvocationToken: request.toolInvocationToken + }, token); + + // Show tool result + for (const resultPart of toolResult.content) { + if (resultPart instanceof vscode.LanguageModelTextPart) { + stream.markdown(`\n\n**User Response:** ${resultPart.value}\n\n`); + } + } + } + } + } catch (err) { + if (err instanceof vscode.LanguageModelError) { + stream.markdown(`Error: ${err.message}`); + } else { + throw err; + } + } + + return; + } + +} \ No newline at end of file diff --git a/src/api/README.md b/src/api/README.md new file mode 100644 index 0000000..94facec --- /dev/null +++ b/src/api/README.md @@ -0,0 +1,46 @@ +Este diretório contém o **contrato público** (interfaces) consumido por extensões **addon**. + +## Como um addon obtém a API do Seamless Agent + +No `activate()` do seu addon, obtenha a extensão e chame `activate()` para receber o objeto de API: + +- Extensão: `jraylan.seamless-agent` +- API: `ISeamlessAgentAPI` + +### Exemplo (addon) + +- Busque a extensão pelo ID +- Ative-a para obter a API +- Registre o addon com `registerAddon()` + +> Recomendação: use `import type { ISeamlessAgentAPI, IAddon } from '...';` (tipos) e mantenha a integração em runtime via `vscode.extensions.getExtension(...).activate()`. + +## O que a API oferece + +### UI + +- **Tabs**: `api.ui.registerTab(tab)` permite criar novas abas no webview do Seamless Agent. +- **Histórico**: `api.ui.registerHistoryProvider(provider)` permite injetar itens/tipos adicionais no histórico. +- **Settings**: `api.ui.registerSettingsSection(section)` permite adicionar seções de configuração na aba Settings do webview. + +### Tools (LLM) + +- Defina ferramentas em `addon.ai.tools` (ver `IAddonTool`). +- O Seamless Agent expõe e executa essas ferramentas no contexto do agente. + +### Storage + +- `api.storage` fornece persistência (namespaced) para o addon armazenar preferências e dados. + +### Events + +- `api.events` permite observar eventos do Seamless Agent (ex.: refresh de UI, tool executada, etc.). + +## Compatibilidade + +A API expõe `api.version`. Addons devem validar a versão para garantir compatibilidade. + +## Importante (types-only) + +Este repositório gera um pacote separado (para NPM) contendo **somente typedefs** para uso por addons. +Evite depender de implementações deste diretório no runtime do addon. diff --git a/src/api/SeamlessAgentAPI.ts b/src/api/SeamlessAgentAPI.ts new file mode 100644 index 0000000..55a27f9 --- /dev/null +++ b/src/api/SeamlessAgentAPI.ts @@ -0,0 +1,485 @@ +/** + * Seamless Agent Public API + * + * Facade implementation providing a unified API for addon extensions. + * Implements the Facade Pattern to simplify addon integration. + */ + +import * as vscode from 'vscode'; +import type { + ISeamlessAgentAPI, + IAddon, + IAddonRegistration, + IUIIntegration, + IToolsIntegration, + IEventEmitter, + IStorageIntegration, + ICustomTab, + ISettingsSection, + IHistoryItemProvider, + IAddonTool, + IToolExecutionContext, + IAskUserParams, + IUserResponse, + IPlanReviewParams, + IPlanReviewResult, +} from './types'; +import { SeamlessAgentEvents } from './types'; +import { SeamlessEventEmitter } from './events'; +import { AddonRegistry } from '../addons/registry'; + +/** + * Current API version + */ +export const API_VERSION = '1.0.0'; + +/** + * UI Integration implementation + */ +class UIIntegrationImpl implements IUIIntegration { + private readonly customTabs: Map = new Map(); + private readonly historyProviders: Map = new Map(); + private readonly settingsSections: Map = new Map(); + + constructor( + private readonly registry: AddonRegistry, + private readonly eventEmitter: IEventEmitter + ) { } + + /** + * Register a custom tab + */ + registerTab(tab: ICustomTab): vscode.Disposable { + if (this.customTabs.has(tab.id)) { + throw new Error(`Tab with ID '${tab.id}' is already registered`); + } + + this.customTabs.set(tab.id, tab); + this.eventEmitter.emit(SeamlessAgentEvents.UI_REFRESH, { type: 'tab_added', tabId: tab.id }); + + return { + dispose: () => { + this.customTabs.delete(tab.id); + this.eventEmitter.emit(SeamlessAgentEvents.UI_REFRESH, { type: 'tab_removed', tabId: tab.id }); + } + }; + } + + /** + * Register a history item provider + */ + registerHistoryProvider(provider: IHistoryItemProvider): vscode.Disposable { + if (this.historyProviders.has(provider.id)) { + throw new Error(`History provider with ID '${provider.id}' is already registered`); + } + + this.historyProviders.set(provider.id, provider); + this.eventEmitter.emit(SeamlessAgentEvents.UI_REFRESH, { type: 'history_provider_added' }); + + return { + dispose: () => { + this.historyProviders.delete(provider.id); + this.eventEmitter.emit(SeamlessAgentEvents.UI_REFRESH, { type: 'history_provider_removed' }); + } + }; + } + + /** + * Register a settings section + */ + registerSettingsSection(section: ISettingsSection): vscode.Disposable { + if (this.settingsSections.has(section.id)) { + throw new Error(`Settings section with ID '${section.id}' is already registered`); + } + + this.settingsSections.set(section.id, section); + this.eventEmitter.emit(SeamlessAgentEvents.UI_REFRESH, { type: 'settings_section_added' }); + + return { + dispose: () => { + this.settingsSections.delete(section.id); + this.eventEmitter.emit(SeamlessAgentEvents.UI_REFRESH, { type: 'settings_section_removed' }); + } + }; + } + + /** + * Refresh the webview UI + */ + refresh(): void { + this.eventEmitter.emit(SeamlessAgentEvents.UI_REFRESH, { type: 'manual_refresh' }); + } + + /** + * Get all registered tabs (including from addons via registry) + */ + getTabs(): ICustomTab[] { + // Combine tabs from registry and directly registered tabs + const registryTabs = this.registry.getAllTabs(); + const directTabs = Array.from(this.customTabs.values()); + + const allTabs = [...registryTabs, ...directTabs]; + + // Sort by priority + return allTabs.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100)); + } + + /** + * Get all registered settings sections + */ + getSettingsSections(): ISettingsSection[] { + // Combine sections from registry and directly registered sections + const registrySections = this.registry.getAllSettingsSections(); + const directSections = Array.from(this.settingsSections.values()); + + const allSections = [...registrySections, ...directSections]; + + // Sort by priority + return allSections.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100)); + } + + /** + * Get all registered history providers + */ + getHistoryProviders(): IHistoryItemProvider[] { + return Array.from(this.historyProviders.values()); + } +} + +/** + * Tools Integration implementation + */ +class ToolsIntegrationImpl implements IToolsIntegration { + private readonly tools: Map = new Map(); + private readonly toolDisposables: Map = new Map(); + private askUserFn?: (params: IAskUserParams) => Promise; + private planReviewFn?: (params: IPlanReviewParams) => Promise; + + constructor( + private readonly registry: AddonRegistry, + private readonly eventEmitter: IEventEmitter, + private readonly getAPI: () => ISeamlessAgentAPI + ) { } + + /** + * Set the askUser function for direct API calls + */ + setAskUserFunction(fn: (params: IAskUserParams) => Promise): void { + this.askUserFn = fn; + } + + /** + * Set the planReview function for direct API calls + */ + setPlanReviewFunction(fn: (params: IPlanReviewParams) => Promise): void { + this.planReviewFn = fn; + } + + /** + * Register an AI tool + */ + registerTool(tool: IAddonTool): vscode.Disposable { + if (this.tools.has(tool.name)) { + throw new Error(`Tool with name '${tool.name}' is already registered`); + } + + this.tools.set(tool.name, tool); + + // Register with VS Code Language Model API + const lmTool = vscode.lm.registerTool(tool.name, { + invoke: async (options, token) => { + const context: IToolExecutionContext = { + api: this.getAPI(), + requestId: `${tool.name}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, + }; + + try { + const result = await tool.execute(options.input, context, token); + + this.eventEmitter.emit(SeamlessAgentEvents.TOOL_EXECUTED, { + toolName: tool.name, + success: true, + requestId: context.requestId, + }); + + return new vscode.LanguageModelToolResult([ + new vscode.LanguageModelTextPart(JSON.stringify(result)) + ]); + } catch (error) { + this.eventEmitter.emit(SeamlessAgentEvents.TOOL_EXECUTED, { + toolName: tool.name, + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + requestId: context.requestId, + }); + + throw error; + } + } + }); + + this.toolDisposables.set(tool.name, lmTool); + + return { + dispose: () => { + this.tools.delete(tool.name); + const disposable = this.toolDisposables.get(tool.name); + if (disposable) { + disposable.dispose(); + this.toolDisposables.delete(tool.name); + } + } + }; + } + + /** + * Get all registered tools + */ + getTools(): IAddonTool[] { + // Combine tools from registry and directly registered tools + const registryTools = this.registry.getAllTools(); + const directTools = Array.from(this.tools.values()); + + return [...registryTools, ...directTools]; + } + + /** + * Call the native askUser tool + */ + async askUser(params: IAskUserParams): Promise { + if (!this.askUserFn) { + throw new Error('askUser function not initialized'); + } + return this.askUserFn(params); + } + + /** + * Call the native planReview tool + */ + async planReview(params: IPlanReviewParams): Promise { + if (!this.planReviewFn) { + throw new Error('planReview function not initialized'); + } + return this.planReviewFn(params); + } +} + +/** + * Storage Integration implementation for addon data persistence + */ +class StorageIntegrationImpl implements IStorageIntegration { + private readonly namespace: string; + private readonly globalState: vscode.Memento; + + constructor(context: vscode.ExtensionContext, addonId: string) { + this.namespace = `addon.${addonId}`; + this.globalState = context.globalState; + } + + /** + * Get a stored value + */ + get(key: string, defaultValue?: T): T | undefined { + const fullKey = `${this.namespace}.${key}`; + return this.globalState.get(fullKey, defaultValue as T); + } + + /** + * Set a stored value + */ + async set(key: string, value: T): Promise { + const fullKey = `${this.namespace}.${key}`; + await this.globalState.update(fullKey, value); + } + + /** + * Delete a stored value + */ + async delete(key: string): Promise { + const fullKey = `${this.namespace}.${key}`; + await this.globalState.update(fullKey, undefined); + } + + /** + * Get all keys for this addon + */ + keys(): string[] { + const allKeys = this.globalState.keys(); + const prefix = `${this.namespace}.`; + return allKeys + .filter(key => key.startsWith(prefix)) + .map(key => key.substring(prefix.length)); + } + + /** + * Clear all addon storage + */ + async clear(): Promise { + const keys = this.keys(); + for (const key of keys) { + await this.delete(key); + } + } +} + +/** + * Storage Integration Factory + */ +class StorageIntegrationFactory { + private readonly storages: Map = new Map(); + + constructor(private readonly context: vscode.ExtensionContext) { } + + /** + * Get or create storage for an addon + */ + getStorage(addonId: string): IStorageIntegration { + if (!this.storages.has(addonId)) { + this.storages.set(addonId, new StorageIntegrationImpl(this.context, addonId)); + } + return this.storages.get(addonId)!; + } +} + +/** + * Main Seamless Agent API implementation. + * Facade providing unified access to all addon integration features. + */ +export class SeamlessAgentAPI implements ISeamlessAgentAPI, vscode.Disposable { + public readonly version: string = API_VERSION; + + private readonly _eventEmitter: SeamlessEventEmitter; + private readonly _registry: AddonRegistry; + private readonly _ui: UIIntegrationImpl; + private readonly _tools: ToolsIntegrationImpl; + private readonly _storageFactory: StorageIntegrationFactory; + private readonly _disposables: vscode.Disposable[] = []; + + constructor(private readonly _context: vscode.ExtensionContext) { + // Initialize event emitter + this._eventEmitter = new SeamlessEventEmitter(); + this._disposables.push({ dispose: () => this._eventEmitter.dispose() }); + + // Initialize registry + this._registry = new AddonRegistry(this._eventEmitter); + this._disposables.push(this._registry); + + // Initialize UI integration + this._ui = new UIIntegrationImpl(this._registry, this._eventEmitter); + + // Initialize Tools integration + this._tools = new ToolsIntegrationImpl( + this._registry, + this._eventEmitter, + () => this + ); + + // Initialize storage factory + this._storageFactory = new StorageIntegrationFactory(_context); + + console.log(`[SeamlessAgentAPI] Initialized API v${this.version}`); + } + + /** + * Get the extension context + */ + get context(): vscode.ExtensionContext { + return this._context; + } + + /** + * Get the UI integration + */ + get ui(): IUIIntegration { + return this._ui; + } + + /** + * Get the Tools integration + */ + get tools(): IToolsIntegration { + return this._tools; + } + + /** + * Get the event emitter + */ + get events(): IEventEmitter { + return this._eventEmitter; + } + + /** + * Get storage for a specific addon (internal use) + */ + get storage(): IStorageIntegration { + // Return a no-op storage for the main extension + // Addons get their own namespaced storage via registerAddon + return this._storageFactory.getStorage('seamless-agent'); + } + + /** + * Get the addon registry (internal use) + */ + get registry(): AddonRegistry { + return this._registry; + } + + // ========================================================================= + // Internal Methods (for extension initialization only, not for addons) + // ========================================================================= + + /** + * @internal + * Set the native askUser function implementation. + * This is called during extension initialization, not by addons. + * Addons should use api.tools.askUser() to call this function. + */ + _setAskUserFunction(fn: (params: IAskUserParams) => Promise): void { + this._tools.setAskUserFunction(fn); + } + + /** + * @internal + * Set the native planReview function implementation. + * This is called during extension initialization, not by addons. + * Addons should use api.tools.planReview() to call this function. + */ + _setPlanReviewFunction(fn: (params: IPlanReviewParams) => Promise): void { + this._tools.setPlanReviewFunction(fn); + } + + /** + * Register an addon + */ + registerAddon(addon: IAddon): IAddonRegistration { + return this._registry.register(addon); + } + + /** + * Unregister an addon by ID + */ + unregisterAddon(addonId: string): void { + this._registry.unregister(addonId); + } + + /** + * Dispose all resources + */ + dispose(): void { + for (const disposable of this._disposables) { + try { + disposable.dispose(); + } catch (err) { + console.error('[SeamlessAgentAPI] Error disposing:', err); + } + } + this._disposables.length = 0; + } +} + +/** + * Create a new Seamless Agent API instance + */ +export function createSeamlessAgentAPI(context: vscode.ExtensionContext): SeamlessAgentAPI { + return new SeamlessAgentAPI(context); +} diff --git a/src/api/events.ts b/src/api/events.ts new file mode 100644 index 0000000..cad1686 --- /dev/null +++ b/src/api/events.ts @@ -0,0 +1,199 @@ +/** + * Event Emitter System for Seamless Agent + * + * Provides a type-safe event system for communication between + * the core extension and registered addons. + */ + +import type * as vscode from 'vscode'; +import { IEventEmitter, SeamlessAgentEvents } from './types'; + +/** + * Type-safe event listener + */ +type EventListener = (data: T) => void; + +/** + * Event emitter implementation using the Observer Pattern. + * Provides a pub/sub mechanism for addon communication. + */ +export class SeamlessEventEmitter implements IEventEmitter { + private readonly listeners: Map> = new Map(); + private readonly onceListeners: Map> = new Map(); + private readonly disposables: Map = new Map(); + + /** + * Subscribe to an event + * @param event - Event name + * @param listener - Event listener callback + * @returns Disposable for cleanup + */ + public on(event: string, listener: EventListener): vscode.Disposable { + if (!this.listeners.has(event)) { + this.listeners.set(event, new Set()); + } + + const eventListeners = this.listeners.get(event)!; + eventListeners.add(listener as EventListener); + + const disposable: vscode.Disposable = { + dispose: () => { + eventListeners.delete(listener as EventListener); + this.disposables.delete(listener as EventListener); + + // Clean up empty sets + if (eventListeners.size === 0) { + this.listeners.delete(event); + } + } + }; + + this.disposables.set(listener as EventListener, disposable); + return disposable; + } + + /** + * Subscribe to an event for a single invocation + * @param event - Event name + * @param listener - Event listener callback + * @returns Disposable for cleanup + */ + public once(event: string, listener: EventListener): vscode.Disposable { + if (!this.onceListeners.has(event)) { + this.onceListeners.set(event, new Set()); + } + + const eventListeners = this.onceListeners.get(event)!; + eventListeners.add(listener as EventListener); + + const disposable: vscode.Disposable = { + dispose: () => { + eventListeners.delete(listener as EventListener); + this.disposables.delete(listener as EventListener); + + // Clean up empty sets + if (eventListeners.size === 0) { + this.onceListeners.delete(event); + } + } + }; + + this.disposables.set(listener as EventListener, disposable); + return disposable; + } + + /** + * Emit an event to all listeners + * @param event - Event name + * @param data - Event data + */ + public emit(event: string, data: T): void { + // Call regular listeners + const regularListeners = this.listeners.get(event); + if (regularListeners) { + for (const listener of regularListeners) { + try { + listener(data); + } catch (error) { + console.error(`[SeamlessAgent] Error in event listener for '${event}':`, error); + } + } + } + + // Call once listeners and remove them + const onceListeners = this.onceListeners.get(event); + if (onceListeners) { + const listenersToCall = [...onceListeners]; + this.onceListeners.delete(event); + + for (const listener of listenersToCall) { + try { + listener(data); + } catch (error) { + console.error(`[SeamlessAgent] Error in once listener for '${event}':`, error); + } + + // Clean up disposable + const disposable = this.disposables.get(listener); + if (disposable) { + this.disposables.delete(listener); + } + } + } + } + + /** + * Remove all listeners for a specific event or all events + * @param event - Optional event name. If not provided, removes all listeners. + */ + public removeAllListeners(event?: string): void { + if (event) { + // Remove listeners for specific event + const regularListeners = this.listeners.get(event); + if (regularListeners) { + for (const listener of regularListeners) { + const disposable = this.disposables.get(listener); + if (disposable) { + this.disposables.delete(listener); + } + } + this.listeners.delete(event); + } + + const onceListeners = this.onceListeners.get(event); + if (onceListeners) { + for (const listener of onceListeners) { + const disposable = this.disposables.get(listener); + if (disposable) { + this.disposables.delete(listener); + } + } + this.onceListeners.delete(event); + } + } else { + // Remove all listeners + this.listeners.clear(); + this.onceListeners.clear(); + this.disposables.clear(); + } + } + + /** + * Get the count of listeners for an event + * @param event - Event name + * @returns Number of listeners + */ + public listenerCount(event: string): number { + const regular = this.listeners.get(event)?.size ?? 0; + const once = this.onceListeners.get(event)?.size ?? 0; + return regular + once; + } + + /** + * Get all event names that have listeners + * @returns Array of event names + */ + public eventNames(): string[] { + const names = new Set(); + + for (const event of this.listeners.keys()) { + names.add(event); + } + + for (const event of this.onceListeners.keys()) { + names.add(event); + } + + return [...names]; + } + + /** + * Dispose all listeners and clean up + */ + public dispose(): void { + this.removeAllListeners(); + } +} + +// Re-export event names for convenience +export { SeamlessAgentEvents }; diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..b104d17 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,86 @@ +/** + * Seamless Agent Public API + * + * This module exports the public API for addon extensions to integrate + * with the Seamless Agent extension. + * + * @example + * ```typescript + * import * as vscode from 'vscode'; + * + * export async function activate(context: vscode.ExtensionContext) { + * const seamlessExt = vscode.extensions.getExtension('jraylan.seamless-agent'); + * if (!seamlessExt) return; + * + * const api = await seamlessExt.activate(); + * + * const registration = api.registerAddon({ + * id: 'my-addon', + * name: 'My Custom Addon', + * version: '1.0.0', + * ai: { + * tools: [{ + * name: 'my_tool', + * description: 'My custom tool', + * execute: async (params, context, token) => { + * return { success: true }; + * } + * }] + * } + * }); + * + * context.subscriptions.push(registration); + * } + * ``` + */ + +// Export main API class and factory +export { SeamlessAgentAPI, createSeamlessAgentAPI, API_VERSION } from './SeamlessAgentAPI'; + +// Export event system +export { SeamlessEventEmitter } from './events'; + +// Export all public types +export type { + // Core API + ISeamlessAgentAPI, + + // Addon definition + IAddon, + IAddonRegistration, + IAddonLifecycle, + + // UI Integration + IUIIntegration, + IAddonUICapabilities, + ICustomTab, + IUIContent, + IHistoryType, + IHistoryItemProvider, + IHistoryItem, + + // Settings + ISettingsSection, + IAddonSettingSection, + IAddonSettingDefinition, + ISettingItem, + + // Tools Integration + IToolsIntegration, + IAddonAICapabilities, + IAddonTool, + IToolExecutionContext, + IAskUserParams, + IUserResponse, + IPlanReviewParams, + IPlanReviewResult, + + // Events + IEventEmitter, + + // Storage + IStorageIntegration, +} from './types'; + +// Export event constants +export { SeamlessAgentEvents } from './types'; diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..cdd4148 --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,679 @@ +/** + * Public API Types for Seamless Agent Addons + * + * This module defines the public interfaces that addon extensions + * use to integrate with the Seamless Agent extension. + */ + +import type * as vscode from 'vscode'; +import type { codiconsLibrary } from '@vscode/codicons/dist/codiconsLibrary'; + +// ============================================================================ +// Core API Interfaces +// ============================================================================ + +/** + * Main public API interface for the Seamless Agent extension. + * This is the entry point for addon extensions to integrate with Seamless Agent. + */ +export interface ISeamlessAgentAPI { + /** Current API version for compatibility checking */ + readonly version: string; + + /** Extension context for accessing VS Code services */ + readonly context: vscode.ExtensionContext; + + /** + * Register an addon with the Seamless Agent + * @param addon - The addon configuration + * @returns Registration handle for cleanup + */ + registerAddon(addon: IAddon): IAddonRegistration; + + /** + * Unregister an addon by ID + * @param addonId - The unique addon identifier + */ + unregisterAddon(addonId: string): void; + + /** + * UI integration capabilities + */ + readonly ui: IUIIntegration; + + /** + * AI Tools integration capabilities + */ + readonly tools: IToolsIntegration; + + /** + * Event system for addon communication + */ + readonly events: IEventEmitter; + + /** + * Storage integration for addon data + */ + readonly storage: IStorageIntegration; +} + +// ============================================================================ +// Addon Definition Interfaces +// ============================================================================ + +/** + * Addon definition interface. + * Addons implement this interface to describe their capabilities. + */ +export interface IAddon { + /** Unique identifier for the addon (e.g., 'my-extension.my-addon') */ + readonly id: string; + + /** Human-readable name */ + readonly name: string; + + /** Semantic version string */ + readonly version: string; + + /** Optional description */ + readonly description?: string; + + /** Optional author name or handle */ + readonly author?: string; + + /** Optional repository URL */ + readonly repositoryUrl?: string; + + /** UI capabilities */ + readonly ui?: IAddonUICapabilities; + + /** AI/LLM tool capabilities */ + readonly ai?: IAddonAICapabilities; + + /** Settings sections */ + readonly settings?: IAddonSettingSection[]; + + /** Lifecycle hooks */ + readonly lifecycle?: IAddonLifecycle; +} + +/** + * Registration handle returned when an addon is registered. + * Implements Disposable for cleanup. + */ +export interface IAddonRegistration extends vscode.Disposable { + /** The registered addon */ + readonly addon: IAddon; + + /** Registration ID */ + readonly id: string; + + /** Whether the addon is currently active */ + readonly isActive: boolean; + + /** Deactivate the addon without unregistering */ + deactivate(): void; + + /** Reactivate a deactivated addon */ + activate(): void; +} + +/** + * Addon lifecycle hooks + */ +export interface IAddonLifecycle { + /** Called when the addon is activated */ + onActivate?(): Promise | void; + + /** Called when the addon is deactivated */ + onDeactivate?(): Promise | void; + + /** Called when settings change */ + onSettingsChange?(settings: Record): Promise | void; +} + +// ============================================================================ +// UI Integration Interfaces +// ============================================================================ + +/** + * UI integration capabilities for addons + */ +export interface IUIIntegration { + /** + * Register a custom tab in the webview + * @param tab - Tab configuration + * @returns Disposable for cleanup + */ + registerTab(tab: ICustomTab): vscode.Disposable; + + /** + * Register a history item provider + * @param provider - History provider configuration + * @returns Disposable for cleanup + */ + registerHistoryProvider(provider: IHistoryItemProvider): vscode.Disposable; + + /** + * Register a settings section in the Settings tab + * @param section - Settings section configuration + * @returns Disposable for cleanup + */ + registerSettingsSection(section: ISettingsSection): vscode.Disposable; + + /** + * Refresh the webview UI + */ + refresh(): void; + + /** + * Get all registered tabs + */ + getTabs(): ICustomTab[]; + + /** + * Get all registered settings sections + */ + getSettingsSections(): ISettingsSection[]; +} + +/** + * Addon UI capabilities definition + */ +export interface IAddonUICapabilities { + /** + * Get content for a specific tab + * @param tabId - The tab identifier + */ + getTabContent?(tabId: string): Promise; + + /** + * Get history content for specific types + * @param types - History types to retrieve + */ + getHistoryContent?(...types: string[]): Promise; + + /** + * Handle clearing history for specific types + * @param types - History types to clear + */ + handleClearHistory?(...types: string[]): Promise; + + /** + * Get available history types + */ + getHistoryTypes?(): Promise; + + /** + * Custom tabs provided by this addon + */ + tabs?: ICustomTab[]; +} + +/** + * Custom tab definition for webview + */ +export interface ICustomTab { + /** Unique tab identifier */ + id: string; + + /** Display label */ + label: string; + + /** Codicon name for the tab icon */ + icon: keyof typeof codiconsLibrary; + + /** Sort priority (lower = first) */ + priority?: number; + + /** + * Render the tab content + * @returns HTML string to display + */ + render(): Promise | string; + + /** + * Handle messages from the webview + * @param message - Message from webview + * @returns Response to send back + */ + onMessage?(message: unknown): Promise; + + /** + * Called when the tab becomes active + */ + onActivate?(): void; + + /** + * Called when the tab becomes inactive + */ + onDeactivate?(): void; +} + +/** + * UI content descriptor for dynamic rendering + */ +export interface IUIContent { + /** Content type */ + type: 'web-component' | 'html'; + + /** For web-component type: custom element tag name */ + tagname?: string; + + /** Sorting key for ordering */ + sortingKey: string; + + /** Script URI for web components */ + scriptUri?: string; + + /** Raw HTML content (for html type) */ + html?: string; +} + +/** + * History type definition for filtering + */ +export interface IHistoryType { + /** Codicon name */ + icon: keyof typeof codiconsLibrary; + + /** Type identifier */ + type: string; + + /** Display label */ + label: string; +} + +/** + * History item provider interface + */ +export interface IHistoryItemProvider { + /** Provider identifier */ + id: string; + + /** History types this provider handles */ + types: string[]; + + /** + * Get history items + * @param type - Optional type filter + * @returns History items + */ + getItems(type?: string): Promise; + + /** + * Clear history items + * @param type - Optional type filter + */ + clearItems(type?: string): Promise; +} + +/** + * History item definition + */ +export interface IHistoryItem { + /** Unique item ID */ + id: string; + + /** Item type */ + type: string; + + /** Timestamp */ + timestamp: number; + + /** Display title */ + title: string; + + /** Optional description */ + description?: string; + + /** Item status */ + status?: 'completed' | 'cancelled' | 'pending'; + + /** Additional metadata */ + metadata?: Record; +} + +// ============================================================================ +// Settings Interfaces +// ============================================================================ + +/** + * Settings section definition + */ +export interface ISettingsSection { + /** Unique section identifier */ + id: string; + + /** Section title */ + title: string; + + /** Optional description */ + description?: string; + + /** Settings items in this section */ + settings: ISettingItem[]; + + /** Sort priority (lower = first) */ + priority?: number; +} + +/** + * Addon setting section (from addon definition) + */ +export interface IAddonSettingSection { + /** Setting key (will be prefixed with addon ID) */ + key: string; + + /** Display label */ + label: string; + + /** Optional description */ + description?: string; + + /** Settings in this section */ + settings: IAddonSettingDefinition[]; +} + +/** + * Individual setting definition from addon + */ +export interface IAddonSettingDefinition { + /** Setting key */ + key: string; + + /** Display label */ + label: string; + + /** Optional description */ + description?: string; + + /** Setting type */ + type: 'boolean' | 'string' | 'number' | 'select' | 'multiselect' | 'text'; + + /** Default value */ + defaultValue?: unknown; + + /** Options for select/multiselect types */ + options?: Array<{ value: string; label: string }>; + + /** Validation constraints */ + validation?: { + min?: number; + max?: number; + pattern?: string; + required?: boolean; + }; +} + +/** + * Setting item for UI rendering + */ +export interface ISettingItem { + /** Full setting key */ + key: string; + + /** Display label */ + label: string; + + /** Description */ + description?: string; + + /** Setting type */ + type: 'boolean' | 'string' | 'number' | 'select' | 'multiselect' | 'text'; + + /** Current value */ + value: unknown; + + /** Default value */ + defaultValue?: unknown; + + /** Options for select types */ + options?: Array<{ value: string; label: string }>; + + /** Whether setting is from VS Code configuration */ + isVSCodeSetting?: boolean; +} + +// ============================================================================ +// Tools Integration Interfaces +// ============================================================================ + +/** + * AI Tools integration interface + */ +export interface IToolsIntegration { + /** + * Register an AI tool + * @param tool - Tool configuration + * @returns Disposable for cleanup + */ + registerTool(tool: IAddonTool): vscode.Disposable; + + /** + * Get all registered tools + */ + getTools(): IAddonTool[]; + + /** + * Native askUser tool access + */ + askUser(params: IAskUserParams): Promise; + + /** + * Native planReview tool access + */ + planReview(params: IPlanReviewParams): Promise; +} + +/** + * Addon AI capabilities definition + */ +export interface IAddonAICapabilities { + /** Tools provided by this addon */ + tools: IAddonTool[]; +} + +/** + * Addon tool definition + */ +export interface IAddonTool { + /** Tool name (should be unique) */ + name: string; + + /** Tool description for LLM */ + description: string; + + /** JSON schema for input validation */ + inputSchema?: Record; + + /** Tags for categorization */ + tags?: string[]; + + /** + * Execute the tool + * @param params - Input parameters + * @param context - Execution context + * @param token - Cancellation token + * @returns Tool result + */ + execute( + params: unknown, + context: IToolExecutionContext, + token: vscode.CancellationToken + ): Promise; +} + +/** + * Tool execution context + */ +export interface IToolExecutionContext { + /** The Seamless Agent API */ + api: ISeamlessAgentAPI; + + /** Request ID for tracking */ + requestId: string; +} + +/** + * Parameters for askUser tool + */ +export interface IAskUserParams { + question: string; + title?: string; + agentName?: string; +} + +/** + * User response from askUser + */ +export interface IUserResponse { + responded: boolean; + response: string; + attachments: string[]; +} + +/** + * Parameters for planReview tool + */ +export interface IPlanReviewParams { + plan: string; + title?: string; + chatId?: string; + mode?: 'review' | 'walkthrough'; +} + +/** + * Result from planReview + */ +export interface IPlanReviewResult { + status: 'approved' | 'recreateWithChanges' | 'acknowledged' | 'cancelled'; + requiredRevisions?: Array<{ + revisedPart: string; + revisorInstructions: string; + }>; + reviewId: string; +} + +// ============================================================================ +// Event System Interfaces +// ============================================================================ + +/** + * Event emitter interface + */ +export interface IEventEmitter { + /** + * Subscribe to an event + * @param event - Event name + * @param listener - Event listener + * @returns Disposable for cleanup + */ + on(event: string, listener: (data: T) => void): vscode.Disposable; + + /** + * Subscribe to an event (one-time) + * @param event - Event name + * @param listener - Event listener + * @returns Disposable for cleanup + */ + once(event: string, listener: (data: T) => void): vscode.Disposable; + + /** + * Emit an event + * @param event - Event name + * @param data - Event data + */ + emit(event: string, data: T): void; + + /** + * Remove all listeners for an event + * @param event - Event name + */ + removeAllListeners(event?: string): void; +} + +/** + * Standard event names + */ +export const SeamlessAgentEvents = { + /** Addon registered */ + ADDON_REGISTERED: 'addon:registered', + /** Addon unregistered */ + ADDON_UNREGISTERED: 'addon:unregistered', + /** Settings changed */ + SETTINGS_CHANGED: 'settings:changed', + /** UI refresh requested */ + UI_REFRESH: 'ui:refresh', + /** Tool executed */ + TOOL_EXECUTED: 'tool:executed', + /** Tab changed */ + TAB_CHANGED: 'tab:changed', +} as const; + +// ============================================================================ +// Storage Integration Interfaces +// ============================================================================ + +/** + * Storage integration interface for addon data persistence + */ +export interface IStorageIntegration { + /** + * Get a stored value + * @param key - Storage key (will be namespaced by addon ID) + * @param defaultValue - Default value if not found + */ + get(key: string, defaultValue?: T): T | undefined; + + /** + * Set a stored value + * @param key - Storage key + * @param value - Value to store + */ + set(key: string, value: T): Promise; + + /** + * Delete a stored value + * @param key - Storage key + */ + delete(key: string): Promise; + + /** + * Get all keys for the addon + */ + keys(): string[]; + + /** + * Clear all addon storage + */ + clear(): Promise; +} + +// ============================================================================ +// Type Aliases for Tool Compatibility +// ============================================================================ + +/** + * Input parameters for the askUser tool + * (Alias for IAskUserParams for tool implementations) + */ +export type AskUserInput = IAskUserParams; + +/** + * Result from the askUser tool + * (Alias for IUserResponse for tool implementations) + */ +export type AskUserToolResult = IUserResponse; + +/** + * Input parameters for the planReview tool + * (Alias for IPlanReviewParams for tool implementations) + */ +export type PlanReviewInput = IPlanReviewParams; + +/** + * Result from the planReview tool + * (Alias for IPlanReviewResult for tool implementations) + */ +export type PlanReviewToolResult = IPlanReviewResult; diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..c46ec1a --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,180 @@ +import * as vscode from 'vscode'; +import { registerNativeTools } from '../tools'; +import { AgentInteractionProvider } from '../webview/webviewProvider'; +import { initializeChatHistoryStorage, getChatHistoryStorage } from '../storage/chatHistoryStorage'; +import { strings } from '../localization'; +import { OrchestrationAgent } from '../agent'; +import { ExtensionCoreOptions, IExtensionCore } from './types'; +import { SeamlessAgentAPI, createSeamlessAgentAPI } from '../api'; +import { AddonRegistry } from '../addons/registry'; + + +const PARTICIPANT_ID = 'seamless-agent.agent'; + + +/** + * Core class for the Seamless Agent extension. + * Manages initialization, lifecycle, and provides access to core services. + */ +export class ExtensionCore implements IExtensionCore { + private provider: AgentInteractionProvider; + private api: SeamlessAgentAPI; + public readonly subscriptions: vscode.Disposable[] = []; + + constructor(private context: vscode.ExtensionContext, options?: ExtensionCoreOptions) { + // Initialize the public API first (creates event emitter and registry) + this.api = createSeamlessAgentAPI(context); + this.subscriptions.push(this.api); + + // Initialize the chat history storage (must be done before tools are registered) + initializeChatHistoryStorage(this); + + if (options?.createMcpServer) { + this.setupMCPServer().then(() => { + console.log('MCP Server initialized'); + }).catch((err) => { + console.error('Error initializing MCP Server:', err); + }); + } + + // Register the webview provider for the Agent Console panel + this.provider = new AgentInteractionProvider(this); + + this.subscriptions.push( + vscode.window.registerWebviewViewProvider(AgentInteractionProvider.viewType, this.provider, { + webviewOptions: { retainContextWhenHidden: true } + }) + ); + + // Register the ask_user tool with the webview provider + // This also sets up the native tool functions in the API + registerNativeTools(this, this.provider); + + // Register command to cancel pending plans + const cancelPendingPlansCommand = vscode.commands.registerCommand('seamless-agent.cancelPendingPlans', async () => { + const storage = getChatHistoryStorage(); + const pendingReviews = storage.getPendingPlanReviews(); + + if (pendingReviews.length === 0) { + vscode.window.showInformationMessage('No pending plan reviews to cancel.'); + return; + } + + // Create QuickPick items + const items = pendingReviews.map(review => ({ + label: review.title || 'Plan Review', + description: `Created: ${new Date(review.timestamp).toLocaleString()}`, + detail: review.plan?.substring(0, 100) + (review.plan && review.plan.length > 100 ? '...' : ''), + id: review.id + })); + + // Show QuickPick with multi-select + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Select pending plans to cancel', + canPickMany: true, + title: 'Cancel Pending Plans' + }); + + if (selected && selected.length > 0) { + // Import PlanReviewPanel to close any open panels + const { PlanReviewPanel } = await import('../webview/planReviewPanel'); + + // Mark selected plans as cancelled and close their panels + for (const item of selected) { + storage.updateInteraction(item.id, { status: 'cancelled' }); + // Close the panel if it's open + PlanReviewPanel.closeIfOpen(item.id); + } + + vscode.window.showInformationMessage(`Cancelled ${selected.length} pending plan(s).`); + + // Refresh the panel if it's visible + this.provider.refreshHome(); + } + }); + + this.subscriptions.push(cancelPendingPlansCommand); + + // Register command to show pending requests + const showPendingCommand = vscode.commands.registerCommand('seamless-agent.showPending', () => { + this.provider.switchTab('pending'); + }); + this.subscriptions.push(showPendingCommand); + + // Register command to show history + const showHistoryCommand = vscode.commands.registerCommand('seamless-agent.showHistory', () => { + this.provider.switchTab('history'); + }); + this.subscriptions.push(showHistoryCommand); + + // Register command to clear history + const clearHistoryCommand = vscode.commands.registerCommand('seamless-agent.clearHistory', async () => { + const result = await vscode.window.showWarningMessage( + strings.confirmClearHistory, + { modal: true }, + strings.confirm + ); + if (result === strings.confirm) { + this.provider.clearHistory(); + } + }); + + this.subscriptions.push(clearHistoryCommand); + + const orchestrationAgent = new OrchestrationAgent(); + + const participant = vscode.chat.createChatParticipant(PARTICIPANT_ID, orchestrationAgent.handler); + participant.iconPath = new vscode.ThemeIcon('question'); + + this.subscriptions.push(participant, orchestrationAgent); + } + + /** + * Get the VS Code extension context + */ + getContext(): vscode.ExtensionContext { + return this.context; + } + + /** + * Get the public API instance for addon integration + */ + getAPI(): SeamlessAgentAPI { + return this.api; + } + + /** + * Get the addon registry + */ + getAddonRegistry(): AddonRegistry { + return this.api.registry; + } + + /** + * Get the webview provider + */ + getProvider(): AgentInteractionProvider { + return this.provider; + } + + /** + * Dispose all resources + */ + dispose() { + this.subscriptions.forEach(sub => sub.dispose()); + } + + private async setupMCPServer(): Promise { + const { ApiServiceManager } = await import('../mcp/apiService'); + const apiServiceManager = new ApiServiceManager(this, this.provider); + apiServiceManager.start().then(() => { + // Register restart command + this.subscriptions.push( + vscode.commands.registerCommand('seamless-agent.restartMcpServer', async () => { + await apiServiceManager?.restart(); + }) + ); + }); + } + +} \ No newline at end of file diff --git a/src/core/types.ts b/src/core/types.ts new file mode 100644 index 0000000..2b44a5b --- /dev/null +++ b/src/core/types.ts @@ -0,0 +1,44 @@ +import type * as vscode from 'vscode'; +import type { SeamlessAgentAPI } from '../api/SeamlessAgentAPI'; +import type { AddonRegistry } from '../addons/registry'; +import type { AgentInteractionProvider } from '../webview/webviewProvider'; + +/** + * Core extension interface + */ +export interface IExtensionCore { + /** + * Get the VS Code extension context + */ + getContext(): vscode.ExtensionContext; + + /** + * Extension subscriptions for cleanup + */ + readonly subscriptions: vscode.Disposable[]; + + /** + * Get the public API instance + */ + getAPI(): SeamlessAgentAPI; + + /** + * Get the addon registry + */ + getAddonRegistry(): AddonRegistry; + + /** + * Get the webview provider + */ + getProvider(): AgentInteractionProvider; +} + +/** + * Extension core initialization options + */ +export type ExtensionCoreOptions = { + /** + * Whether to create the MCP server + */ + createMcpServer?: boolean; +} \ No newline at end of file diff --git a/src/extension.antigravity.ts b/src/extension.antigravity.ts index 43c61b9..68074d0 100644 --- a/src/extension.antigravity.ts +++ b/src/extension.antigravity.ts @@ -1,145 +1,14 @@ import * as vscode from 'vscode'; -import { registerNativeTools, askUser } from './tools'; -import { AgentInteractionProvider } from './webview/webviewProvider'; -import { ApiServiceManager } from './mcp/apiService'; -import { initializeChatHistoryStorage, getChatHistoryStorage } from './storage/chatHistoryStorage'; -import { strings } from './localization'; +import { ExtensionCore } from './core'; -const PARTICIPANT_ID = 'seamless-agent.agent'; -let apiServiceManager: ApiServiceManager | undefined; -export async function activate(context: vscode.ExtensionContext) { - console.log('Seamless Agent extension active'); - - // Initialize the chat history storage (must be done before provider is created) - initializeChatHistoryStorage(context); - - // Create provider - const provider = new AgentInteractionProvider(context); - - // Register webview provider - context.subscriptions.push( - vscode.window.registerWebviewViewProvider(AgentInteractionProvider.viewType, provider, { - webviewOptions: { - retainContextWhenHidden: true - } - }) - ); - - // Initialize API Service (replaces MCP Server) - apiServiceManager = new ApiServiceManager(context, provider); - await apiServiceManager.start(); - - // Register restart command - context.subscriptions.push( - vscode.commands.registerCommand('seamless-agent.restartMcpServer', async () => { - await apiServiceManager?.restart(); - }) - ); - - // Create Status Bar Item - const restartStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); - restartStatusBarItem.command = 'seamless-agent.restartMcpServer'; - restartStatusBarItem.text = '$(sync) Restart API'; - restartStatusBarItem.tooltip = 'Restart the Seamless Agent API Service'; - restartStatusBarItem.show(); - context.subscriptions.push(restartStatusBarItem); - - // Register chat participant - const handler: vscode.ChatRequestHandler = async (request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken) => { - // Chat handler implementation... - - try { - await askUser({ - question: "This is a test question from the chat participant. Do you accept?", - title: "Chat Confirmation" - }, provider, token); - - stream.markdown('User accepted the prompt!'); - } catch (err) { - stream.markdown('User declined or request failed.'); - } - - return { metadata: { command: '' } }; - }; - const transcriptParticipant = vscode.chat.createChatParticipant(PARTICIPANT_ID, handler); - transcriptParticipant.iconPath = vscode.Uri.joinPath(context.extensionUri, 'resources', 'icon.png'); - context.subscriptions.push(transcriptParticipant); - - // Keep the registerNativeTools for backward compatibility or direct usage - try { - registerNativeTools(context, provider); - } catch (e) { - console.warn('Failed to register native tools:', e); - } - - // Register command to cancel pending plans - const cancelPendingPlansCommand = vscode.commands.registerCommand('seamless-agent.cancelPendingPlans', async () => { - const storage = getChatHistoryStorage(); - const pendingReviews = storage.getPendingPlanReviews(); - - if (pendingReviews.length === 0) { - vscode.window.showInformationMessage('No pending plan reviews to cancel.'); - return; - } - - const items = pendingReviews.map(review => ({ - label: review.title || 'Plan Review', - description: `Created: ${new Date(review.timestamp).toLocaleString()}`, - detail: review.plan?.substring(0, 100) + (review.plan && review.plan.length > 100 ? '...' : ''), - id: review.id - })); - - const selected = await vscode.window.showQuickPick(items, { - placeHolder: 'Select pending plans to cancel', - canPickMany: true, - title: 'Cancel Pending Plans' - }); - - if (selected && selected.length > 0) { - const { PlanReviewPanel } = await import('./webview/planReviewPanel'); - for (const item of selected) { - storage.updateInteraction(item.id, { status: 'cancelled' }); - PlanReviewPanel.closeIfOpen(item.id); - } - vscode.window.showInformationMessage(`Cancelled ${selected.length} pending plan(s).`); - provider.refreshHome(); - } - }); - context.subscriptions.push(cancelPendingPlansCommand); - - // Register command to show pending requests - const showPendingCommand = vscode.commands.registerCommand('seamless-agent.showPending', () => { - provider.switchTab('pending'); - }); - context.subscriptions.push(showPendingCommand); - - // Register command to show history - const showHistoryCommand = vscode.commands.registerCommand('seamless-agent.showHistory', () => { - provider.switchTab('history'); - }); - context.subscriptions.push(showHistoryCommand); - - // Register command to clear history - const clearHistoryCommand = vscode.commands.registerCommand('seamless-agent.clearHistory', async () => { - const result = await vscode.window.showWarningMessage( - strings.confirmClearHistory, - { modal: true }, - strings.confirm - ); - if (result === strings.confirm) { - const storage = getChatHistoryStorage(); - storage.clearAll(); - provider.refreshHome(); - } - }); - context.subscriptions.push(clearHistoryCommand); +export function activate(context: vscode.ExtensionContext) { + console.log('Seamless Agent extension active'); + const core = new ExtensionCore(context,); + context.subscriptions.push(core); } -// This method is called when your extension is deactivated export function deactivate() { - if (apiServiceManager) { - apiServiceManager.dispose(); - } -} + console.log('Seamless Agent extension deactivated'); +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 07c0cea..43b360f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,182 +1,38 @@ import * as vscode from 'vscode'; -import { registerNativeTools } from './tools'; -import { AgentInteractionProvider } from './webview/webviewProvider'; -import { initializeChatHistoryStorage, getChatHistoryStorage } from './storage/chatHistoryStorage'; -import { strings } from './localization'; - -const PARTICIPANT_ID = 'seamless-agent.agent'; - -// Store provider reference for cleanup on deactivation -let agentProvider: AgentInteractionProvider | null = null; - -export function activate(context: vscode.ExtensionContext) { +import { ExtensionCore } from './core'; +import type { ISeamlessAgentAPI } from './api'; + +// Store reference to core for API access +let core: ExtensionCore | undefined; + +/** + * Activate the Seamless Agent extension. + * Returns the public API for addon extensions to integrate. + * + * @example + * ```typescript + * // In an addon extension: + * const seamlessExt = vscode.extensions.getExtension('jraylan.seamless-agent'); + * if (seamlessExt) { + * const api = await seamlessExt.activate(); + * const registration = api.registerAddon({ ... }); + * } + * ``` + */ +export function activate(context: vscode.ExtensionContext): ISeamlessAgentAPI { console.log('Seamless Agent extension active'); - - // Initialize the chat history storage (must be done before tools are registered) - initializeChatHistoryStorage(context); - - // Register the webview provider for the Agent Console panel - const provider = new AgentInteractionProvider(context); - agentProvider = provider; // Store reference for deactivation cleanup - - (context.subscriptions as unknown as Array).push( - vscode.window.registerWebviewViewProvider(AgentInteractionProvider.viewType, provider, { - webviewOptions: { retainContextWhenHidden: true } - }) - ); - - // Register the ask_user tool with the webview provider - registerNativeTools(context, provider); - - // Register command to cancel pending plans - const cancelPendingPlansCommand = vscode.commands.registerCommand('seamless-agent.cancelPendingPlans', async () => { - const storage = getChatHistoryStorage(); - const pendingReviews = storage.getPendingPlanReviews(); - - if (pendingReviews.length === 0) { - vscode.window.showInformationMessage('No pending plan reviews to cancel.'); - return; - } - - // Create QuickPick items - const items = pendingReviews.map(review => ({ - label: review.title || 'Plan Review', - description: `Created: ${new Date(review.timestamp).toLocaleString()}`, - detail: review.plan?.substring(0, 100) + (review.plan && review.plan.length > 100 ? '...' : ''), - id: review.id - })); - - // Show QuickPick with multi-select - const selected = await vscode.window.showQuickPick(items, { - placeHolder: 'Select pending plans to cancel', - canPickMany: true, - title: 'Cancel Pending Plans' - }); - - if (selected && selected.length > 0) { - // Import PlanReviewPanel to close any open panels - const { PlanReviewPanel } = await import('./webview/planReviewPanel'); - - // Mark selected plans as cancelled and close their panels - for (const item of selected) { - storage.updateInteraction(item.id, { status: 'cancelled' }); - // Close the panel if it's open - PlanReviewPanel.closeIfOpen(item.id); - } - - vscode.window.showInformationMessage(`Cancelled ${selected.length} pending plan(s).`); - - // Refresh the panel if it's visible - provider.refreshHome(); - } - }); - - (context.subscriptions as unknown as Array).push(cancelPendingPlansCommand); - - // Register command to show pending requests - const showPendingCommand = vscode.commands.registerCommand('seamless-agent.showPending', () => { - provider.switchTab('pending'); - }); - (context.subscriptions as unknown as Array).push(showPendingCommand); - - // Register command to show history - const showHistoryCommand = vscode.commands.registerCommand('seamless-agent.showHistory', () => { - provider.switchTab('history'); - }); - (context.subscriptions as unknown as Array).push(showHistoryCommand); - - // Register command to clear history - const clearHistoryCommand = vscode.commands.registerCommand('seamless-agent.clearHistory', async () => { - const result = await vscode.window.showWarningMessage( - strings.confirmClearHistory, - { modal: true }, - strings.confirm - ); - if (result === strings.confirm) { - provider.clearHistory(); - } - }); - (context.subscriptions as unknown as Array).push(clearHistoryCommand); - - // Create a Chat Participant that uses our tool - const handler: vscode.ChatRequestHandler = async ( - request: vscode.ChatRequest, - chatContext: vscode.ChatContext, - stream: vscode.ChatResponseStream, - token: vscode.CancellationToken - ) => { - // Get our ask_user tool - const askUserTool = vscode.lm.tools.find(t => t.name === 'ask_user'); - - // Get all available tools including ours - const tools = askUserTool ? [askUserTool] : []; - - // Build the prompt with instruction to use the tool - const systemPrompt = `You are a helpful assistant. -IMPORTANT: You have access to an "ask_user" tool. You MUST use this tool before completing any task to confirm with the user that their request was fulfilled correctly. -Never finish a response without first calling the ask_user tool to verify with the user.`; - - const messages = [ - vscode.LanguageModelChatMessage.User(systemPrompt), - vscode.LanguageModelChatMessage.User(request.prompt) - ]; - - // Get the model - let model = request.model; - - const options: vscode.LanguageModelChatRequestOptions = { - tools: tools.map(t => ({ - name: t.name, - description: t.description, - inputSchema: t.inputSchema - })), - }; - - try { - const response = await model.sendRequest(messages, options, token); - - for await (const part of response.stream) { - if (part instanceof vscode.LanguageModelTextPart) { - stream.markdown(part.value); - } else if (part instanceof vscode.LanguageModelToolCallPart) { - // Handle tool calls - stream.progress(`Calling ${part.name}...`); - const toolResult = await vscode.lm.invokeTool(part.name, { - input: part.input, - toolInvocationToken: request.toolInvocationToken - }, token); - - // Show tool result - for (const resultPart of toolResult.content) { - if (resultPart instanceof vscode.LanguageModelTextPart) { - stream.markdown(`\n\n**User Response:** ${resultPart.value}\n\n`); - } - } - } - } - } catch (err) { - if (err instanceof vscode.LanguageModelError) { - stream.markdown(`Error: ${err.message}`); - } else { - throw err; - } - } - - return; - }; - - // Register the chat participant - const participant = vscode.chat.createChatParticipant(PARTICIPANT_ID, handler); - participant.iconPath = new vscode.ThemeIcon('question'); - - (context.subscriptions as unknown as Array).push(participant); + core = new ExtensionCore(context); + context.subscriptions.push(core); + + // Return the public API for addon extensions + return core.getAPI(); } export function deactivate() { - // Clean up any orphaned temp files on extension deactivation - if (agentProvider) { - agentProvider.cleanupAllTempFiles(); - agentProvider = null; - } console.log('Seamless Agent extension deactivated'); -} \ No newline at end of file + core = undefined; +} + +// Re-export public API types for addon developers +export * from './api'; +export type { IAddon, IAddonRegistration } from './api/types'; \ No newline at end of file diff --git a/src/localization.ts b/src/localization.ts index 9749cce..23225b6 100644 --- a/src/localization.ts +++ b/src/localization.ts @@ -144,4 +144,14 @@ export const strings = { get attachmentFolderDepth2() { return localize('attachment.folderDepth.depth2'); }, get attachmentFolderDepthRecursive() { return localize('attachment.folderDepth.recursive'); }, get pastedImage() { return localize('attachment.pastedImage'); }, + + // Settings + get settings() { return localize('settings.title'); }, + get settingsDescription() { return localize('settings.description'); }, + get loadingSettings() { return localize('settings.loading'); }, + get registeredAddons() { return localize('settings.registeredAddons'); }, + get noAddonsRegistered() { return localize('settings.noAddonsRegistered'); }, + get openInVSCodeSettings() { return localize('settings.openInVSCodeSettings'); }, + get addonVersion() { return localize('settings.addonVersion'); }, + get addonAuthor() { return localize('settings.addonAuthor'); }, }; diff --git a/src/mcp/apiService.ts b/src/mcp/apiService.ts index 65c6064..dfb1985 100644 --- a/src/mcp/apiService.ts +++ b/src/mcp/apiService.ts @@ -4,6 +4,7 @@ import * as crypto from 'crypto'; import { AgentInteractionProvider } from '../webview/webviewProvider'; import { askUser, planReview } from '../tools'; import { PlanReviewInput, parsePlanReviewInput } from '../tools/schemas'; +import { IExtensionCore } from '../core/types'; export { planReviewApproval, walkthroughReview } from '../tools/planReview'; @@ -22,19 +23,19 @@ export class ApiServiceManager { private authToken: string | undefined; constructor( - private context: vscode.ExtensionContext, + private core: IExtensionCore, private provider: AgentInteractionProvider ) { } async start() { try { this.port = await this.findAvailablePort(); - const storedToken = this.context.globalState.get('seamlessAgent.apiAuthToken'); + const storedToken = this.core.getContext().globalState.get('seamlessAgent.apiAuthToken'); if (storedToken && typeof storedToken === 'string' && storedToken.length >= 20) { this.authToken = storedToken; } else { this.authToken = crypto.randomBytes(32).toString('base64url'); - await this.context.globalState.update('seamlessAgent.apiAuthToken', this.authToken); + await this.core.getContext().globalState.update('seamlessAgent.apiAuthToken', this.authToken); } console.log(`Starting API service on port ${this.port}`); @@ -224,7 +225,7 @@ export class ApiServiceManager { try { const result = await planReview( params, - this.context, + this.core, this.provider, tokenSource.token ); @@ -355,7 +356,7 @@ export class ApiServiceManager { const mcpConfigPath = path.join(os.homedir(), '.gemini', 'antigravity', 'mcp_config.json'); // Get the path to the bundled CLI script in dist/ - const cliScriptPath = path.join(this.context.extensionPath, 'dist', 'seamless-agent-mcp.js'); + const cliScriptPath = path.join(this.core.getContext().extensionPath, 'dist', 'seamless-agent-mcp.js'); try { // Ensure directory exists diff --git a/src/mcp/mcpServer.ts b/src/mcp/mcpServer.ts index 4dba60d..b32d792 100644 --- a/src/mcp/mcpServer.ts +++ b/src/mcp/mcpServer.ts @@ -9,6 +9,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/ import { z } from 'zod'; import { AgentInteractionProvider } from '../webview/webviewProvider'; import { askUser, planReviewApproval, walkthroughReview } from '../tools'; +import { IExtensionCore } from '../core/types'; export class McpServerManager { private server: http.Server | undefined; @@ -17,7 +18,7 @@ export class McpServerManager { private transport: StreamableHTTPServerTransport | undefined; constructor( - private context: vscode.ExtensionContext, + private core: IExtensionCore, private provider: AgentInteractionProvider ) { } @@ -104,7 +105,7 @@ export class McpServerManager { title: args.title ? String(args.title) : undefined, chatId: args.chatId ? String(args.chatId) : undefined }, - this.context, + this.core, this.provider, tokenSource.token ); @@ -146,7 +147,7 @@ export class McpServerManager { title: args.title ? String(args.title) : undefined, chatId: args.chatId ? String(args.chatId) : undefined }, - this.context, + this.core, this.provider, tokenSource.token ); diff --git a/src/storage/chatHistoryStorage.ts b/src/storage/chatHistoryStorage.ts index 6b8053d..87b19b9 100644 --- a/src/storage/chatHistoryStorage.ts +++ b/src/storage/chatHistoryStorage.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import type { RequiredPlanRevisions, StoredInteraction } from '../webview/types'; import { getStorageContext } from '../config/storage'; +import { IExtensionCore } from '../core/types'; /** * Storage keys for global state @@ -15,11 +16,9 @@ const STORAGE_KEYS = { * Simplified: each interaction is individual, no chat grouping */ export class ChatHistoryStorage { - private context: vscode.ExtensionContext; private config: vscode.WorkspaceConfiguration; - constructor(context: vscode.ExtensionContext) { - this.context = context; + constructor(private core: IExtensionCore) { this.config = vscode.workspace.getConfiguration('seamless-agent'); } @@ -30,9 +29,9 @@ export class ChatHistoryStorage { get storage(): vscode.Memento { if (getStorageContext() === 'workspace') { - return this.context.workspaceState; + return this.core.getContext().workspaceState; } - return this.context.globalState; + return this.core.getContext().globalState; } /** @@ -263,8 +262,8 @@ let storageInstance: ChatHistoryStorage | undefined; /** * Initialize the storage with extension context */ -export function initializeChatHistoryStorage(context: vscode.ExtensionContext): ChatHistoryStorage { - storageInstance = new ChatHistoryStorage(context); +export function initializeChatHistoryStorage(core: IExtensionCore): ChatHistoryStorage { + storageInstance = new ChatHistoryStorage(core); return storageInstance; } diff --git a/src/tools/askUser.ts b/src/tools/askUser.ts index ca5804a..4f90c56 100644 --- a/src/tools/askUser.ts +++ b/src/tools/askUser.ts @@ -2,7 +2,6 @@ import * as vscode from 'vscode'; import { strings } from '../localization'; import { AgentInteractionProvider } from '../webview/webviewProvider'; import { UserResponseResult } from '../webview/types'; -import { getChatHistoryStorage } from '../storage/chatHistoryStorage'; import { AskUserInput, AskUserToolResult } from './schemas'; /** diff --git a/src/tools/index.ts b/src/tools/index.ts index 9b9f034..06ba935 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -27,11 +27,12 @@ import { parsePlanReviewInput, parseWalkthroughReviewInput, } from './schemas'; +import { IExtensionCore } from '../core/types'; /** * Registers the native VS Code LM Tools */ -export function registerNativeTools(context: vscode.ExtensionContext, provider: AgentInteractionProvider) { +export function registerNativeTools(core: IExtensionCore, provider: AgentInteractionProvider) { // Register the tool defined in package.json const confirmationTool = vscode.lm.registerTool('ask_user', { @@ -133,7 +134,7 @@ export function registerNativeTools(context: vscode.ExtensionContext, provider: title: params.title, chatId: undefined }, - context, + core, provider, token ); @@ -173,7 +174,7 @@ export function registerNativeTools(context: vscode.ExtensionContext, provider: title: params.title, chatId: params.chatId }, - context, + core, provider, token ); @@ -209,7 +210,7 @@ export function registerNativeTools(context: vscode.ExtensionContext, provider: title: params.title, chatId: params.chatId }, - context, + core, provider, token ); @@ -220,7 +221,7 @@ export function registerNativeTools(context: vscode.ExtensionContext, provider: } }); - (context.subscriptions as unknown as Array).push( + core.subscriptions.push( confirmationTool, approvePlanTool, planReviewTool, @@ -228,5 +229,36 @@ export function registerNativeTools(context: vscode.ExtensionContext, provider: ); // Initialize chat history storage - initializeChatHistoryStorage(context); + initializeChatHistoryStorage(core); + + // Set up native tool functions in the API for addon access + const api = core.getAPI(); + + // Configure askUser implementation (internal - addons call api.tools.askUser()) + api._setAskUserFunction(async (params) => { + const result = await askUser( + { question: params.question, title: params.title, agentName: params.agentName }, + provider, + new vscode.CancellationTokenSource().token + ); + return result; + }); + + // Configure planReview implementation (internal - addons call api.tools.planReview()) + api._setPlanReviewFunction(async (params) => { + const { planReviewApproval: planReviewFn } = await import('./planReview'); + const result = await planReviewFn( + { plan: params.plan, title: params.title, chatId: params.chatId }, + core, + provider, + new vscode.CancellationTokenSource().token + ); + return { + status: result.status, + requiredRevisions: result.requiredRevisions, + reviewId: result.reviewId + }; + }); + + console.log('[Seamless Agent] Native tools registered and API functions configured'); } diff --git a/src/tools/planReview.ts b/src/tools/planReview.ts index e57e9b9..e6f18d6 100644 --- a/src/tools/planReview.ts +++ b/src/tools/planReview.ts @@ -4,6 +4,7 @@ import type { PlanReviewOptions } from '../webview/types'; import { AgentInteractionProvider } from '../webview/webviewProvider'; import { getChatHistoryStorage } from '../storage/chatHistoryStorage'; import { PlanReviewInput, PlanReviewToolResult, WalkthroughReviewInput } from './schemas'; +import { IExtensionCore } from '../core/types'; export type PlanReviewApprovalInput = Pick; @@ -14,7 +15,7 @@ export type PlanReviewApprovalInput = Pick { @@ -67,7 +68,7 @@ export async function planReview( }; // Show the plan review panel - const result = await PlanReviewPanel.showWithOptions(context.extensionUri, options); + const result = await PlanReviewPanel.showWithOptions(core.getContext().extensionUri, options); const interactionState = ['approved', 'recreateWithChanges', 'acknowledged'].includes(result.action) @@ -117,7 +118,7 @@ export async function planReview( */ export async function planReviewApproval( params: PlanReviewApprovalInput, - context: vscode.ExtensionContext, + core: IExtensionCore, provider: AgentInteractionProvider, token: vscode.CancellationToken ): Promise { @@ -128,7 +129,7 @@ export async function planReviewApproval( mode: 'review', chatId: params.chatId }, - context, + core, provider, token ); @@ -139,7 +140,7 @@ export async function planReviewApproval( */ export async function walkthroughReview( params: WalkthroughReviewInput, - context: vscode.ExtensionContext, + core: IExtensionCore, provider: AgentInteractionProvider, token: vscode.CancellationToken ): Promise { @@ -150,7 +151,7 @@ export async function walkthroughReview( mode: 'walkthrough', chatId: params.chatId }, - context, + core, provider, token ); diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..056eeea --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,6 @@ + + +// Type definitions for @vscode/codicons/dist/codiconsUtil.js +declare module "*codiconsUtil.js" { + export function register(name: string, code: number): any; +} \ No newline at end of file diff --git a/src/webview/main.ts b/src/webview/main.ts index d08e9db..20df667 100644 --- a/src/webview/main.ts +++ b/src/webview/main.ts @@ -111,6 +111,9 @@ declare global { chatHistory: string; clearHistory: string; pastedImage: string; + // Settings tab + settings: string; + noAddonsRegistered: string; } ; @@ -174,7 +177,7 @@ import type { // History filter state let currentHistoryFilter: string = 'all'; - type HomeTab = 'pending' | 'history'; + type HomeTab = 'pending' | 'history' | 'settings'; function setHomeToolbarActiveTab(tab: HomeTab): void { document.querySelectorAll('.home-toolbar-btn[data-tab]').forEach(btn => { @@ -247,6 +250,12 @@ import type { type: 'clearHistory' }); }); + + // Settings link to open VS Code settings + const settingsLinkBtn = document.querySelector('.settings-link-btn[data-action="openVSCodeSettings"]') as HTMLElement | null; + settingsLinkBtn?.addEventListener('click', () => { + vscode.postMessage({ type: 'openVSCodeSettings' }); + }); } /** @@ -545,17 +554,27 @@ import type { /** * Switch between tabs in the home view */ - function switchTab(tab: 'pending' | 'history'): void { + function switchTab(tab: 'pending' | 'history' | 'settings'): void { + // Get settings content element + const contentSettings = document.getElementById('content-settings'); + // Update content panes visibility contentPending?.classList.toggle('hidden', tab !== 'pending'); contentHistory?.classList.toggle('hidden', tab !== 'history'); + contentSettings?.classList.toggle('hidden', tab !== 'settings'); + + setHomeToolbarActiveTab(tab as HomeTab); - setHomeToolbarActiveTab(tab); + // If switching to settings, request settings data + if (tab === 'settings') { + vscode.postMessage({ type: 'getSettings' }); + } // Announce tab change to screen readers const tabNames: Record = { pending: window.__STRINGS__?.pendingItems || 'Pending Items', history: window.__STRINGS__?.chatHistory || 'Chat History', + settings: window.__STRINGS__?.settings || 'Settings', } ; @@ -886,6 +905,284 @@ import type { updateHomeToolbarBadgesFromDom(); } + /** + * Settings data types (matching types.ts) + */ + interface SettingItemData { + key: string; + label: string; + description?: string; + type: 'boolean' | 'string' | 'number' | 'select' | 'multiselect' | 'text'; + value: unknown; + defaultValue?: unknown; + options?: Array<{ value: string; label: string }>; + } + + interface SettingsSectionData { + id: string; + title: string; + description?: string; + settings: SettingItemData[]; + priority?: number; + } + + interface AddonInfoData { + id: string; + name: string; + version: string; + description?: string; + author?: string; + repositoryUrl?: string; + isActive: boolean; + toolCount: number; + tabCount: number; + } + + /** + * Render settings in the Settings tab + * Note: Native Seamless Agent settings are now accessed via VS Code Settings + * through the link button in the webview + */ + function renderSettings(sections: SettingsSectionData[], addons: AddonInfoData[]): void { + const addonSettingsSections = document.getElementById('addon-settings-sections'); + const addonsListContent = document.getElementById('addons-list-content'); + + // Render addon settings sections (native settings are accessed via VS Code link) + if (addonSettingsSections) { + clearChildren(addonSettingsSections); + // All sections are addon sections now (native settings removed from API response) + for (const section of sections) { + addonSettingsSections.appendChild(renderSettingsSection(section)); + } + } + + // Render registered addons list + if (addonsListContent) { + clearChildren(addonsListContent); + if (addons.length === 0) { + addonsListContent.appendChild(el('p', { + className: 'placeholder', + text: window.__STRINGS__?.noAddonsRegistered || 'No addons registered' + })); + } else { + for (const addon of addons) { + addonsListContent.appendChild(renderAddonCard(addon)); + } + } + } + + // Note: collapse/expand is handled by event delegation initialized at startup + } + + /** + * Render a settings section + */ + function renderSettingsSection(section: SettingsSectionData): HTMLElement { + const sectionEl = el('div', { className: 'settings-section' }); + + const header = el('div', { + className: 'settings-section-header', + attrs: { 'data-section': section.id } + }); + appendChildren(header, codicon('chevron-down'), ' '); + header.appendChild(el('h4', { text: section.title })); + + const content = el('div', { className: 'settings-section-content' }); + + if (section.description) { + content.appendChild(el('p', { + className: 'settings-description', + text: section.description + })); + } + + for (const setting of section.settings) { + content.appendChild(renderSettingItem(setting)); + } + + appendChildren(sectionEl, header, content); + return sectionEl; + } + + /** + * Render a single setting item + */ + function renderSettingItem(setting: SettingItemData): HTMLElement { + const item = el('div', { className: 'setting-item' }); + + switch (setting.type) { + case 'boolean': + const checkboxRow = el('div', { className: 'setting-item-row setting-checkbox' }); + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.id = `setting-${setting.key}`; + checkbox.checked = Boolean(setting.value); + checkbox.addEventListener('change', () => { + vscode.postMessage({ + type: 'updateSetting', + key: setting.key, + value: checkbox.checked + }); + }); + const checkboxLabel = el('label', { + className: 'setting-label', + text: setting.label, + attrs: { for: `setting-${setting.key}` } + }); + appendChildren(checkboxRow, checkbox, checkboxLabel); + item.appendChild(checkboxRow); + break; + + case 'select': + item.appendChild(el('label', { + className: 'setting-label', + text: setting.label + })); + const select = document.createElement('select'); + select.className = 'setting-select'; + select.id = `setting-${setting.key}`; + for (const opt of setting.options || []) { + const option = document.createElement('option'); + option.value = opt.value; + option.textContent = opt.label; + if (opt.value === setting.value) { + option.selected = true; + } + select.appendChild(option); + } + select.addEventListener('change', () => { + vscode.postMessage({ + type: 'updateSetting', + key: setting.key, + value: select.value + }); + }); + item.appendChild(select); + break; + + case 'string': + case 'number': + item.appendChild(el('label', { + className: 'setting-label', + text: setting.label + })); + const input = document.createElement('input'); + input.type = setting.type === 'number' ? 'number' : 'text'; + input.className = 'setting-input'; + input.id = `setting-${setting.key}`; + input.value = String(setting.value ?? ''); + input.addEventListener('change', () => { + const value = setting.type === 'number' + ? parseFloat(input.value) + : input.value; + vscode.postMessage({ + type: 'updateSetting', + key: setting.key, + value + }); + }); + item.appendChild(input); + break; + + case 'text': + item.appendChild(el('label', { + className: 'setting-label', + text: setting.label + })); + const textarea = document.createElement('textarea'); + textarea.className = 'setting-input'; + textarea.id = `setting-${setting.key}`; + textarea.rows = 3; + textarea.value = String(setting.value ?? ''); + textarea.addEventListener('change', () => { + vscode.postMessage({ + type: 'updateSetting', + key: setting.key, + value: textarea.value + }); + }); + item.appendChild(textarea); + break; + } + + if (setting.description) { + item.appendChild(el('p', { + className: 'setting-description', + text: setting.description + })); + } + + return item; + } + + /** + * Render an addon info card + */ + function renderAddonCard(addon: AddonInfoData): HTMLElement { + const card = el('div', { className: 'addon-card' }); + + const header = el('div', { className: 'addon-card-header' }); + header.appendChild(el('span', { className: 'addon-card-name', text: addon.name })); + header.appendChild(el('span', { + className: 'addon-card-version', + text: `v${addon.version}` + })); + header.appendChild(el('span', { + className: `addon-status ${addon.isActive ? 'active' : 'inactive'}`, + text: addon.isActive ? 'Active' : 'Inactive' + })); + card.appendChild(header); + + if (addon.description) { + card.appendChild(el('p', { + className: 'addon-card-description', + text: addon.description + })); + } + + const meta = el('div', { className: 'addon-card-meta' }); + if (addon.author) { + const authorSpan = el('span'); + appendChildren(authorSpan, codicon('account'), ' ', addon.author); + meta.appendChild(authorSpan); + } + if (addon.toolCount > 0) { + const toolsSpan = el('span'); + appendChildren(toolsSpan, codicon('tools'), ` ${addon.toolCount} tools`); + meta.appendChild(toolsSpan); + } + if (addon.tabCount > 0) { + const tabsSpan = el('span'); + appendChildren(tabsSpan, codicon('layout'), ` ${addon.tabCount} tabs`); + meta.appendChild(tabsSpan); + } + if (meta.children.length > 0) { + card.appendChild(meta); + } + + return card; + } + + /** + * Initialize settings sections collapse/expand behavior using event delegation. + * This should only be called once during initialization. + */ + function initSettingsSections(): void { + const settingsContainer = document.getElementById('content-settings'); + if (!settingsContainer) {return;} + + // Use event delegation to handle clicks on section headers + // This way we don't need to re-attach listeners when content is re-rendered + settingsContainer.addEventListener('click', (event) => { + const target = event.target as HTMLElement; + const header = target.closest('.settings-section-header'); + if (header) { + const section = header.closest('.settings-section'); + section?.classList.toggle('collapsed'); + } + }); + } + /** * Show interaction detail view (for ask_user) @@ -1349,6 +1646,7 @@ import type { const index = parseInt((item as HTMLElement).getAttribute('data-index') || '0', 10); selectAutocompleteItem(index); }); + item.addEventListener('mouseenter', () => { const index = parseInt((item as HTMLElement).getAttribute('data-index') || '0', 10); selectedAutocompleteIndex = index; @@ -1729,6 +2027,9 @@ import type { cancelBtn?.addEventListener('click', handleCancel); backBtn?.addEventListener('click', handleBack); + // Initialize settings sections collapse/expand (event delegation) + initSettingsSections(); + // Attach button click handler - opens file picker attachBtn?.addEventListener('click', () => { if (currentRequestId) { @@ -1901,6 +2202,9 @@ import type { switchTab(message.tab); } + break; + case 'showSettings': + renderSettings(message.settings || [], message.addons || []); break; case 'clear': showHome(); hideAutocomplete(); @@ -1924,4 +2228,4 @@ declare function acquireVsCodeApi(): { postMessage(message: unknown): void; getState(): unknown; setState(state: unknown): void; -}; \ No newline at end of file +}; diff --git a/src/webview/types.ts b/src/webview/types.ts index 8e5ed6e..5f7f3ba 100644 --- a/src/webview/types.ts +++ b/src/webview/types.ts @@ -126,7 +126,13 @@ export type ToWebviewMessage = | { | { type: 'switchTab'; - tab: 'pending' | 'history' + tab: 'pending' | 'history' | 'settings' + } + + | { + type: 'showSettings'; + settings: SettingsSectionData[]; + addons: AddonInfoData[]; } | { @@ -221,6 +227,20 @@ export type FromWebviewMessage = | { type: 'deleteInteraction'; interactionId: string } + + | { + type: 'getSettings' + } + + | { + type: 'updateSetting'; + key: string; + value: unknown + } + + | { + type: 'openVSCodeSettings' + } ; @@ -271,3 +291,34 @@ export interface UserResponseResult { response: string; attachments: AttachmentInfo[]; } + +// Settings data types for webview +export interface SettingItemData { + key: string; + label: string; + description?: string; + type: 'boolean' | 'string' | 'number' | 'select' | 'multiselect' | 'text'; + value: unknown; + defaultValue?: unknown; + options?: Array<{ value: string; label: string }>; +} + +export interface SettingsSectionData { + id: string; + title: string; + description?: string; + settings: SettingItemData[]; + priority?: number; +} + +export interface AddonInfoData { + id: string; + name: string; + version: string; + description?: string; + author?: string; + repositoryUrl?: string; + isActive: boolean; + toolCount: number; + tabCount: number; +} diff --git a/src/webview/webviewProvider.ts b/src/webview/webviewProvider.ts index f0bd5b5..aa6cdb2 100644 --- a/src/webview/webviewProvider.ts +++ b/src/webview/webviewProvider.ts @@ -20,6 +20,7 @@ import { FileSearchResult, UserResponseResult, } from "./types"; +import { IExtensionCore } from '../core/types'; export class AgentInteractionProvider implements vscode.WebviewViewProvider { @@ -45,7 +46,8 @@ export class AgentInteractionProvider implements vscode.WebviewViewProvider { // Chat history storage for plan reviews private _chatHistoryStorage: ChatHistoryStorage; - constructor(private readonly _context: vscode.ExtensionContext) { + + constructor(private core: IExtensionCore) { // Use the singleton instance that was initialized in extension.ts this._chatHistoryStorage = getChatHistoryStorage(); //this.loadSessionsFromDisk() @@ -59,7 +61,7 @@ export class AgentInteractionProvider implements vscode.WebviewViewProvider { } private get _extensionUri(): vscode.Uri { - return this._context.extensionUri; + return this.core.getContext().extensionUri; } /** @@ -389,6 +391,12 @@ export class AgentInteractionProvider implements vscode.WebviewViewProvider { break; case 'deleteInteraction': this._handleDeleteInteraction(message.interactionId); break; + case 'getSettings': this._handleGetSettings(); + break; + case 'updateSetting': this._handleUpdateSetting(message.key, message.value); + break; + case 'openVSCodeSettings': this._handleOpenVSCodeSettings(); + break; } } @@ -823,7 +831,7 @@ export class AgentInteractionProvider implements vscode.WebviewViewProvider { const ext = extMap[effectiveMimeType] || '.png'; // Use VS Code storage for temp images - const storageUri = this._context.storageUri; + const storageUri = this.core.getContext().storageUri; if (!storageUri) { throw new Error('Storage URI not available'); @@ -1001,7 +1009,7 @@ export class AgentInteractionProvider implements vscode.WebviewViewProvider { */ public cleanupAllTempFiles(): void { try { - const storageUri = this._context.storageUri; + const storageUri = this.core.getContext().storageUri; if (!storageUri) return; const tempDir = path.join(storageUri.fsPath, 'temp-images'); @@ -1164,6 +1172,101 @@ export class AgentInteractionProvider implements vscode.WebviewViewProvider { } } + /** + * Handle getting settings data for the Settings tab + */ + private _handleGetSettings(): void { + // Import types + type SettingsSectionDataType = import('./types').SettingsSectionData; + type AddonInfoDataType = import('./types').AddonInfoData; + + const settings: SettingsSectionDataType[] = []; + const addons: AddonInfoDataType[] = []; + + try { + const api = this.core.getAPI(); + const registry = api.registry; + + // Get settings sections from addons + const addonSections = api.ui.getSettingsSections(); + for (const section of addonSections) { + settings.push({ + id: section.id, + title: section.title, + description: section.description, + settings: section.settings.map(s => ({ + key: s.key, + label: s.label, + description: s.description, + type: s.type, + value: s.value, + defaultValue: s.defaultValue, + options: s.options, + })), + priority: section.priority, + }); + } + + // Get addon info + const registrations = registry.getAll(); + for (const reg of registrations) { + const addon = reg.addon; + addons.push({ + id: addon.id, + name: addon.name, + version: addon.version, + description: addon.description, + author: addon.author, + repositoryUrl: addon.repositoryUrl, + isActive: reg.isActive, + toolCount: addon.ai?.tools?.length ?? 0, + tabCount: addon.ui?.tabs?.length ?? 0, + }); + } + } catch (err) { + console.error('[Seamless Agent] Error getting addon settings:', err); + } + + // Send settings to webview + const message: ToWebviewMessage = { + type: 'showSettings', + settings, + addons, + }; + this._view?.webview.postMessage(message); + } + + /** + * Handle updating a setting value + */ + private async _handleUpdateSetting(key: string, value: unknown): Promise { + try { + // Check if it's a VS Code setting (seamless-agent.*) + if (key.startsWith('seamless-agent.')) { + const settingKey = key.replace('seamless-agent.', ''); + const config = vscode.workspace.getConfiguration('seamless-agent'); + await config.update(settingKey, value, vscode.ConfigurationTarget.Global); + } else { + // It's an addon setting - store in extension storage + const api = this.core.getAPI(); + await api.storage.set(key, value); + } + + // Refresh settings view + this._handleGetSettings(); + } catch (err) { + console.error('[Seamless Agent] Error updating setting:', err); + vscode.window.showErrorMessage(`Failed to update setting: ${err instanceof Error ? err.message : 'Unknown error'}`); + } + } + + /** + * Handle opening VS Code settings filtered to Seamless Agent + */ + private _handleOpenVSCodeSettings(): void { + vscode.commands.executeCommand('workbench.action.openSettings', `@ext:${this.core.getContext().extension.id}`); + } + private _getHtmlContent(webview: vscode.Webview): string { // Get URIs for resources const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'media', 'main.css')); @@ -1235,6 +1338,15 @@ export class AgentInteractionProvider implements vscode.WebviewViewProvider { '{{historyFilterAll}}': strings.historyFilterAll, '{{historyFilterAskUser}}': strings.historyFilterAskUser, '{{historyFilterPlanReview}}': strings.historyFilterPlanReview, + // Settings strings + '{{settings}}': strings.settings, + '{{settingsDescription}}': strings.settingsDescription, + '{{loadingSettings}}': strings.loadingSettings, + '{{registeredAddons}}': strings.registeredAddons, + '{{noAddonsRegistered}}': strings.noAddonsRegistered, + '{{openInVSCodeSettings}}': strings.openInVSCodeSettings, + '{{addonVersion}}': strings.addonVersion, + '{{addonAuthor}}': strings.addonAuthor, }; for (const [placeholder, value] of Object.entries(replacements)) { diff --git a/tsconfig.addon-typedefs.json b/tsconfig.addon-typedefs.json new file mode 100644 index 0000000..cc8cc50 --- /dev/null +++ b/tsconfig.addon-typedefs.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "declarationMap": false, + "stripInternal": true, + "noEmitOnError": true, + "baseUrl": ".", + "paths": { + "@vscode/codicons/dist/codiconsLibrary": [ + "typings/addon-typedefs/codiconsLibrary.d.ts" + ] + }, + "outDir": "dist-addon-api" + }, + "include": [ + "src/addon-typedefs/index.ts", + "typings/addon-typedefs/codiconsLibrary.d.ts" + ], + "exclude": ["node_modules", "dist", "dist-addon-api"] +} diff --git a/tsconfig.json b/tsconfig.json index ea7548a..e0dd618 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,8 +10,10 @@ "rootDir": "src", "lib": ["ES2022"], "skipLibCheck": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "allowJs": true, + "checkJs": false }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] -} \ No newline at end of file +} diff --git a/typings/addon-typedefs/codiconsLibrary.d.ts b/typings/addon-typedefs/codiconsLibrary.d.ts new file mode 100644 index 0000000..4ecc224 --- /dev/null +++ b/typings/addon-typedefs/codiconsLibrary.d.ts @@ -0,0 +1,13 @@ +/** + * Shim para `@vscode/codicons/dist/codiconsLibrary`. + * + * Motivo: + * - O pacote `@vscode/codicons` referencia arquivos `.js` internos (ex.: `codiconsUtil.js`) + * que nem sempre vêm acompanhados de declarações de tipo resolvíveis pelo `tsc`. + * - Para gerar um pacote *types-only* dos contratos públicos do Seamless Agent, + * basta sabermos que `codiconsLibrary` é um objeto indexável. + */ + +declare module '@vscode/codicons/dist/codiconsLibrary' { + export const codiconsLibrary: Record; +} From d50ef75d9f6c54fb1841f8d035461240186a9312 Mon Sep 17 00:00:00 2001 From: Jefferson Raylan Date: Wed, 31 Dec 2025 08:30:48 -0300 Subject: [PATCH 02/18] Removed personal todo list --- TODO | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 TODO diff --git a/TODO b/TODO deleted file mode 100644 index 03b7fca..0000000 --- a/TODO +++ /dev/null @@ -1,7 +0,0 @@ -- Adicionar pause no tool getNextTask -- Resume task -- Usar o markdown do próprio vscode -- Gravar gif -- Atualizar o README.md -- Remover botão de remover de mensagens de tasks que já enviadas ou cuja lista já foi encerrada -- Criar contextos de funcionalidades para poder ser referenciados From 41ec0e43e31360d4266cbf3739b66f0645f67af8 Mon Sep 17 00:00:00 2001 From: Jefferson Raylan Date: Wed, 31 Dec 2025 08:31:48 -0300 Subject: [PATCH 03/18] Added TODO to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2c44268..22cb113 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ node_modules **/*instructions.md dist/** **/*.vsix -.seamless-agent/ \ No newline at end of file +.seamless-agent/ +TODO \ No newline at end of file From 56fff678d1a5cb315f9847dd98222e0c602e680e Mon Sep 17 00:00:00 2001 From: Jefferson Raylan Date: Wed, 31 Dec 2025 08:57:49 -0300 Subject: [PATCH 04/18] Refactor and more Move build scripts to a separated folder Upgrade vunerable dependency Implement relevant Copilot review sugestions --- .gitignore | 1 + package.json | 14 +++++++------- .../build-addon-typedefs.js | 12 ++++++++---- build-package.js => scripts/build-package.js | 2 +- esbuild.js => scripts/esbuild.js | 0 src/agent/index.ts | 4 ++-- src/webview/webviewProvider.ts | 2 +- 7 files changed, 20 insertions(+), 15 deletions(-) rename build-addon-typedefs.js => scripts/build-addon-typedefs.js (93%) rename build-package.js => scripts/build-package.js (98%) rename esbuild.js => scripts/esbuild.js (100%) diff --git a/.gitignore b/.gitignore index 22cb113..6ecb3c3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ node_modules dist/** **/*.vsix .seamless-agent/ +dist-addon-api/** TODO \ No newline at end of file diff --git a/package.json b/package.json index 49aec86..b2c8a8f 100644 --- a/package.json +++ b/package.json @@ -288,16 +288,16 @@ }, "scripts": { "compile": "npm run compile:vscode", - "compile:vscode": "npm run check-types && node esbuild.js", - "compile:antigravity": "npm run check-types && node esbuild.js --target=antigravity", - "build:addon-typedefs": "node build-addon-typedefs.js", + "compile:vscode": "npm run check-types && node scripts/esbuild.js", + "compile:antigravity": "npm run check-types && node scripts/esbuild.js --target=antigravity", + "build:addon-typedefs": "node scripts/build-addon-typedefs.js", "watch": "npm-run-all -p watch:*", - "watch:esbuild": "node esbuild.js --watch", - "watch:antigravity": "node esbuild.js --watch --target=antigravity", + "watch:esbuild": "node scripts/esbuild.js --watch", + "watch:antigravity": "node scripts/esbuild.js --watch --target=antigravity", "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", "package": "npm run package:vsce", - "package:vsce": "node build-package.js vsce", - "package:ovsx": "node build-package.js ovsx", + "package:vsce": "node scripts/build-package.js vsce", + "package:ovsx": "node scripts/build-package.js ovsx", "check-types": "tsc --noEmit", "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/build-addon-typedefs.js b/scripts/build-addon-typedefs.js similarity index 93% rename from build-addon-typedefs.js rename to scripts/build-addon-typedefs.js index b41af5e..8b8eac2 100644 --- a/build-addon-typedefs.js +++ b/scripts/build-addon-typedefs.js @@ -72,7 +72,7 @@ function getLocalTscBin(repoRoot) { } function main() { - const repoRoot = __dirname; + const repoRoot = path.resolve(__dirname, '..'); const args = parseArgs(process.argv.slice(2)); const outDir = path.resolve(repoRoot, args.get('--outDir') || 'dist-addon-api'); @@ -81,6 +81,10 @@ function main() { const rootPkg = readJson(path.join(repoRoot, 'package.json')); const version = String(args.get('--version') || rootPkg.version); + const vscodeVersion = rootPkg.devDependencies['@types/vscode'] || '^1.81.0'; + const codiconsVersion = rootPkg.devDependencies['@vscode/codicons'] || '^0.0.44'; + + console.log('[addon-typedefs] output:', outDir); console.log('[addon-typedefs] package name:', pkgName); console.log('[addon-typedefs] version:', version); @@ -138,13 +142,13 @@ function main() { }, // Dependências apenas de tipagem/compilação do consumidor peerDependencies: { - '@types/vscode': '^1.104.0', - '@vscode/codicons': '^0.0.44' + '@types/vscode': vscodeVersion, + '@vscode/codicons': codiconsVersion, }, files: [ '**/*.d.ts', 'README.md', - 'LICENSE.md' + 'LICENSE.md', ] }; diff --git a/build-package.js b/scripts/build-package.js similarity index 98% rename from build-package.js rename to scripts/build-package.js index 5f35c3d..4abb1bb 100644 --- a/build-package.js +++ b/scripts/build-package.js @@ -74,7 +74,7 @@ try { // If 'ovsx' CLI is strictly required for packaging, we would use it, but usually vsce produces the standard VSIX. // The user command name 'package:ovsx' implies targeting the OVSX registry/ecosystem. // We'll add a flag to the output filename to distinguish them. - const version = require('./package.json').version; + const version = require('../package.json').version; const outFile = target === 'ovsx' ? `seamless-agent-${version}-antigravity.vsix` : `seamless-agent-${version}.vsix`; execSync(`npx vsce package --out ${outFile}`, { stdio: 'inherit' }); diff --git a/esbuild.js b/scripts/esbuild.js similarity index 100% rename from esbuild.js rename to scripts/esbuild.js diff --git a/src/agent/index.ts b/src/agent/index.ts index 5382796..7de6c97 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -7,7 +7,7 @@ export class OrchestrationAgent { public dispose() { - + // Intentionally left empty: OrchestrationAgent currently has no resources to dispose. } public handler: vscode.ChatRequestHandler = async ( @@ -67,7 +67,7 @@ export class OrchestrationAgent { } } catch (err) { if (err instanceof vscode.LanguageModelError) { - stream.markdown(`Error: ${err.message}`); + stream.markdown(`Error: ${String(err)}`); } else { throw err; } diff --git a/src/webview/webviewProvider.ts b/src/webview/webviewProvider.ts index aa6cdb2..157125d 100644 --- a/src/webview/webviewProvider.ts +++ b/src/webview/webviewProvider.ts @@ -1256,7 +1256,7 @@ export class AgentInteractionProvider implements vscode.WebviewViewProvider { this._handleGetSettings(); } catch (err) { console.error('[Seamless Agent] Error updating setting:', err); - vscode.window.showErrorMessage(`Failed to update setting: ${err instanceof Error ? err.message : 'Unknown error'}`); + vscode.window.showErrorMessage(`Failed to update setting: ${err instanceof Error ? String(err) : 'Unknown error'}`); } } From adde7115414d6a7ed2e2bcf3fbb613c7589706f7 Mon Sep 17 00:00:00 2001 From: Jefferson Raylan Date: Wed, 31 Dec 2025 08:59:27 -0300 Subject: [PATCH 05/18] Update src/extension.antigravity.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/extension.antigravity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.antigravity.ts b/src/extension.antigravity.ts index 68074d0..6faa672 100644 --- a/src/extension.antigravity.ts +++ b/src/extension.antigravity.ts @@ -5,7 +5,7 @@ import { ExtensionCore } from './core'; export function activate(context: vscode.ExtensionContext) { console.log('Seamless Agent extension active'); - const core = new ExtensionCore(context,); + const core = new ExtensionCore(context); context.subscriptions.push(core); } From b04b8964b93d917d65a5ab8648d3af7cd71d854f Mon Sep 17 00:00:00 2001 From: Jefferson Raylan Date: Wed, 31 Dec 2025 09:18:06 -0300 Subject: [PATCH 06/18] Implement relevant Copilot review sugestions and more Change storage namespace sepparator from `.` to `::` Make call to `this.core.getContext()` null safe Format code on `main.ts` file --- src/api/SeamlessAgentAPI.ts | 10 +- src/webview/main.ts | 297 +++++++++++++++++++-------------- src/webview/webviewProvider.ts | 2 +- 3 files changed, 174 insertions(+), 135 deletions(-) diff --git a/src/api/SeamlessAgentAPI.ts b/src/api/SeamlessAgentAPI.ts index 55a27f9..83ab513 100644 --- a/src/api/SeamlessAgentAPI.ts +++ b/src/api/SeamlessAgentAPI.ts @@ -272,7 +272,7 @@ class StorageIntegrationImpl implements IStorageIntegration { private readonly globalState: vscode.Memento; constructor(context: vscode.ExtensionContext, addonId: string) { - this.namespace = `addon.${addonId}`; + this.namespace = `addon::${addonId}`; this.globalState = context.globalState; } @@ -280,7 +280,7 @@ class StorageIntegrationImpl implements IStorageIntegration { * Get a stored value */ get(key: string, defaultValue?: T): T | undefined { - const fullKey = `${this.namespace}.${key}`; + const fullKey = `${this.namespace}::${key}`; return this.globalState.get(fullKey, defaultValue as T); } @@ -288,7 +288,7 @@ class StorageIntegrationImpl implements IStorageIntegration { * Set a stored value */ async set(key: string, value: T): Promise { - const fullKey = `${this.namespace}.${key}`; + const fullKey = `${this.namespace}::${key}`; await this.globalState.update(fullKey, value); } @@ -296,7 +296,7 @@ class StorageIntegrationImpl implements IStorageIntegration { * Delete a stored value */ async delete(key: string): Promise { - const fullKey = `${this.namespace}.${key}`; + const fullKey = `${this.namespace}::${key}`; await this.globalState.update(fullKey, undefined); } @@ -305,7 +305,7 @@ class StorageIntegrationImpl implements IStorageIntegration { */ keys(): string[] { const allKeys = this.globalState.keys(); - const prefix = `${this.namespace}.`; + const prefix = `${this.namespace}::`; return allKeys .filter(key => key.startsWith(prefix)) .map(key => key.substring(prefix.length)); diff --git a/src/webview/main.ts b/src/webview/main.ts index 20df667..9aea3dc 100644 --- a/src/webview/main.ts +++ b/src/webview/main.ts @@ -128,7 +128,6 @@ import type { RequestItem, FileSearchResult, ToolCallInteraction, - RequiredPlanRevisions, StoredInteraction } from './types'; @@ -259,8 +258,8 @@ import type { } /** - * Apply filter to history items - */ + * Apply filter to history items + */ function applyHistoryFilter(filter: string): void { currentHistoryFilter = filter; @@ -293,8 +292,8 @@ import type { } /** - * Initialize history filter buttons - */ + * Initialize history filter buttons + */ function initHistoryFilters(): void { document.querySelectorAll('.filter-btn').forEach(btn => { btn.addEventListener('click', () => { @@ -305,9 +304,9 @@ import type { } /** - * Bind a single delegated handler for history list interactions. - * This avoids losing per-item handlers when the list is re-rendered via innerHTML. - */ + * Bind a single delegated handler for history list interactions. + * This avoids losing per-item handlers when the list is re-rendered via innerHTML. + */ function initHistoryListDelegation(): void { if (!historyList) return; @@ -363,9 +362,9 @@ import type { } /** - * Announce a message to screen readers via the live region - * @param message The message to announce - */ + * Announce a message to screen readers via the live region + * @param message The message to announce + */ function announceToScreenReader(message: string): void { if (srAnnounce) { // Clear and set text to trigger announcement @@ -374,9 +373,7 @@ import type { // Use setTimeout to ensure the DOM change is detected setTimeout(() => { srAnnounce.textContent = message; - } - - , 50); + }, 50); } } @@ -412,10 +409,34 @@ import type { } } + /** + * Helper function to create TextNode + * + * @param {string} text + * @return {*} {Text} + */ function tn(text: string): Text { return document.createTextNode(text); } + /** + * Helper function to create HTMLElement + * + * @template K + * @param {K} tag + * @param {{ + * className?: string; + * text?: string; + * html?: string; + * title?: string; + * attrs?: Record; + * on?: Partial<{ + * [K in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[K]) => any; + * }> + * }} [options] + * @param {...ElementChild[]} children + * @return {*} {HTMLElementTagNameMap[K]} + */ function el( tag: K, options?: { @@ -452,13 +473,19 @@ import type { return node; } + /** + * Helper function to create icon + * + * @param {string} name + * @return {*} {HTMLSpanElement} + */ function codicon(name: string): HTMLSpanElement { return el('span', { className: `codicon codicon-${name}` }); } /** - * Show the list of pending requests - */ + * Show the list of pending requests + */ function showList(requests: RequestItem[]): void { if (requests.length === 0) { @@ -515,8 +542,8 @@ import type { } /** -* Show the question form and hide other views -*/ + * Show the question form and hide other views + */ function showQuestion(question: string, title: string, requestId: string): void { currentRequestId = requestId; @@ -552,12 +579,14 @@ import type { } /** - * Switch between tabs in the home view - */ + * Switch between tabs in the home view + */ function switchTab(tab: 'pending' | 'history' | 'settings'): void { + if (typeof tab !== 'string') return; + // Get settings content element const contentSettings = document.getElementById('content-settings'); - + // Update content panes visibility contentPending?.classList.toggle('hidden', tab !== 'pending'); contentHistory?.classList.toggle('hidden', tab !== 'history'); @@ -575,17 +604,15 @@ import type { pending: window.__STRINGS__?.pendingItems || 'Pending Items', history: window.__STRINGS__?.chatHistory || 'Chat History', settings: window.__STRINGS__?.settings || 'Settings', - } - - ; + }; announceToScreenReader(`${tabNames[tab]}tab selected`); } /** - * Update the unified pending placeholder visibility - * Shows placeholder only when both requests and reviews are empty - */ + * Update the unified pending placeholder visibility + * Shows placeholder only when both requests and reviews are empty + */ function updatePendingPlaceholder(): void { const hasRequests = ! !(pendingRequestsList && pendingRequestsList.children.length > 0); const hasReviews = ! !(pendingReviewsList && pendingReviewsList.children.length > 0); @@ -596,8 +623,8 @@ import type { } /** - * Show home view (pending requests + recent interactions) - */ + * Show home view (pending requests + recent interactions) + */ function showHome(): void { currentRequestId = null; currentInteractionId = null; @@ -630,9 +657,9 @@ import type { } /** - * Extract a meaningful title from the LLM's input question - * Uses the first sentence (up to ~80 chars) as the title - */ + * Extract a meaningful title from the LLM's input question + * Uses the first sentence (up to ~80 chars) as the title + */ function extractTitleFromQuestion(question: string): string { if (!question) return 'Tool Call'; @@ -980,7 +1007,7 @@ import type { function renderSettingsSection(section: SettingsSectionData): HTMLElement { const sectionEl = el('div', { className: 'settings-section' }); - const header = el('div', { + const header = el('div', { className: 'settings-section-header', attrs: { 'data-section': section.id } }); @@ -990,9 +1017,9 @@ import type { const content = el('div', { className: 'settings-section-content' }); if (section.description) { - content.appendChild(el('p', { - className: 'settings-description', - text: section.description + content.appendChild(el('p', { + className: 'settings-description', + text: section.description })); } @@ -1018,13 +1045,13 @@ import type { checkbox.id = `setting-${setting.key}`; checkbox.checked = Boolean(setting.value); checkbox.addEventListener('change', () => { - vscode.postMessage({ - type: 'updateSetting', - key: setting.key, - value: checkbox.checked + vscode.postMessage({ + type: 'updateSetting', + key: setting.key, + value: checkbox.checked }); }); - const checkboxLabel = el('label', { + const checkboxLabel = el('label', { className: 'setting-label', text: setting.label, attrs: { for: `setting-${setting.key}` } @@ -1034,9 +1061,9 @@ import type { break; case 'select': - item.appendChild(el('label', { + item.appendChild(el('label', { className: 'setting-label', - text: setting.label + text: setting.label })); const select = document.createElement('select'); select.className = 'setting-select'; @@ -1051,10 +1078,10 @@ import type { select.appendChild(option); } select.addEventListener('change', () => { - vscode.postMessage({ - type: 'updateSetting', - key: setting.key, - value: select.value + vscode.postMessage({ + type: 'updateSetting', + key: setting.key, + value: select.value }); }); item.appendChild(select); @@ -1062,9 +1089,9 @@ import type { case 'string': case 'number': - item.appendChild(el('label', { + item.appendChild(el('label', { className: 'setting-label', - text: setting.label + text: setting.label })); const input = document.createElement('input'); input.type = setting.type === 'number' ? 'number' : 'text'; @@ -1072,22 +1099,22 @@ import type { input.id = `setting-${setting.key}`; input.value = String(setting.value ?? ''); input.addEventListener('change', () => { - const value = setting.type === 'number' - ? parseFloat(input.value) + const value = setting.type === 'number' + ? parseFloat(input.value) : input.value; - vscode.postMessage({ - type: 'updateSetting', - key: setting.key, - value + vscode.postMessage({ + type: 'updateSetting', + key: setting.key, + value }); }); item.appendChild(input); break; case 'text': - item.appendChild(el('label', { + item.appendChild(el('label', { className: 'setting-label', - text: setting.label + text: setting.label })); const textarea = document.createElement('textarea'); textarea.className = 'setting-input'; @@ -1095,10 +1122,10 @@ import type { textarea.rows = 3; textarea.value = String(setting.value ?? ''); textarea.addEventListener('change', () => { - vscode.postMessage({ - type: 'updateSetting', - key: setting.key, - value: textarea.value + vscode.postMessage({ + type: 'updateSetting', + key: setting.key, + value: textarea.value }); }); item.appendChild(textarea); @@ -1106,9 +1133,9 @@ import type { } if (setting.description) { - item.appendChild(el('p', { - className: 'setting-description', - text: setting.description + item.appendChild(el('p', { + className: 'setting-description', + text: setting.description })); } @@ -1120,23 +1147,24 @@ import type { */ function renderAddonCard(addon: AddonInfoData): HTMLElement { const card = el('div', { className: 'addon-card' }); - const header = el('div', { className: 'addon-card-header' }); + header.appendChild(el('span', { className: 'addon-card-name', text: addon.name })); - header.appendChild(el('span', { - className: 'addon-card-version', - text: `v${addon.version}` + header.appendChild(el('span', { + className: 'addon-card-version', + text: `v${addon.version}` })); - header.appendChild(el('span', { + header.appendChild(el('span', { className: `addon-status ${addon.isActive ? 'active' : 'inactive'}`, text: addon.isActive ? 'Active' : 'Inactive' })); + card.appendChild(header); if (addon.description) { - card.appendChild(el('p', { - className: 'addon-card-description', - text: addon.description + card.appendChild(el('p', { + className: 'addon-card-description', + text: addon.description })); } @@ -1169,7 +1197,9 @@ import type { */ function initSettingsSections(): void { const settingsContainer = document.getElementById('content-settings'); - if (!settingsContainer) {return;} + if (!settingsContainer) { + return; + } // Use event delegation to handle clicks on section headers // This way we don't need to re-attach listeners when content is re-rendered @@ -1294,15 +1324,15 @@ import type { } /** - * Update attachments display - renders chips above textarea - */ + * Update attachments display - renders chips above textarea + */ function updateAttachmentsDisplay(): void { updateChipsDisplay(); } /** - * Update chips display above textarea - */ + * Update chips display above textarea + */ function updateChipsDisplay(): void { if (!chipsContainer) return; @@ -1314,7 +1344,6 @@ import type { else { chipsContainer.classList.remove('hidden'); - const preview = document.querySelector('.image-hover-preview') as HTMLElement; const previewImg = preview?.querySelector('img') as HTMLImageElement; @@ -1397,8 +1426,8 @@ import type { } /** - * Remove an attachment by ID - */ + * Remove an attachment by ID + */ function removeAttachment(attachmentId: string): void { if (currentRequestId) { vscode.postMessage({ @@ -1414,8 +1443,8 @@ import type { } /** - * Handle submit button click - */ + * Handle submit button click + */ function handleSubmit(): void { const response = responseInput?.value.trim() || ''; @@ -1433,8 +1462,8 @@ import type { } /** - * Handle cancel button click - */ + * Handle cancel button click + */ function handleCancel(): void { if (currentRequestId) { vscode.postMessage({ @@ -1493,8 +1522,8 @@ import type { } /** - * Get Codicon icon name for a file based on its extension - */ + * Get Codicon icon name for a file based on its extension + */ function getFileIcon(filename: string): string { const ext = filename.split('.').pop()?.toLowerCase() || ''; @@ -2082,19 +2111,22 @@ import type { // Autocomplete navigation if (autocompleteVisible) { switch (event.key) { - case 'ArrowDown': event.preventDefault(); + case 'ArrowDown': + event.preventDefault(); if (selectedAutocompleteIndex < autocompleteResults.length - 1) { selectedAutocompleteIndex++; updateAutocompleteSelection(); } return; - case 'ArrowUp': event.preventDefault(); + case 'ArrowUp': + event.preventDefault(); if (selectedAutocompleteIndex > 0) { selectedAutocompleteIndex--; updateAutocompleteSelection(); } return; - case 'Enter': case 'Tab': + case 'Enter': + case 'Tab': if (selectedAutocompleteIndex >= 0) { event.preventDefault(); selectAutocompleteItem(selectedAutocompleteIndex); @@ -2128,11 +2160,14 @@ import type { const message = event.data; switch (message.type) { - case 'showQuestion': showQuestion(message.question, message.title, message.requestId); + case 'showQuestion': + showQuestion(message.question, message.title, message.requestId); break; - case 'showList': showList(message.requests); + case 'showList': + showList(message.requests); break; - case 'showHome': recentInteractions = message.recentInteractions || []; + case 'showHome': + recentInteractions = message.recentInteractions || []; showHome(); // Update pending requests if provided @@ -2155,55 +2190,59 @@ import type { } break; - case 'showInteractionDetail': showInteractionDetail(message.interaction); + case 'showInteractionDetail': + showInteractionDetail(message.interaction); break; - case 'updateAttachments': if (message.requestId === currentRequestId) { - - // Preserve flags from existing attachments when updating - const existingFlags = new Map(currentAttachments.map(a => [a.id, { - isImage: a.isImage, isTextReference: a.isTextReference - }])); - - currentAttachments = (message.attachments || []).map((att: AttachmentInfo) => { - const existing = existingFlags.get(att.id); - return { - ...att, - isImage: att.isImage || existing?.isImage || /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(att.name), - isTextReference: att.isTextReference ?? existing?.isTextReference ?? false, - }; - }); - updateAttachmentsDisplay(); - } + case 'updateAttachments': + if (message.requestId === currentRequestId) { + + // Preserve flags from existing attachments when updating + const existingFlags = new Map(currentAttachments.map(a => [a.id, { + isImage: a.isImage, isTextReference: a.isTextReference + }])); + + currentAttachments = (message.attachments || []).map((att: AttachmentInfo) => { + const existing = existingFlags.get(att.id); + return { + ...att, + isImage: att.isImage || existing?.isImage || /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(att.name), + isTextReference: att.isTextReference ?? existing?.isTextReference ?? false, + }; + }); + updateAttachmentsDisplay(); + } break; - case 'fileSearchResults': if (autocompleteQuery !== undefined) { - showAutocomplete(message.files || []); - } + case 'fileSearchResults': + if (autocompleteQuery !== undefined) { + showAutocomplete(message.files || []); + } break; - case 'imageSaved': if (message.requestId === currentRequestId && message.attachment) { - // Add to local attachments if not already there - const exists = currentAttachments.some(a => a.id === message.attachment.id); - - if (!exists) { - currentAttachments.push({ - ...message.attachment, - isImage: true, - }); - updateChipsDisplay(); + case 'imageSaved': + if (message.requestId === currentRequestId && message.attachment) { + // Add to local attachments if not already there + const exists = currentAttachments.some(a => a.id === message.attachment.id); + + if (!exists) { + currentAttachments.push({ + ...message.attachment, + isImage: true, + }); + updateChipsDisplay(); + } } - } break; - case 'switchTab': if (message.tab) { - switchTab(message.tab); - } - + case 'switchTab': + if (message.tab) { + switchTab(message.tab); + } break; - case 'showSettings': + case 'showSettings': renderSettings(message.settings || [], message.addons || []); break; case 'clear': showHome(); diff --git a/src/webview/webviewProvider.ts b/src/webview/webviewProvider.ts index 157125d..89bb208 100644 --- a/src/webview/webviewProvider.ts +++ b/src/webview/webviewProvider.ts @@ -1264,7 +1264,7 @@ export class AgentInteractionProvider implements vscode.WebviewViewProvider { * Handle opening VS Code settings filtered to Seamless Agent */ private _handleOpenVSCodeSettings(): void { - vscode.commands.executeCommand('workbench.action.openSettings', `@ext:${this.core.getContext().extension.id}`); + vscode.commands.executeCommand('workbench.action.openSettings', `@ext:${this.core.getContext()?.extension?.id}`); } private _getHtmlContent(webview: vscode.Webview): string { From 5ff7f4f358dbe33c14ebef74c2bc6be6fd0f7b91 Mon Sep 17 00:00:00 2001 From: Jefferson Raylan Date: Wed, 31 Dec 2025 13:40:06 -0300 Subject: [PATCH 07/18] fix workflow and typedefs documentation --- .../workflows/manual-publish-antigravity.yml | 2 +- .github/workflows/manual-publish-typedefs.yml | 42 +++++++++++++++++++ .github/workflows/manual-publish-vscode.yml | 2 +- scripts/build-addon-typedefs.js | 33 +++++++++------ 4 files changed, 64 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/manual-publish-typedefs.yml diff --git a/.github/workflows/manual-publish-antigravity.yml b/.github/workflows/manual-publish-antigravity.yml index a7b87e0..7f236fa 100644 --- a/.github/workflows/manual-publish-antigravity.yml +++ b/.github/workflows/manual-publish-antigravity.yml @@ -14,7 +14,7 @@ permissions: id-token: write jobs: - publish: + publish-antigravity: runs-on: ubuntu-latest permissions: id-token: write diff --git a/.github/workflows/manual-publish-typedefs.yml b/.github/workflows/manual-publish-typedefs.yml new file mode 100644 index 0000000..c1b855f --- /dev/null +++ b/.github/workflows/manual-publish-typedefs.yml @@ -0,0 +1,42 @@ +name: Manual Publish Typedefs + +on: + workflow_dispatch: + inputs: + pre-release: + description: "Pre release" + required: false + type: boolean + default: false + +permissions: + contents: write + id-token: write + +jobs: + publish-typedefs: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + attestations: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install dependencies + run: npm ci + + - name: Build addon typedefs package + run: npm run build:addon-typedefs + + - name: Publish addon typedefs to npm + working-directory: ./dist-addon-api + run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/manual-publish-vscode.yml b/.github/workflows/manual-publish-vscode.yml index 466cbb1..a8d0820 100644 --- a/.github/workflows/manual-publish-vscode.yml +++ b/.github/workflows/manual-publish-vscode.yml @@ -14,7 +14,7 @@ permissions: id-token: write jobs: - publish: + publish-vscode: runs-on: ubuntu-latest permissions: id-token: write diff --git a/scripts/build-addon-typedefs.js b/scripts/build-addon-typedefs.js index 8b8eac2..f1c70ab 100644 --- a/scripts/build-addon-typedefs.js +++ b/scripts/build-addon-typedefs.js @@ -1,14 +1,14 @@ /* eslint-disable no-console */ /** - * Gera um pacote "types-only" para autores de addons. + * Generates a types-only package for addon authors. * - * Saída padrão: ./dist-addon-api + * Default output: ./dist-addon-api * - * O pacote gerado contém: + * The generated package contains: * - .d.ts (emitDeclarationOnly) - * - package.json minimal - * - README.md (composto a partir de src/api/README.md e src/addons/README.md) + * - minimal package.json + * - README.md (composed from src/api/README.md and src/addons/README.md) * - LICENSE */ @@ -100,19 +100,26 @@ function main() { stdio: 'inherit', }); - // Criar index.d.ts na raiz do pacote para facilitar imports - // (reexporta o entrypoint gerado em dist-addon-api/addon-typedefs/index.d.ts) - const indexDts = `/**\n * Seamless Agent Addon API (types-only)\n *\n * Use apenas em contexto de tipos: \`import type { ... }\`.\n */\n\nexport * from './addon-typedefs';\n`; + // Create index.d.ts at the package root to simplify imports + // (re-exports the entrypoint generated in dist-addon-api/addon-typedefs/index.d.ts) + const indexDts = [ + "/**", + "* Seamless Agent Addon API (types-only)", + "*", + "* Use only in type-only contexts: `import type { ... }`.", + "*/", + "export * from './addon-typedefs';", + ].join('\n'); writeFile(path.join(outDir, 'index.d.ts'), indexDts); - // README: compor a partir dos READMEs dos módulos + // README: compose from the modules' README files const apiReadme = safeReadText(path.join(repoRoot, 'src', 'api', 'README.md')); const addonsReadme = safeReadText(path.join(repoRoot, 'src', 'addons', 'README.md')); const readmeParts = [ '# Seamless Agent — Addon API (Types Only)\n', - 'Este pacote contém **apenas definições de tipos** (TypeScript) para autores de addons integrarem com a extensão **Seamless Agent**.\n', - '\n> Dica: use sempre `import type { ... }` para garantir que nada seja importado em runtime.\n', + 'This package contains **type definitions only** (TypeScript) for addon authors to integrate with the **Seamless Agent** extension.\n', + '\n> Tip: always use `import type { ... }` to ensure nothing is imported at runtime.\n', apiReadme ? `\n---\n\n## API\n\n${apiReadme.trim()}\n` : '', addonsReadme ? `\n---\n\n## Addons\n\n${addonsReadme.trim()}\n` : '', ].filter(Boolean); @@ -125,7 +132,7 @@ function main() { copyFile(licenseSrc, path.join(outDir, 'LICENSE.md')); } - // package.json do pacote types-only + // package.json for the types-only package const typesPkg = { name: pkgName, version, @@ -140,7 +147,7 @@ function main() { types: './index.d.ts' } }, - // Dependências apenas de tipagem/compilação do consumidor + // Dependencies used only for consumer typing/compilation peerDependencies: { '@types/vscode': vscodeVersion, '@vscode/codicons': codiconsVersion, From ea6765655662279e0795ccf006454d69a910ed54 Mon Sep 17 00:00:00 2001 From: Jefferson Raylan Date: Wed, 31 Dec 2025 14:00:49 -0300 Subject: [PATCH 08/18] Update addon typedef package name --- scripts/build-addon-typedefs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-addon-typedefs.js b/scripts/build-addon-typedefs.js index f1c70ab..d5736bb 100644 --- a/scripts/build-addon-typedefs.js +++ b/scripts/build-addon-typedefs.js @@ -76,7 +76,7 @@ function main() { const args = parseArgs(process.argv.slice(2)); const outDir = path.resolve(repoRoot, args.get('--outDir') || 'dist-addon-api'); - const pkgName = String(args.get('--name') || 'seamless-agent-addon-api'); + const pkgName = String(args.get('--name') || '@seamless-agent/addon'); const rootPkg = readJson(path.join(repoRoot, 'package.json')); const version = String(args.get('--version') || rootPkg.version); From f2fb8ac185f238a372578d421b431e2c8a32013f Mon Sep 17 00:00:00 2001 From: Jefferson Raylan Date: Wed, 31 Dec 2025 15:27:31 -0300 Subject: [PATCH 09/18] Fix addon tab not showing --- media/main.css | 24 +++++- media/webview.html | 5 ++ package.json | 2 +- src/core/index.ts | 8 ++ src/webview/main.ts | 137 +++++++++++++++++++++++++++++++-- src/webview/types.ts | 34 +++++++- src/webview/webviewProvider.ts | 87 +++++++++++++++++++++ 7 files changed, 288 insertions(+), 9 deletions(-) diff --git a/media/main.css b/media/main.css index 9acd587..4ccd640 100644 --- a/media/main.css +++ b/media/main.css @@ -917,6 +917,28 @@ button:disabled { white-space: nowrap; } +/* Custom tabs container for addons */ +.custom-tabs-container { + display: contents; +} + +/* Custom tab content pane */ +.custom-tab-pane { + display: none; + padding: 8px 0; +} + +.custom-tab-pane.active { + display: block; +} + +.custom-tab-pane .custom-tab-loading { + color: var(--vscode-descriptionForeground); + font-style: italic; + padding: 16px; + text-align: center; +} + #home-view .section { flex-shrink: 0; } @@ -1789,7 +1811,7 @@ button:disabled { .addon-card-version { font-size: 11px; - color: var(--vscode-descriptionForeground); + color: var(--vscode-badge-foreground); background-color: var(--vscode-badge-background); padding: 1px 6px; border-radius: 10px; diff --git a/media/webview.html b/media/webview.html index ad596e8..0f7de0a 100644 --- a/media/webview.html +++ b/media/webview.html @@ -65,6 +65,8 @@

data-badge-for="history" > + +