diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..02364ee --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Something does not work as expected +title: "fix: " +labels: bug +--- + +**Describe the bug** + + + +**How to reproduce** + +1. +2. +3. + +**Environment** + +- Editor (VS Code or VSCodium) and version: +- Operating system: +- Provider used (claudeCli, codexCli, openrouter, ...): +- Extension version: + +**Logs** + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..70cf1a3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature request +about: An idea to improve the extension +title: "feat: " +labels: enhancement +--- + +**What problem it solves** + + + +**Proposed solution** + + + +**Alternatives considered** + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..0ea03f5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ +## Summary + + + +## Checklist + +- [ ] `pnpm typecheck`, `pnpm lint`, `pnpm test` and `pnpm build` pass locally +- [ ] Commits follow Conventional Commits (in English, atomic) +- [ ] New behavior has a test in `tests/` +- [ ] `README.md` and `CHANGELOG.md` updated, if the change is user-visible +- [ ] No key, secret or sensitive diff included diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8d21892 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm typecheck + - run: pnpm lint + - run: pnpm test + - run: pnpm build diff --git a/.gitignore b/.gitignore index a0500c6..c7c5ca4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ dist/ *.vsix .vscode-test/ coverage/ +.env* +!.env.example diff --git a/.vscodeignore b/.vscodeignore index a52c6db..8b246ba 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -8,9 +8,16 @@ node_modules/** **/*.ts **/*.mts .gitignore +.env* +.github/** +CONTRIBUTING.md +CODE_OF_CONDUCT.md +SECURITY.md +CHANGELOG.md +docs/** biome.json tsconfig.json -vitest.config.ts +vitest.config.mts esbuild.config.mjs pnpm-workspace.yaml pnpm-lock.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index fffb869..142ed1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,71 +1,71 @@ # Changelog -Todas as mudanças notáveis deste projeto são documentadas aqui, seguindo o -espírito do [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/) e -versionamento semântico. +All notable changes to this project are documented here, following the +spirit of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and +semantic versioning. ## [Unreleased] -### Adicionado +### Added -- `generateCommit.disableThinking`: desliga o raciocínio nos providers CLI - para menor latência (Codex com `model_reasoning_effort="none"`, Claude - CLI com `MAX_THINKING_TOKENS=0`). O dropdown de Effort fica desabilitado - no painel enquanto o toggle está ligado. -- Campo "Custom prompt instructions" agora é uma textarea multilinha - redimensionável verticalmente. -- Sugestões de modelo ao vivo no campo Model do painel: a lista completa é - buscada no endpoint de modelos de cada provider (cache de 1 hora) e - modelos novos aparecem automaticamente no dropdown, com a opção "Custom…" - para digitar um ID fora da lista. -- Codex CLI: catálogo vivo de modelos via `codex debug models` e dropdown - de Effort com os níveis de reasoning do modelo selecionado (cache de 1 - hora; fallback estático quando o CLI não responde). -- Claude CLI: dropdown de Model com os aliases `fable`, `opus`, `sonnet` e - `haiku`, e dropdown de Effort com os 5 níveis do `--effort`. +- `generateCommit.disableThinking`: disables reasoning on CLI providers for + lower latency (Codex with `model_reasoning_effort="none"`, Claude CLI + with `MAX_THINKING_TOKENS=0`). The Effort dropdown is disabled in the + panel while the toggle is on. +- The "Custom prompt instructions" field is now a vertically resizable + multiline textarea. +- Live model suggestions in the panel's Model field: the full list is + fetched from each provider's models endpoint (1-hour cache) and new + models appear automatically in the dropdown, with a "Custom…" option to + type an ID outside the list. +- Codex CLI: live model catalog via `codex debug models` and an Effort + dropdown with the reasoning levels of the selected model (1-hour cache; + static fallback when the CLI does not respond). +- Claude CLI: Model dropdown with the `fable`, `opus`, `sonnet` and + `haiku` aliases, and an Effort dropdown with the 5 `--effort` levels. -### Alterado +### Changed -- Painel de configurações: os campos do provider ativo (Model, Effort, - Base URL, Auth header) ficam na seção Provider; a seção "Advanced per - provider" foi removida. -- Campo de idioma virou dropdown com lista de idiomas + opção "Custom…" - (antes: texto livre com datalist). -- Selects do painel com seta SVG própria, border-radius de 5px e padding - ajustado. +- Settings panel: the active provider's fields (Model, Effort, Base URL, + Auth header) live in the Provider section; the "Advanced per provider" + section was removed. +- The language field became a dropdown with a language list plus a + "Custom…" option (previously: free text with a datalist). +- Panel selects with their own SVG arrow, 5px border-radius and adjusted + padding. ## [0.1.0] - 2026-08-02 -Primeira versão funcional. +First functional version. -### Adicionado +### Added -- Geração de mensagem de commit (Conventional Commits) a partir do diff - staged, escrita na caixa de commit do Source Control para revisão, sem - nunca commitar automaticamente. -- Botão com ícone de estrelinhas na caixa de commit (menu `scm/inputBox`, - API proposta `contribSourceControlInputBoxMenu`, habilitável via - `--enable-proposed-api=everton.generate-commit`), com fallback pela - Command Palette. -- Providers plugáveis: OpenRouter, Anthropic-compatible (presets Kimi, - GLM, MiniMax e endpoint custom com auth `x-api-key` ou `bearer`), Claude - Code CLI e Codex CLI (ambos sem chave, usando o login do próprio CLI). -- Painel Generate Commit na Activity Bar: formulário inline na sidebar com - todas as configurações (provider, modelo, chaves de API com status, - idioma, limites, comportamento, timeout e seções avançadas por provider), - validado e sem janelas flutuantes. -- Segurança: varredura local de segredos com bloqueio por padrão (opt-in - por ocorrência), exclusão de lockfiles/binários/minificados/arquivos - grandes, truncamento sinalizado no prompt, chaves somente em - SecretStorage, somente HTTPS, zero telemetria, log apenas de metadados - com redação de segredos, cancelamento que mata o grupo de processos do - CLI e guarda contra gerações concorrentes e diff obsoleto. -- Multi-repo com QuickPick de escolha quando ambíguo; suporte a diff - unstaged configurável (`ask`/`always`/`never`). -- Erros de provider distintos e acionáveis (autenticação, créditos, rate - limit, servidor, rede, timeout). -- Documentação: README, `docs/ARCHITECTURE.md`, `SECURITY.md`, tabela de - verificações datadas (endpoints, model IDs e flags verificados em fontes - oficiais em 2026-08-02). +- Commit message generation (Conventional Commits) from the staged diff, + written into the Source Control commit box for review, never committing + automatically. +- Button with a sparkles icon in the commit box (`scm/inputBox` menu, + proposed API `contribSourceControlInputBoxMenu`, enabled via + `--enable-proposed-api=everton.generate-commit`), with a Command Palette + fallback. +- Pluggable providers: OpenRouter, Anthropic-compatible (Kimi, GLM, MiniMax + presets and a custom endpoint with `x-api-key` or `bearer` auth), Claude + Code CLI and Codex CLI (both keyless, using the CLI's own login). +- Generate Commit panel on the Activity Bar: inline form in the sidebar + with every setting (provider, model, API keys with status, language, + limits, behavior, timeout and advanced per-provider sections), validated + and with no floating windows. +- Security: local secret scanning with blocking by default (per-occurrence + opt-in), exclusion of lockfiles/binaries/minified/large files, truncation + flagged in the prompt, keys only in SecretStorage, HTTPS only, zero + telemetry, metadata-only logging with secret redaction, cancellation that + kills the CLI process group, and guards against concurrent generations + and stale diffs. +- Multi-repo with a selection QuickPick when ambiguous; configurable + unstaged diff support (`ask`/`always`/`never`). +- Distinct, actionable provider errors (authentication, credits, rate + limit, server, network, timeout). +- Documentation: README, `docs/ARCHITECTURE.md`, `SECURITY.md`, dated + verification table (endpoints, model IDs and flags verified against + official sources on 2026-08-02). [0.1.0]: https://github.com/everton-dgn/generate-commit-extension/releases/tag/v0.1.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d7ed912 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,83 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement via [GitHub Security Advisories](https://github.com/everton-dgn/generate-commit-extension/security/advisories/new), this repository's private channel for any sensitive report, whether related to conduct or to security. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cbc379f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,76 @@ +# Contributing + +Every contribution is welcome: fixes, features, documentation and bug +reports. This guide covers the full flow, from environment setup to the PR. + +## Setting up the environment + +Requirements: Node.js LTS and pnpm 11. The pnpm version is pinned in the +`packageManager` field of `package.json`; with Corepack enabled, it is used +automatically: + +```bash +corepack enable +pnpm install +``` + +## Developing + +```bash +pnpm watch # bundle into dist/ with watch +# F5 in VS Code: opens the Extension Development Host +``` + +The extension's architecture (providers, generation flow, settings panel) +is documented in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). A new +provider is one implementation of the `Provider` interface in +`src/providers/`, registered in `src/providers/registry.ts`. + +## Before opening the PR + +Run locally the same steps the CI runs: + +```bash +pnpm typecheck # tsc --noEmit +pnpm lint # biome check . +pnpm test # vitest run +pnpm build # esbuild +``` + +New behavior calls for new tests in `tests/` (Vitest). If the change +affects settings, providers or user-visible flows, update `README.md` and +`CHANGELOG.md` in the same PR. + +## Commits + +We follow [Conventional Commits](https://www.conventionalcommits.org/en/), +in English and with a small scope (atomic commits): + +```text +feat: add kimi coding endpoint preset +fix: abort cli process group on cancel +docs: document machine-scoped settings +``` + +Common types: `feat`, `fix`, `docs`, `chore`, `ci`, `refactor`, `test`. + +## Opening the PR + +1. Fork the repository and create a branch from `main` (`feat/...`, + `fix/...`, `docs/...`). +2. Open the PR against `main`, filling in the template. +3. CI must be green before merging. + +## Reporting bugs and vulnerabilities + +Bugs: open an issue using the bug template, with reproduction steps and +environment. Never paste API keys, diffs or provider responses into the +issue. + +Security vulnerabilities: do **not** open a public issue. Follow the +process described in [SECURITY.md](SECURITY.md). + +## Code of conduct + +By participating, you agree to the +[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). diff --git a/README.md b/README.md index c7ff8c3..a3534e8 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,61 @@ # Generate Commit with AI -Extensão para VS Code e VSCodium que gera mensagens de commit no formato -[Conventional Commits](https://www.conventionalcommits.org/pt-br/) a partir do -diff staged, usando o provedor de IA da sua escolha. A mensagem é escrita na -caixa de commit do painel Source Control para você revisar e editar. A -extensão **nunca commita por você**. +VS Code and VSCodium extension that generates +[Conventional Commits](https://www.conventionalcommits.org/en/) messages from +your staged diff, using the AI provider of your choice. The message is written +into the Source Control commit box for you to review and edit. The extension +**never commits for you**. -## O botão na caixa de commit (leia antes) +## The commit box button (read first) -O ponto de contribuição `scm/inputBox` (o mesmo que o Copilot usa para o -botão de sparkle dentro da caixa de commit) é uma **API proposta**, não -estável, verificado em 2026-08-02 no código-fonte do VS Code 1.131.0 -(`menusExtensionPoint.ts`, proposta `contribSourceControlInputBoxMenu`, em -vigor desde o VS Code 1.85). Extensões instaladas via VSIX só o usam quando o -editor é aberto com o flag da proposta: +The `scm/inputBox` contribution point (the same one Copilot uses for the +sparkle button inside the commit box) is a **proposed API**, not a stable one, +verified on 2026-08-02 against the VS Code 1.131.0 source code +(`menusExtensionPoint.ts`, proposal `contribSourceControlInputBoxMenu`, in +effect since VS Code 1.85). Extensions installed via VSIX can only use it when +the editor is launched with the proposal flag: ```bash codium --enable-proposed-api=everton.generate-commit ``` -Sem esse flag, o menu `scm/inputBox` é rejeitado silenciosamente e a extensão -segue funcionando pela Command Palette (`Generate Commit: Generate Commit -Message`) e pelo painel Generate Commit na barra lateral esquerda. - -Em modo de desenvolvimento (F5), a proposta funciona sem flags. - -## Funcionalidades - -- Gera a mensagem a partir do diff staged; se não houver nada staged, oferece - usar o diff unstaged (comportamento configurável). -- Multi-repo: usa o repositório do botão clicado; com um único repo, usa - direto; ambíguo, abre QuickPick. -- Cancelamento: o botão de cancelar na notificação de progresso aborta a - chamada HTTP ou mata o processo do CLI. -- Troca rápida de provider/modelo pelo comando `Generate Commit: Switch - Provider / Model`. -- Painel **Generate Commit** na barra lateral esquerda (ícone de - estrelinhas): todas as configurações em um formulário inline na própria - sidebar — provider, modelo, esforço (CLIs), base URL e estilo de auth do - provider ativo, chaves com status, idioma, limites, prompt customizado, - fallback e timeout — sem nenhuma janela flutuante e sem editar JSON. -- **Sugestões de modelo ao vivo**: o campo Model mostra a lista completa - atual do provider (buscada no endpoint de modelos dele a cada hora, com - fallback silencioso para texto livre). Modelo novo lançado pelo provider - aparece sozinho na lista; a opção "Custom…" permite digitar um ID fora - dela. No Codex CLI a lista vem de `codex debug models`, e o campo Effort - mostra só os níveis de reasoning que o modelo selecionado suporta. -- Primeiro uso guiado: sem nenhum provider disponível, o fluxo abre a - configuração (escolha do provider, chave mascarada, validação rápida, - salvamento no Secret Storage). -- Arquitetura plugável: um provider = uma implementação da interface - `Provider` em `src/providers/`. - -## Instalação - -Baixe o `.vsix` e instale por um dos caminhos: - -1. Menu `Extensions` → `...` → `Install from VSIX...`; +Without that flag, the `scm/inputBox` menu is silently rejected and the +extension keeps working through the Command Palette (`Generate Commit: +Generate Commit Message`) and the Generate Commit panel in the left sidebar. + +In development mode (F5), the proposal works without flags. + +## Features + +- Generates the message from the staged diff; if nothing is staged, offers to + use the unstaged diff (configurable behavior). +- Multi-repo: uses the repository of the clicked button; with a single repo, + uses it directly; when ambiguous, opens a QuickPick. +- Cancellation: the cancel button on the progress notification aborts the + HTTP call or kills the CLI process. +- Quick provider/model switching via the `Generate Commit: Switch Provider / + Model` command. +- **Generate Commit** panel in the left sidebar (sparkles icon): every + setting in an inline form right in the sidebar — provider, model, effort + (CLIs), base URL and auth style of the active provider, API keys with + status, language, limits, custom prompt, fallback and timeout — with no + floating windows and no JSON editing. +- **Live model suggestions**: the Model field shows the provider's full + current model list (fetched from its models endpoint every hour, with a + silent fallback to free text). A newly released model shows up on its own; + the "Custom…" option lets you type an ID outside the list. For the Codex + CLI the list comes from `codex debug models`, and the Effort field shows + only the reasoning levels the selected model supports. +- Guided first run: with no provider available, the flow opens the settings + (provider choice, masked key, quick validation, saved to Secret Storage). +- Pluggable architecture: one provider = one implementation of the + `Provider` interface in `src/providers/`. + +## Installation + +Download the `.vsix` and install it through one of these paths: + +1. `Extensions` menu → `...` → `Install from VSIX...`; 2. Terminal: ```bash @@ -65,166 +64,169 @@ codium --install-extension generate-commit-0.1.0.vsix ## Providers -| Provider | Tipo | Endpoint / binário default | Modelo default | Onde obter a chave | -|-----------------|------|---------------------------------------------------|----------------------------|--------------------| -| Claude Code CLI | CLI | binário `claude` no PATH | default do CLI | não precisa (usa seu login do Claude Code) | -| Codex CLI | CLI | binário `codex` no PATH | default do CLI | não precisa (usa seu login do Codex) | -| OpenRouter | HTTP | `https://openrouter.ai/api/v1` | `google/gemini-2.5-flash-lite` | [openrouter.ai/keys](https://openrouter.ai/keys) | -| Kimi (Moonshot) | HTTP | `https://api.moonshot.ai/anthropic` | `kimi-k2.6` | [platform.kimi.ai/console/api-keys](https://platform.kimi.ai/console/api-keys) | -| GLM (z.ai) | HTTP | `https://api.z.ai/api/anthropic` | `glm-4.5-air` | [z.ai/manage-apikey](https://z.ai/manage-apikey/apikey-list) | -| MiniMax | HTTP | `https://api.minimax.io/anthropic` | `MiniMax-M2.5-highspeed` | [platform.minimax.io](https://platform.minimax.io/user-center/basic-information/interface-key) | -| Anthropic (custom) | HTTP | `https://api.anthropic.com` (editável) | `claude-haiku-4-5-20251001` | [console.anthropic.com](https://console.anthropic.com/settings/keys) | - -Notas: - -- Com o `claude` instalado e logado, a extensão funciona **sem configurar - nada** (é o provider default). -- Providers de CLI indisponíveis aparecem na seção "Unavailable" do QuickPick - com a indicação "CLI not found". -- A detecção de CLI procura no PATH, depois no shell de login (resolve o PATH - mínimo de apps gráficos no macOS) e por fim em diretórios comuns - (`/opt/homebrew/bin`, `/usr/local/bin`, `~/.local/bin`, entre outros). -- Kimi: evite `kimi-k2.7-code`, que rejeita requisições sem thinking; o - default `kimi-k2.6` aceita requisições simples (verificado em 2026-08-02). -- China: Kimi tem host `https://api.moonshot.cn/anthropic` e MiniMax - `https://api.minimaxi.com/anthropic` (editáveis nas settings). -- O provider "Anthropic (custom)" cobre qualquer endpoint compatível com a - Messages API: edite base URL, modelo e o estilo de autenticação - (`x-api-key` ou `bearer`). -- As sugestões do campo Model vêm do endpoint de modelos de cada provider - (`/models`, verificado em 2026-08-02: OpenRouter sem chave; Kimi, GLM, - MiniMax e Anthropic com a chave configurada), com cache de 1 hora. Se a - busca falhar (sem chave, rede, endpoint custom sem listagem), o campo - segue texto livre sem erro. Claude CLI sugere os aliases estáticos - (`fable`, `opus`, `sonnet`, `haiku`, verificados com `claude --help` - 2.1.220); Codex CLI lê o catálogo vivo de `codex debug models` - (codex-cli 0.146.0), que também informa os níveis de esforço de cada - modelo — o campo Effort do painel mostra apenas os níveis válidos para o - modelo selecionado. -- Kimi for Coding: quem tem chave do serviço `api.kimi.com/coding` (em vez - da plataforma Moonshot) pode apontar `generateCommit.kimi.baseUrl` para - `https://api.kimi.com/coding` — mensagens (`/v1/messages`) e catálogo - (`/v1/models`) funcionam nesse host (verificado em 2026-08-02). - -### Configurar uma chave de API - -No painel **Generate Commit** da barra lateral, seção **API keys**: cole a -chave do provider (campo de senha, nunca pré-preenchido), clique em Save e -aguarde a validação rápida. O status fica visível ao lado do provider -(configured/not set). A chave fica em `context.secrets` (Secret Storage do -VS Code), **nunca** em `settings.json` e nunca renderizada no painel. +| Provider | Type | Default endpoint / binary | Default model | Where to get the key | +|-----------------|------|--------------------------------------------------|---------------------------|----------------------| +| Claude Code CLI | CLI | `claude` binary on PATH | CLI default | not needed (uses your Claude Code login) | +| Codex CLI | CLI | `codex` binary on PATH | CLI default | not needed (uses your Codex login) | +| OpenRouter | HTTP | `https://openrouter.ai/api/v1` | `google/gemini-2.5-flash-lite` | [openrouter.ai/keys](https://openrouter.ai/keys) | +| Kimi (Moonshot) | HTTP | `https://api.moonshot.ai/anthropic` | `kimi-k2.6` | [platform.kimi.ai/console/api-keys](https://platform.kimi.ai/console/api-keys) | +| GLM (z.ai) | HTTP | `https://api.z.ai/api/anthropic` | `glm-4.5-air` | [z.ai/manage-apikey](https://z.ai/manage-apikey/apikey-list) | +| MiniMax | HTTP | `https://api.minimax.io/anthropic` | `MiniMax-M2.5-highspeed` | [platform.minimax.io](https://platform.minimax.io/user-center/basic-information/interface-key) | +| Anthropic (custom) | HTTP | `https://api.anthropic.com` (editable) | `claude-haiku-4-5-20251001` | [console.anthropic.com](https://console.anthropic.com/settings/keys) | + +Notes: + +- With `claude` installed and logged in, the extension works **with zero + configuration** (it is the default provider). +- Unavailable CLI providers show up in the "Unavailable" section of the + QuickPick with a "CLI not found" hint. +- CLI detection looks on PATH, then in the login shell (works around the + minimal PATH of graphical apps on macOS), and finally in common + directories (`/opt/homebrew/bin`, `/usr/local/bin`, `~/.local/bin`, among + others). +- Kimi: avoid `kimi-k2.7-code`, which rejects requests without thinking; + the `kimi-k2.6` default accepts simple requests (verified on 2026-08-02). +- China: Kimi has the `https://api.moonshot.cn/anthropic` host and MiniMax + has `https://api.minimaxi.com/anthropic` (editable in settings). +- The "Anthropic (custom)" provider covers any Messages API-compatible + endpoint: edit the base URL, the model and the authentication style + (`x-api-key` or `bearer`). +- The Model field suggestions come from each provider's models endpoint + (`/models`, verified on 2026-08-02: OpenRouter without a key; Kimi, GLM, + MiniMax and Anthropic with the configured key), cached for 1 hour. If the + fetch fails (no key, network, custom endpoint without listing), the field + stays free text with no error. Claude CLI suggests the static aliases + (`fable`, `opus`, `sonnet`, `haiku`, verified with `claude --help` + 2.1.220); Codex CLI reads the live catalog from `codex debug models` + (codex-cli 0.146.0), which also reports each model's effort levels — the + panel's Effort field shows only the levels valid for the selected model. +- Kimi for Coding: anyone with a key for the `api.kimi.com/coding` service + (instead of the Moonshot platform) can point `generateCommit.kimi.baseUrl` + to `https://api.kimi.com/coding` — messages (`/v1/messages`) and catalog + (`/v1/models`) work on that host (verified on 2026-08-02). + +### Configuring an API key + +In the **Generate Commit** panel in the sidebar, **API keys** section: paste +the provider key (password field, never pre-filled), click Save and wait for +the quick validation. The status stays visible next to the provider +(configured/not set). The key lives in `context.secrets` (VS Code Secret +Storage), **never** in `settings.json` and never rendered in the panel. ## Settings -Todas as opções abaixo também podem ser alteradas pela interface, no painel -**Generate Commit** da barra lateral (formulário inline com validação; o -comando `Generate Commit: Settings` abre o painel), além da tela de -Settings do editor (filtre por `generateCommit`). - -| Setting | Default | Descrição | -|---------|---------|-----------| -| `generateCommit.provider` | `claudeCli` | Provider ativo (`claudeCli`, `codexCli`, `openrouter`, `kimi`, `glm`, `minimax`, `anthropicCustom`). Indisponível cai para o primeiro disponível. | -| `generateCommit.language` | `en` | Idioma da mensagem (`en`, `pt-BR`, `es`, ...). | -| `generateCommit.maxDiffChars` | `50000` | Teto de caracteres do diff enviado; trunca na fronteira de arquivo e sinaliza no prompt. | -| `generateCommit.maxFileSizeKB` | `50` | Arquivos cujo chunk de diff excede este tamanho são excluídos. | -| `generateCommit.includeRecentCommits` | `true` | Envia os 10 commits recentes como referência de estilo. | -| `generateCommit.disableThinking` | `false` | Desliga o raciocínio nos providers CLI para menor latência: Codex roda com `model_reasoning_effort="none"` e o Claude CLI com `MAX_THINKING_TOKENS=0` (verificado em 2026-08-02). A setting Effort é ignorada enquanto ligado. | -| `generateCommit.customPrompt` | `""` | Instruções extras anexadas ao system prompt. | -| `generateCommit.unstagedFallback` | `ask` | Sem diff staged: perguntar, sempre usar unstaged ou nunca. | -| `generateCommit.timeoutSeconds` | `60` | Timeout de requests HTTP e execuções de CLI. | -| `generateCommit..model` | ver tabela | Modelo por provider. | -| `generateCommit..baseUrl` | ver tabela | Base URL por provider HTTP (somente HTTPS). | -| `generateCommit.anthropicCustom.authHeader` | `x-api-key` | Estilo de auth do endpoint custom (`x-api-key` ou `bearer`). | -| `generateCommit.claudeCli.effort` | `low` | `--effort` do Claude Code (`low`, `medium`, `high`, `xhigh`, `max`; vazio = default do CLI). | -| `generateCommit.codexCli.effort` | `low` | `model_reasoning_effort` do Codex. Os valores aceitos dependem do modelo — o painel lista os níveis do modelo selecionado, lidos ao vivo de `codex debug models` (em 2026-08-02: `low` a `xhigh` em todos; `max` e `ultra` na família gpt-5.6). Vazio = default do CLI. | - -## Segurança - -- **Varredura de segredos antes de enviar**: chaves (`sk-`, `sk-or-`, - `sk-ant-`, `ghp_`, `github_pat_`, `AKIA`/`ASIA`, `xox...`, `AIza...`), - JWT, blocos PEM de chave privada, credenciais em URLs, arquivos `.env*` e - keystores (`.jks`, `.keystore`, `.p12`, `.pfx`). Ao detectar, a extensão - **bloqueia** e lista arquivo + tipo de segredo (nunca o valor); enviar é - sempre uma decisão explícita sua, por ocorrência. -- **Exclusões de diff**: lockfiles (`pnpm-lock.yaml`, `package-lock.json`, - `yarn.lock`, `bun.lock`, `bun.lockb`, `deno.lock`), binários, minificados - (`*.min.js/css`) e arquivos acima do limite de tamanho. -- **Truncamento** no teto configurável, com aviso dentro do prompt. -- **Somente HTTPS**; timeout configurável (default 60 s). -- **Log só com metadados** (provider, modelo, latência, tamanho do diff) no - Output Channel "Generate Commit". Diff, chaves e respostas nunca são - logados; stderr de CLIs **nunca** é exibido ou logado cru (é classificado - por uma lista fechada de assinaturas, ex.: "not logged in"), e os detalhes - de erro HTTP passam pela mesma redação de segredos do scanner antes de - chegar à UI. -- **Chaves fora do alcance de workspaces**: `*.baseUrl` e - `anthropicCustom.authHeader` têm escopo `machine`, então um - `.vscode/settings.json` de repositório não pode redirecionar suas chaves - para um host de terceiros. -- **Cancelamento robusto**: o botão de cancelar aborta a chamada HTTP ou mata - o **grupo de processos** do CLI (SIGTERM, depois SIGKILL), incluindo - subprocessos filhos. -- **Concorrência e consistência**: uma geração por repositório (iniciar outra - aborta a anterior e só a mais recente escreve na caixa); o diff staged é - relido antes de preencher a caixa e o resultado é descartado se o stage - mudou durante a geração. -- **Zero telemetria** e zero chamadas a serviços além do provider escolhido. -- **Webview do painel travada**: o formulário de configurações roda numa - WebviewView com CSP `default-src 'none'` (só arquivos locais do bundle, - sem conteúdo remoto, sem script/estilo inline), valores renderizados via - DOM seguro (sem `innerHTML` com dados do usuário), mensagens validadas - contra uma whitelist de chaves antes de qualquer `config.update` e - segredos nunca renderizados no DOM (campos de senha só escrevem). -- Sem a API `vscode.lm` (que depende do Copilot e não existe no VSCodium). - -## Desenvolvimento +Every option below can also be changed through the interface, in the +**Generate Commit** panel in the left sidebar (inline form with validation; +the `Generate Commit: Settings` command opens the panel), as well as in the +editor's Settings screen (filter by `generateCommit`). + +| Setting | Default | Description | +|---------|---------|-------------| +| `generateCommit.provider` | `claudeCli` | Active provider (`claudeCli`, `codexCli`, `openrouter`, `kimi`, `glm`, `minimax`, `anthropicCustom`). An unavailable one falls back to the first available. | +| `generateCommit.language` | `en` | Language of the generated message (`en`, `pt-BR`, `es`, ...). | +| `generateCommit.maxDiffChars` | `50000` | Character cap of the diff sent; truncates at file boundaries and flags it in the prompt. | +| `generateCommit.maxFileSizeKB` | `50` | Files whose diff chunk exceeds this size are excluded. | +| `generateCommit.includeRecentCommits` | `true` | Sends the 10 recent commits as style reference. | +| `generateCommit.disableThinking` | `false` | Disables reasoning on CLI providers for lower latency: Codex runs with `model_reasoning_effort="none"` and the Claude CLI with `MAX_THINKING_TOKENS=0` (verified on 2026-08-02). The Effort setting is ignored while this is on. | +| `generateCommit.customPrompt` | `""` | Extra instructions appended to the system prompt. | +| `generateCommit.unstagedFallback` | `ask` | With no staged diff: ask, always use unstaged, or never. | +| `generateCommit.timeoutSeconds` | `60` | Timeout for HTTP requests and CLI executions. | +| `generateCommit..model` | see table | Model per provider. | +| `generateCommit..baseUrl` | see table | Base URL per HTTP provider (HTTPS only). | +| `generateCommit.anthropicCustom.authHeader` | `x-api-key` | Auth style of the custom endpoint (`x-api-key` or `bearer`). | +| `generateCommit.claudeCli.effort` | `low` | Claude Code `--effort` (`low`, `medium`, `high`, `xhigh`, `max`; empty = CLI default). | +| `generateCommit.codexCli.effort` | `low` | Codex `model_reasoning_effort`. Accepted values depend on the model — the panel lists the selected model's levels, read live from `codex debug models` (on 2026-08-02: `low` through `xhigh` on all; `max` and `ultra` on the gpt-5.6 family). Empty = CLI default. | + +## Security + +- **Secret scanning before sending**: keys (`sk-`, `sk-or-`, `sk-ant-`, + `ghp_`, `github_pat_`, `AKIA`/`ASIA`, `xox...`, `AIza...`), JWTs, private + key PEM blocks, credentials in URLs, `.env*` files and keystores (`.jks`, + `.keystore`, `.p12`, `.pfx`). On detection, the extension **blocks** and + lists file + secret type (never the value); sending is always an explicit + decision of yours, per occurrence. +- **Diff exclusions**: lockfiles (`pnpm-lock.yaml`, `package-lock.json`, + `yarn.lock`, `bun.lock`, `bun.lockb`, `deno.lock`), binaries, minified + files (`*.min.js/css`) and files above the size limit. +- **Truncation** at the configurable cap, with a notice inside the prompt. +- **HTTPS only**; configurable timeout (default 60 s). +- **Metadata-only logging** (provider, model, latency, diff size) to the + "Generate Commit" Output Channel. Diffs, keys and responses are never + logged; CLI stderr is **never** displayed or logged raw (it is classified + against a closed list of signatures, e.g. "not logged in"), and HTTP error + details go through the same secret redaction as the scanner before + reaching the UI. +- **Keys out of workspace reach**: `*.baseUrl` and + `anthropicCustom.authHeader` are `machine`-scoped, so a repository's + `.vscode/settings.json` cannot redirect your keys to a third-party host. +- **Robust cancellation**: the cancel button aborts the HTTP call or kills + the CLI **process group** (SIGTERM, then SIGKILL), including child + subprocesses. +- **Concurrency and consistency**: one generation per repository (starting + another aborts the previous one and only the latest writes to the box); + the staged diff is re-read before filling the box and the result is + discarded if the stage changed during generation. +- **Zero telemetry** and zero calls to services other than the chosen + provider. +- **Locked-down panel webview**: the settings form runs in a WebviewView + with CSP `default-src 'none'` (only local bundle files, no remote content, + no inline script/style), values rendered via safe DOM (no `innerHTML` with + user data), messages validated against a key whitelist before any + `config.update`, and secrets never rendered in the DOM (password fields + are write-only). +- No `vscode.lm` API (which depends on Copilot and does not exist on + VSCodium). + +## Development ```bash pnpm install -pnpm watch # bundle em dist/ com watch -# F5: abre o Extension Development Host (proposta scm/inputBox habilitada) +pnpm watch # bundle into dist/ with watch +# F5: opens the Extension Development Host (scm/inputBox proposal enabled) ``` Scripts: `pnpm test` (Vitest), `pnpm typecheck` (tsc), `pnpm lint` (Biome), -`pnpm build` (esbuild), `pnpm package` (gera o `.vsix` com `@vscode/vsce`). +`pnpm build` (esbuild), `pnpm package` (builds the `.vsix` with +`@vscode/vsce`). -## Verificações de fonte oficial (2026-08-02) +## Official source verifications (2026-08-02) -| Item | Valor verificado | Fonte | -|------|------------------|-------| -| `scm/inputBox` é API proposta | `contribSourceControlInputBoxMenu`, desde o VS Code 1.85, ainda proposta no 1.131 | [menusExtensionPoint.ts](https://raw.githubusercontent.com/microsoft/vscode/1.131.0/src/vs/workbench/services/actions/common/menusExtensionPoint.ts) | -| Argumentos do comando no `scm/inputBox` | `(rootUri, context, token)`, não `SourceControl` | [scmInput.ts](https://raw.githubusercontent.com/microsoft/vscode/main/src/vs/workbench/contrib/scm/browser/scmInput.ts) | +| Item | Verified value | Source | +|------|----------------|--------| +| `scm/inputBox` is a proposed API | `contribSourceControlInputBoxMenu`, since VS Code 1.85, still proposed in 1.131 | [menusExtensionPoint.ts](https://raw.githubusercontent.com/microsoft/vscode/1.131.0/src/vs/workbench/services/actions/common/menusExtensionPoint.ts) | +| Command arguments in `scm/inputBox` | `(rootUri, context, token)`, not `SourceControl` | [scmInput.ts](https://raw.githubusercontent.com/microsoft/vscode/main/src/vs/workbench/contrib/scm/browser/scmInput.ts) | | Git API v1 | `getAPI(1)`, `Repository.diff(cached?)`, `inputBox.value`, `log(options?)` | [git.d.ts](https://raw.githubusercontent.com/microsoft/vscode/1.126.0/extensions/git/src/api/git.d.ts) | -| VSCodium atual | 1.126.04524 (VS Code 1.126.0) | [releases](https://github.com/VSCodium/vscodium/releases/latest) | +| Current VSCodium | 1.126.04524 (VS Code 1.126.0) | [releases](https://github.com/VSCodium/vscodium/releases/latest) | | OpenRouter endpoint | `POST https://openrouter.ai/api/v1/chat/completions`, `Authorization: Bearer` | [docs/quickstart](https://openrouter.ai/docs/quickstart) | -| Modelos OpenRouter baratos | `google/gemini-2.5-flash-lite`, `openai/gpt-5-nano`, `meta-llama/llama-3.2-3b-instruct`, `mistralai/mistral-small-24b-instruct-2501`, `openai/gpt-4o-mini` | [api/v1/models](https://openrouter.ai/api/v1/models) | -| Anthropic Messages API | `POST /v1/messages`; headers `x-api-key` + `anthropic-version: 2023-06-01`; texto em `content[i].text` | [platform.claude.com/docs](https://platform.claude.com/docs/en/api/messages) | +| Cheap OpenRouter models | `google/gemini-2.5-flash-lite`, `openai/gpt-5-nano`, `meta-llama/llama-3.2-3b-instruct`, `mistralai/mistral-small-24b-instruct-2501`, `openai/gpt-4o-mini` | [api/v1/models](https://openrouter.ai/api/v1/models) | +| Anthropic Messages API | `POST /v1/messages`; headers `x-api-key` + `anthropic-version: 2023-06-01`; text in `content[i].text` | [platform.claude.com/docs](https://platform.claude.com/docs/en/api/messages) | | Kimi endpoint | `https://api.moonshot.ai/anthropic` (China: `api.moonshot.cn/anthropic`) | [platform.kimi.ai/docs](https://platform.kimi.ai/docs/guide/claude-code-kimi) | -| Kimi modelos | `kimi-k3`, `kimi-k2.7-code` (exige thinking), `kimi-k2.6`, entre outros | [platform.kimi.ai/docs/models](https://platform.kimi.ai/docs/models) | +| Kimi models | `kimi-k3`, `kimi-k2.7-code` (requires thinking), `kimi-k2.6`, among others | [platform.kimi.ai/docs/models](https://platform.kimi.ai/docs/models) | | GLM endpoint | `https://api.z.ai/api/anthropic` | [docs.z.ai](https://docs.z.ai/devpack/tool/claude) | -| GLM modelos | `glm-5.2`, `glm-4.7`, `glm-4.5-air` | [docs.z.ai](https://docs.z.ai/devpack/tool/claude) | -| MiniMax endpoint | `POST https://api.minimax.io/anthropic/v1/messages`; auth Bearer **ou** `x-api-key`; `anthropic-version` não exigido | [platform.minimax.io/docs](https://platform.minimax.io/docs) | -| MiniMax modelos | `MiniMax-M3`, `MiniMax-M2.7`, `MiniMax-M2.5`, `MiniMax-M2.1`, `MiniMax-M2` (+ variantes `-highspeed`) | [platform.minimax.io/docs](https://platform.minimax.io/docs) | -| Claude Code flags | `-p`, `--tools`, `--model`, `--effort (low..max)`, `--output-format`, `--no-session-persistence`; **não existe** flag `--fast` na 2.1.220 | `claude --help` local (2.1.220) | -| Codex flags | `codex exec`, `--model`, `--sandbox read-only`, `--skip-git-repo-check`, `--ephemeral`, `--output-last-message`, `--config k=v` | `codex --help` / `codex exec --help` locais (0.146.0) | -| Codex approval/effort | `approval_policy="never"` e `model_reasoning_effort` via `-c`; o flag `--ask-for-approval` apareceu e desapareceu do `--help` na mesma versão (superfície dinâmica), então a política vai pela chave de config | [config-reference](https://developers.openai.com/codex/config-reference) + verificação local em 2026-08-02 | - -Ressalvas registradas pela pesquisa: Kimi e GLM não documentam o path literal -(`/v1/messages`) nem os headers HTTP crus fora do contexto do Claude Code -(que autentica via `ANTHROPIC_AUTH_TOKEN`, enviado como `Authorization: -Bearer`); por isso os presets deles usam Bearer. MiniMax é o único com o path -confirmado literalmente na especificação OpenAPI oficial. - -## Limitações conhecidas - -- Providers de CLI foram desenhados para macOS/Linux (detecção via - `command -v` e shell de login); no Windows, use um provider HTTP. -- A varredura de segredos é heurística: falsos positivos são possíveis (a - decisão final de envio é sempre sua) e nenhuma lista de padrões cobre - todos os formatos de chave existentes. -- Repositórios sem nenhum commit (HEAD unborn) não têm diff staged legível - pela API do Git; faça o commit inicial antes. - -## Licença - -MIT. Veja o arquivo `LICENSE` na raiz do repositório. +| GLM models | `glm-5.2`, `glm-4.7`, `glm-4.5-air` | [docs.z.ai](https://docs.z.ai/devpack/tool/claude) | +| MiniMax endpoint | `POST https://api.minimax.io/anthropic/v1/messages`; Bearer **or** `x-api-key` auth; `anthropic-version` not required | [platform.minimax.io/docs](https://platform.minimax.io/docs) | +| MiniMax models | `MiniMax-M3`, `MiniMax-M2.7`, `MiniMax-M2.5`, `MiniMax-M2.1`, `MiniMax-M2` (+ `-highspeed` variants) | [platform.minimax.io/docs](https://platform.minimax.io/docs) | +| Claude Code flags | `-p`, `--tools`, `--model`, `--effort (low..max)`, `--output-format`, `--no-session-persistence`; **no** `--fast` flag in 2.1.220 | local `claude --help` (2.1.220) | +| Codex flags | `codex exec`, `--model`, `--sandbox read-only`, `--skip-git-repo-check`, `--ephemeral`, `--output-last-message`, `--config k=v` | local `codex --help` / `codex exec --help` (0.146.0) | +| Codex approval/effort | `approval_policy="never"` and `model_reasoning_effort` via `-c`; the `--ask-for-approval` flag appeared and disappeared from `--help` within the same version (dynamic surface), so the policy goes through the config key | [config-reference](https://developers.openai.com/codex/config-reference) + local verification on 2026-08-02 | + +Caveats recorded by the research: Kimi and GLM do not document the literal +path (`/v1/messages`) nor the raw HTTP headers outside the Claude Code +context (which authenticates via `ANTHROPIC_AUTH_TOKEN`, sent as +`Authorization: Bearer`); that is why their presets use Bearer. MiniMax is +the only one with the path literally confirmed in the official OpenAPI +specification. + +## Known limitations + +- CLI providers were designed for macOS/Linux (detection via `command -v` + and login shell); on Windows, use an HTTP provider. +- Secret scanning is heuristic: false positives are possible (the final + sending decision is always yours) and no pattern list covers every key + format in existence. +- Repositories with no commits (unborn HEAD) have no staged diff readable + through the Git API; make the initial commit first. + +## License + +MIT. See the `LICENSE` file in the repository root. diff --git a/SECURITY.md b/SECURITY.md index 97a916a..559105b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,125 +1,131 @@ -# Política e modelo de segurança +# Security policy and threat model -## Reportar vulnerabilidades +## Reporting vulnerabilities -Abra uma issue privada (GitHub Security Advisories) ou contate o mantenedor -diretamente. Não abra issue pública descrevendo o problema antes de ele ser -corrigido. +Open a private report (GitHub Security Advisories) or contact the maintainer +directly. Do not open a public issue describing the problem before it is +fixed. -## Modelo de segurança +## Threat model -A extensão envia o **conteúdo do seu diff** para o provider de IA escolhido. -Esse é o único dado que sai da máquina, e estas são as camadas de proteção: +The extension sends **your diff content** to the AI provider you chose, plus +the request data that call requires: your API key (HTTP providers), the +optional recent commit subjects and your custom prompt instructions. Nothing +goes to any other service, and these are the protection layers: -### 1. Varredura de segredos antes de qualquer envio +### 1. Secret scanning before any send -- Padrões: chaves (`sk-`, `sk-or-`, `sk-ant-`, `ghp_`/`gho_`/..., - `github_pat_`, `AKIA`/`ASIA`, `xox[baprs]-`, `AIza`), JWT, blocos PEM de - chave privada, credenciais embutidas em URLs (`https://user:pass@host`), - arquivos `.env*` e keystores (`.jks`, `.keystore`, `.p12`, `.pfx`). -- Ao detectar: **bloqueio por padrão**. Um modal lista arquivo + tipo de - segredo (o valor nunca é exibido) e o envio só acontece com "Send - Anyway" explícito, por ocorrência. -- Limitação honesta: a varredura é heurística. Nenhuma lista cobre todos os - formatos de chave, e falsos positivos são possíveis. Revise o que vai no +- Patterns: keys (`sk-`, `sk-or-`, `sk-ant-`, `ghp_`/`gho_`/..., + `github_pat_`, `AKIA`/`ASIA`, `xox[baprs]-`, `AIza`), JWTs, private key + PEM blocks, credentials embedded in URLs (`https://user:pass@host`), + `.env*` files and keystores (`.jks`, `.keystore`, `.p12`, `.pfx`). +- On detection: **blocked by default**. A modal lists file + secret type + (the value is never shown) and sending only happens with an explicit + "Send Anyway", per occurrence. +- Honest limitation: scanning is heuristic. No list covers every key + format, and false positives are possible. Review what goes into the stage. -### 2. Minimização do que é enviado - -- Lockfiles, binários, minificados e arquivos cujo chunk de diff excede - `generateCommit.maxFileSizeKB` × 1024 bytes são excluídos do diff. -- O diff total é truncado em `generateCommit.maxDiffChars`, na fronteira de - arquivo, com aviso explícito no prompt. -- Commits recentes (apenas assuntos) só são incluídos se - `includeRecentCommits` estiver ligado. - -### 3. Chaves de API - -- Armazenadas **somente** em `context.secrets` (SecretStorage do VS Code, - com suporte a keychain do sistema operacional). -- Nunca em `settings.json`, nunca em logs, nunca em mensagens de erro. -- **Escopo `machine` nas URLs sensíveis**: as settings `*.baseUrl` e - `anthropicCustom.authHeader` são declaradas com escopo `machine`, então - valores vindos de `.vscode/settings.json` de um workspace são ignorados. - Sem isso, um repositório malicioso poderia apontar a base URL para um - host controlado por terceiros e receber a sua chave na próxima chamada - (geração ou catálogo de modelos). -- O catálogo de modelos (`GET /models` de cada provider) envia a chave - apenas ao endpoint do próprio provider, com o header exigido pelo - contrato dele (MiniMax lista modelos atrás de `X-Api-Key`, mesmo o - endpoint de mensagens aceitando Bearer; verificado em 2026-08-02). -- A validação de chave usa uma requisição mínima (`max_tokens: 8`). Falha - de autenticação invalida a chave; respostas do endpoint que não sejam de - autenticação (limite, créditos, erro de servidor, payload rejeitado) - confirmam que a chave foi aceita; falhas sem resposta (rede, timeout) - deixam a chave não verificada e impedem o salvamento pelo painel (com - opção explícita "Save anyway", logada como não verificada). - -### 4. Transporte e rede - -- Somente HTTPS: base URLs não-HTTPS são rejeitadas na leitura da config e - na camada HTTP (`postJson` recusa antes do fetch). -- Timeout configurável (default 60 s) em requisições e execuções de CLI. -- **Zero telemetria** e zero chamadas a serviços além do provider ativo. -- Sem a API `vscode.lm`. - -### 5. Logs e mensagens de erro - -- Output Channel somente com metadados (provider, modelo, latência, - tamanhos, contagens). Todo valor passa por redação defensiva de segredos - antes de ser escrito. -- Detalhes de erro HTTP passam pela mesma redação antes de chegar à UI. -- stderr de CLIs **nunca** é exibido ou logado cru: é classificado por uma - lista fechada de assinaturas (ex.: "not logged in", "rate limited"), - porque pode ecoar o prompt (o diff) ou segredos. -- URLs inválidas são logadas apenas pelo host (credenciais embutidas em - userinfo nunca chegam ao log). - -### 6. Execução de CLIs - -- Binários resolvidos por nome validado (`[A-Za-z0-9._-]+`) passado como - argumento posicional ao shell (sem interpolação em string de comando). -- Spawn sem shell (`args` em array), cwd no root do repositório. -- Cancelamento/timeout mata o **grupo de processos** (spawn `detached` + - sinal para o grupo), com escalonamento SIGTERM → SIGKILL. -- Providers CLI não recebem nem exigem chaves de API (usam o login do - próprio CLI). -- Variáveis de ambiente de sessões aninhadas (`CLAUDECODE`, - `CLAUDE_CODE_ENTRYPOINT`) são removidas do ambiente do `claude`. - -### 7. Consistência e abuso - -- Uma geração por repositório: iniciar outra aborta a anterior; respostas - atrasadas de runs antigas são descartadas. -- O diff staged é relido antes de escrever o resultado na caixa de commit; - se o stage mudou durante a geração, o resultado é descartado. -- Erros de API mapeados por tipo (401/403, 402, 429 com `retry-after`, - 5xx, rede, timeout) com mensagens distintas e ações sugeridas. - -### 8. Webview do painel de configurações - -A única webview da extensão é o formulário de settings na sidebar, isolada -assim: - -- **CSP estrita**: `default-src 'none'` com `script-src` e `style-src` - limitados aos arquivos locais do bundle (`media/`). Sem conteúdo remoto, - sem script ou estilo inline, sem imagens e sem fontes externas. -- **Sem XSS por construção**: o HTML estático não interpola dado nenhum; o - estado flui por `postMessage` e é renderizado só com APIs seguras de DOM - (`textContent`, `createElement`, propriedades `value`), nunca `innerHTML` - com dados do usuário. -- **Mensagens nunca confiáveis**: toda escrita vinda da webview passa pela - whitelist `PANEL_SETTINGS` (`validateSettingValue`), que rejeita chaves - desconhecidas e membros herdados (`__proto__` e afins via - `Object.hasOwn`), valida tipos, mínimos, enums e a regra de HTTPS. -- **Segredos fora do DOM**: campos de API key são `type=password`, nunca - pré-preenchidos; a extensão só devolve status (configurada/inválida), e o - valor vai direto para o SecretStorage após validação. -- Escopo local: `localResourceRoots` restrito ao diretório `media/` do - bundle. - -## Fora de escopo (ameaças aceitas) - -- Comprometimento do próprio provider de IA ou da cadeia TLS até ele. -- Segredos em formatos fora dos padrões conhecidos pela varredura. -- Um usuário que explicitamente escolhe "Send Anyway" no modal de segredos. +### 2. Minimizing what is sent + +- Lockfiles, binaries, minified files and files whose diff chunk exceeds + `generateCommit.maxFileSizeKB` × 1024 bytes are excluded from the diff. +- The total diff is truncated at `generateCommit.maxDiffChars`, at file + boundaries, with an explicit notice in the prompt. +- Recent commits (subjects only) are only included if + `includeRecentCommits` is on. + +### 3. API keys + +- Stored **only** in `context.secrets` (VS Code SecretStorage, backed by + the operating system keychain). +- Never in `settings.json`, never in logs, never in error messages. +- **`machine` scope on sensitive URLs**: the `*.baseUrl` and + `anthropicCustom.authHeader` settings are declared with `machine` scope, + so values coming from a workspace's `.vscode/settings.json` are ignored. + Without this, a malicious repository could point the base URL to a + third-party-controlled host and receive your key on the next call + (generation or model catalog). +- The model catalog (`GET /models` of each provider) sends the key only to + the provider's own endpoint, with the header its contract requires + (MiniMax lists models behind `X-Api-Key`, even though the messages + endpoint accepts Bearer; verified on 2026-08-02). +- Key validation uses a minimal request (`max_tokens: 8`). An + authentication failure invalidates the key; non-authentication responses + from the endpoint (rate limit, credits, server error, rejected payload) + confirm the key was accepted; failures with no response (network, + timeout) leave the key unverified and prevent saving through the panel + (with an explicit "Save anyway" option, logged as unverified). + +### 4. Transport and network + +- HTTPS only: non-HTTPS base URLs are rejected at config read time and at + the HTTP layer (`postJson` refuses before fetch). +- Configurable timeout (default 60 s) on requests and CLI executions. +- **Zero telemetry** and zero calls to services other than the active + provider. +- No `vscode.lm` API. + +### 5. Logs and error messages + +- Output Channel with metadata only (provider, model, latency, sizes, + counts). Every value goes through defensive secret redaction before being + written. +- HTTP error details go through the same redaction before reaching the UI. +- CLI stderr is **never** displayed or logged raw: it is classified against + a closed list of signatures (e.g. "not logged in", "rate limited"), + because it may echo the prompt (the diff) or secrets. +- Invalid URLs are logged by host only (credentials embedded in userinfo + never reach the log). + +### 6. CLI execution + +- Binaries resolved by validated name (`[A-Za-z0-9._-]+`); during PATH + detection via the login shell, the name is passed as a positional + argument (`$1`), never interpolated into a command string. +- Execution spawns the resolved binary directly, without a shell (`args` + as an array), cwd at the repository root. +- Cancellation/timeout kills the **process group** (`detached` spawn + + group signal), escalating SIGTERM → SIGKILL. +- CLI providers neither receive nor require API keys (they use the CLI's + own login). +- Environment variables from nested sessions (`CLAUDECODE`, + `CLAUDE_CODE_ENTRYPOINT`) are removed from the `claude` environment. + +### 7. Consistency and abuse + +- One generation per repository: starting another aborts the previous one; + late responses from old runs are discarded. +- The staged diff is re-read before writing the result into the commit box; + if the stage changed during generation, the result is discarded. +- API errors mapped by type (401/403, 402, 429 with `retry-after`, 5xx, + network, timeout) with distinct messages and suggested actions. + +### 8. Settings panel webview + +The extension's only webview is the settings form in the sidebar, isolated +like this: + +- **Strict CSP**: `default-src 'none'` with `script-src` and `style-src` + limited to the bundle's local files (`media/`). No remote content, no + inline script or style, no external images or fonts. +- **XSS-free by construction**: the static HTML interpolates no data at + all; state flows through `postMessage` and is rendered only with safe DOM + APIs (`textContent`, `createElement`, `value` properties), never + `innerHTML` with user data. +- **Never-trusted messages**: every write coming from the webview goes + through the `PANEL_SETTINGS` whitelist (`validateSettingValue`), which + rejects unknown keys and inherited members (`__proto__` and the like via + `Object.hasOwn`), and validates types, minimums, enums and the HTTPS + rule. +- **Secrets out of the DOM**: API key fields are `type=password`, never + pre-filled; the extension only returns status (configured/invalid), and + the value goes straight to SecretStorage after validation. +- Local scope: `localResourceRoots` restricted to the bundle's `media/` + directory. + +## Out of scope (accepted threats) + +- Compromise of the AI provider itself or of the TLS chain up to it. +- Secrets in formats outside the patterns known to the scanner. +- A user who explicitly chooses "Send Anyway" in the secrets modal. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 26ae7b2..8af4207 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,88 +1,89 @@ -# Arquitetura - -Este documento descreve como a extensão é organizada por dentro: o fluxo de -ativação, o fluxo de geração de mensagens, a arquitetura plugável de -providers e as decisões estruturais. Para o modelo de segurança, veja -`SECURITY.md` na raiz. - -## Visão geral dos módulos - -| Arquivo | Papel | -|---------|-------| -| `src/extension.ts` | Ativação: Output Channel, comandos e o painel de settings (WebviewView). | -| `src/commands.ts` | Comando `generate` e o orquestrador do fluxo de geração; ações de erro abrem o painel. | -| `src/git.ts` | Acesso à extensão Git built-in (`vscode.git` API v1), resolução multi-repo, diffs e log recente. | -| `src/config.ts` | Leitura e validação das settings (`generateCommit.*`) e chaves do SecretStorage. | -| `src/providers/` | Interface única + implementações (detalhes abaixo). | -| `src/prompt.ts` | Montagem de system/user prompt e normalização da saída do modelo (puras). | -| `src/diffFilter.ts` | Split do diff por arquivo, exclusões (lockfile, binário, minificado, grande) e truncamento (puras). | -| `src/secretsScan.ts` | Padrões de segredos, varredura do diff e redação de texto (puras). | -| `src/cliDetect.ts` | Resolução de binários de CLI (PATH, shell de login, caminhos comuns). | -| `src/cliRun.ts` | Execução de CLI com stdin, timeout, cancelamento por grupo de processos e classificação de erros. | -| `src/http.ts` | POST JSON somente HTTPS com timeout, cancelamento e mapeamento de erros HTTP. | -| `src/log.ts` | Log somente de metadados, com redação defensiva de segredos. | -| `src/settingsModel.ts` | Modelo puro do painel: whitelist de chaves, validação de valores e helpers. | -| `src/settingsPanel.ts` | WebviewView do painel de configurações (formulário inline na sidebar). | -| `src/modelCatalog.ts` | Catálogo de modelos HTTP: busca `/models` de cada provider, cache com TTL e parser tolerante de formatos. | -| `src/cliCatalog.ts` | Catálogo do Codex CLI: lê `codex debug models` (modelos + níveis de reasoning por modelo), cache com TTL e fallback estático. | -| `src/providersRuntime.ts` | Factories dos providers, disponibilidade, status de chaves e validação de chave. | -| `media/` | JS/CSS do painel (os únicos recursos que a CSP da webview permite). | -| `src/typings/git.d.ts` | Tipagens oficiais da Git API, vendorizadas da tag 1.126.0 do `microsoft/vscode`. | - -## Fluxo de ativação - -1. `activate()` cria o Output Channel "Generate Commit". -2. Registra os comandos (`generateCommit.generate` e `.settings`, que foca - o painel). -3. Registra o `SettingsPanelProvider` na view `generateCommit.settingsView` - (painel da Activity Bar), uma WebviewView sandboxed que hospeda o - formulário de configurações. - -A ativação é preguiça de verdade: `activationEvents` está vazio porque o VS -Code ativa a extensão automaticamente quando qualquer comando contribuído é -invocado (comportamento desde 1.74). Nada roda em segundo plano e não há -telemetria. - -## Fluxo de geração (`generateCommit.generate`) - -1. **Repositório**: o argumento do comando traz `rootUri` (menu - `scm/inputBox`); pela Command Palette, sem argumento. Resolve via - `api.getRepository(uri)`; com um único repo, usa direto; ambíguo, abre - QuickPick. -2. **Diff**: `repo.diff(true)` (staged). Vazio → setting - `unstagedFallback` (`ask`/`always`/`never`) decide se usa - `repo.diff(false)`. Falha (HEAD unborn) → mensagem orientando o commit - inicial. -3. **Filtro**: split por arquivo e exclusão de lockfiles, binários, - minificados e chunks acima de `maxFileSizeKB` (medido em bytes UTF-8). - Tudo excluído → informa e aborta. -4. **Varredura de segredos**: `scanDiff` sobre os arquivos mantidos. Com - achados, modal lista arquivo + tipo (nunca o valor) e exige "Send - Anyway" explícito para prosseguir. -5. **Truncamento**: `maxDiffChars` cortado na fronteira de arquivo, com - marcação explícita no prompt quando trunca. -6. **Prompt**: system (Conventional Commits + idioma + instruções - customizadas) e user (commits recentes como referência de estilo, aviso - de truncamento, diff). -7. **Provider**: lê a disponibilidade de todos em paralelo - (`Promise.all`), escolhe o configurado se disponível, senão o primeiro - disponível na ordem do registry (CLIs primeiro). Nenhum disponível → - oferece o fluxo guiado de configuração. -8. **Geração**: `withProgress` cancelável; o token aborta o - `AbortController`, que cancela o fetch HTTP ou mata o grupo de - processos do CLI. -9. **Escrita**: guarda de geração por repositório (iniciar outra aborta a - anterior; só a mais recente escreve). Se o diff era staged, ele é - relido e comparado antes de escrever; se mudou, o resultado é - descartado. A mensagem final passa por `parseModelOutput` (extrai bloco - de código, remove rótulos e aspas) e vai para `repository.inputBox.value`. - -A extensão **nunca** executa commit. A única escrita é no input box, para -revisão humana. - -## Arquitetura de providers - -Interface única (`src/types.ts`): +# Architecture + +This document describes how the extension is organized on the inside: the +activation flow, the message generation flow, the pluggable provider +architecture and the structural decisions. For the threat model, see +`SECURITY.md` at the root. + +## Module overview + +| File | Role | +|------|------| +| `src/extension.ts` | Activation: Output Channel, commands and the settings panel (WebviewView). | +| `src/commands.ts` | The `generate` command and the generation flow orchestrator; error actions open the panel. | +| `src/git.ts` | Access to the built-in Git extension (`vscode.git` API v1), multi-repo resolution, diffs and recent log. | +| `src/config.ts` | Reading and validating settings (`generateCommit.*`) and SecretStorage keys. | +| `src/providers/` | Single interface + implementations (details below). | +| `src/prompt.ts` | System/user prompt assembly and model output normalization (pure). | +| `src/diffFilter.ts` | Per-file diff splitting, exclusions (lockfile, binary, minified, large) and truncation (pure). | +| `src/secretsScan.ts` | Secret patterns, diff scanning and text redaction (pure). | +| `src/cliDetect.ts` | CLI binary resolution (PATH, login shell, common paths). | +| `src/cliRun.ts` | CLI execution with stdin, timeout, process-group cancellation and error classification. | +| `src/http.ts` | HTTPS-only JSON POST with timeout, cancellation and HTTP error mapping. | +| `src/log.ts` | Metadata-only logging, with defensive secret redaction. | +| `src/settingsModel.ts` | Pure panel model: key whitelist, value validation and helpers. | +| `src/settingsPanel.ts` | Settings panel WebviewView (inline form in the sidebar). | +| `src/modelCatalog.ts` | HTTP model catalog: fetches `/models` from each provider, TTL cache and a format-tolerant parser. | +| `src/cliCatalog.ts` | Codex CLI catalog: reads `codex debug models` (models + per-model reasoning levels), TTL cache and static fallback. | +| `src/providersRuntime.ts` | Provider factories, availability, key status and key validation. | +| `media/` | Panel JS/CSS (the only resources the webview CSP allows). | +| `src/typings/git.d.ts` | Official Git API typings, vendored from the `microsoft/vscode` 1.126.0 tag. | + +## Activation flow + +1. `activate()` creates the "Generate Commit" Output Channel. +2. Registers the commands (`generateCommit.generate` and `.settings`, which + focuses the panel). +3. Registers the `SettingsPanelProvider` in the `generateCommit.settingsView` + view (Activity Bar panel), a sandboxed WebviewView hosting the settings + form. + +Activation is truly lazy: `activationEvents` is empty because VS Code +activates the extension automatically when any contributed command is +invoked (behavior since 1.74). Nothing runs in the background and there is +no telemetry. + +## Generation flow (`generateCommit.generate`) + +1. **Repository**: the command argument carries `rootUri` (`scm/inputBox` + menu); from the Command Palette, no argument. Resolves via + `api.getRepository(uri)`; with a single repo, uses it directly; when + ambiguous, opens a QuickPick. +2. **Diff**: `repo.diff(true)` (staged). Empty → the `unstagedFallback` + setting (`ask`/`always`/`never`) decides whether to use + `repo.diff(false)`. Failure (unborn HEAD) → message guiding the initial + commit. +3. **Filter**: per-file split and exclusion of lockfiles, binaries, + minified files and chunks above `maxFileSizeKB` (measured in UTF-8 + bytes). Everything excluded → informs and aborts. +4. **Secret scanning**: `scanDiff` over the kept files. With findings, a + modal lists file + type (never the value) and requires an explicit + "Send Anyway" to proceed. +5. **Truncation**: `maxDiffChars` cut at file boundaries, with an explicit + marker in the prompt when it truncates. +6. **Prompt**: system (Conventional Commits + language + custom + instructions) and user (recent commits as style reference, truncation + notice, diff). +7. **Provider**: reads the availability of all of them in parallel + (`Promise.all`), picks the configured one if available, otherwise the + first available in registry order (CLIs first). None available → offers + the guided setup flow. +8. **Generation**: cancellable `withProgress`; the token aborts the + `AbortController`, which cancels the HTTP fetch or kills the CLI + process group. +9. **Writing**: per-repository generation guard (starting another aborts + the previous one; only the latest writes). If the diff was staged, it is + re-read and compared before writing; if it changed, the result is + discarded. The final message goes through `parseModelOutput` (extracts + the code block, strips labels and quotes) and into + `repository.inputBox.value`. + +The extension **never** runs a commit. The only write is to the input box, +for human review. + +## Provider architecture + +Single interface (`src/types.ts`): ```ts interface Provider { @@ -94,42 +95,42 @@ interface Provider { } ``` -- `src/providers/registry.ts` — metadados, ordem de exibição/fallback e a - escolha do provider (`resolveProviderChoice`, pura e testada). -- `src/providers/openrouter.ts` — cliente OpenAI-compatible +- `src/providers/registry.ts` — metadata, display/fallback order and + provider choice (`resolveProviderChoice`, pure and tested). +- `src/providers/openrouter.ts` — OpenAI-compatible client (`/chat/completions`). -- `src/providers/anthropic.ts` — cliente genérico da Messages API - (`/v1/messages`), usado pelos presets Kimi, GLM, MiniMax e pelo - endpoint custom (auth `x-api-key` ou `bearer` por preset/setting). -- `src/providers/claudeCli.ts` / `codexCli.ts` — CLIs detectados no PATH - (com cache de sessão), prompt via stdin, sem necessidade de chave. +- `src/providers/anthropic.ts` — generic Messages API client + (`/v1/messages`), used by the Kimi, GLM and MiniMax presets and + by the custom endpoint (`x-api-key` or `bearer` auth per preset/setting). +- `src/providers/claudeCli.ts` / `codexCli.ts` — CLIs detected on PATH + (with session cache), prompt via stdin, no key needed. -`ProviderError` carrega um `kind` (`auth`, `billing`, `rateLimit`, +`ProviderError` carries a `kind` (`auth`, `billing`, `rateLimit`, `server`, `network`, `timeout`, `cancelled`, `cli`, `invalidResponse`, -`unknown`) que dirige mensagens de erro distintas e acionáveis na UI. +`unknown`) that drives distinct, actionable error messages in the UI. -### Adicionar um provider novo +### Adding a new provider -1. Criar `src/providers/meuProvider.ts` expondo uma factory que retorna um - objeto `Provider` (reaproveite `postJson` para HTTP ou `runCli` para - CLI). -2. Registrar o `ProviderMeta` em `registry.ts` (id, label, kind, se precisa - de chave, modelo default, URL do console de chaves). -3. Adicionar o id ao union `ProviderId` em `types.ts`, aos defaults em - `config.ts` e às settings em `package.json` +1. Create `src/providers/myProvider.ts` exposing a factory that returns a + `Provider` object (reuse `postJson` for HTTP or `runCli` for CLI). +2. Register the `ProviderMeta` in `registry.ts` (id, label, kind, whether + it needs a key, default model, key console URL). +3. Add the id to the `ProviderId` union in `types.ts`, to the defaults in + `config.ts` and to the settings in `package.json` (`generateCommit..model`, etc.). -4. Instanciar em `createProviders` (`src/providersRuntime.ts`). -5. Testes: parser/builder puros em `tests/providers.test.ts` e fluxo de - `generate` com mocks em `tests/providers-generate.test.ts`. - -## Build e testes - -- **Bundle**: esbuild (`esbuild.config.mjs`) gera `dist/extension.js` - (CJS, minificado, `vscode` externo). O VS Code carrega só esse arquivo. -- **Testes**: Vitest, apenas módulos puros (sem import de `vscode`). - Módulos com efeitos colaterais (`runCli`, `postJson`) têm testes - comportamentais com processo filho real e fetch stubado; providers são - testados com `vi.mock` das camadas de transporte. +4. Instantiate it in `createProviders` (`src/providersRuntime.ts`). +5. Tests: pure parser/builders in `tests/providers.test.ts` and the + `generate` flow with mocks in `tests/providers-generate.test.ts`. + +## Build and tests + +- **Bundle**: esbuild (`esbuild.config.mjs`) produces `dist/extension.js` + (CJS, minified, `vscode` external). VS Code loads only this file. +- **Tests**: Vitest, pure modules only (no `vscode` import). Modules with + side effects (`runCli`, `postJson`) have behavioral tests with a real + child process and stubbed fetch; providers are tested with `vi.mock` of + the transport layers. - **Lint/format**: Biome 2 (`biome check`). -- **Empacotamento**: `@vscode/vsce` (`pnpm package`), com `.vscodeignore` - limitando o conteúdo a `dist/`, `images/`, manifest, README e LICENSE. +- **Packaging**: `@vscode/vsce` (`pnpm package`), with `.vscodeignore` + limiting the contents to `dist/`, `images/`, manifest, README and + LICENSE. diff --git a/media/settingsPanel.css b/media/settingsPanel.css index 216cbaf..96b015d 100644 --- a/media/settingsPanel.css +++ b/media/settingsPanel.css @@ -61,7 +61,7 @@ textarea.control { .select-wrap select.control { cursor: pointer; - /* Esconde a seta nativa: a seta SVG do wrapper toma o lugar. */ + /* Hides the native arrow: the wrapper's SVG arrow takes its place. */ appearance: none; -webkit-appearance: none; padding-right: 26px; @@ -90,10 +90,10 @@ textarea.control { font-size: 0.95em; } -/* Switch em CSS puro sobre o checkbox nativo: o input vira o trilho e o - ::before vira o polegar. O elemento continua checkbox (acessibilidade, - foco e a lógica de checked intactos); zero JS e zero imagens, então a CSP - segue intacta. */ +/* Pure CSS switch over the native checkbox: the input becomes the track and + ::before becomes the thumb. The element stays a checkbox (accessibility, + focus and checked logic intact); zero JS and zero images, so the CSP + stays intact. */ .checkbox-row input[type="checkbox"] { appearance: none; -webkit-appearance: none; @@ -104,8 +104,8 @@ textarea.control { height: 18px; margin: 0; border-radius: 999px; - /* Em temas claros input-background costuma ser igual ao fundo do painel e - input-border indefinido: sem o fallback o trilho desligado some. */ + /* In light themes input-background often matches the panel background and + input-border is undefined: without the fallback the off track vanishes. */ border: 1px solid var(--vscode-input-border, var(--vscode-descriptionForeground)); background: var(--vscode-input-background); cursor: pointer; @@ -126,9 +126,9 @@ textarea.control { transition: left 0.15s ease; } -/* Verde do "passed" do tema escurecido e levemente azulado via color-mix - (encapsulado em @supports: engines sem color-mix ficam com o verde puro). - O fallback do charts.blue espelha o default do core (#3794ff). */ +/* Theme "passed" green darkened and slightly blued via color-mix + (wrapped in @supports: engines without color-mix keep the plain green). + The charts.blue fallback mirrors the core default (#3794ff). */ .checkbox-row input[type="checkbox"]:checked { background: var(--vscode-testing-iconPassed, #73c991); border-color: var(--vscode-testing-iconPassed, #73c991); @@ -149,9 +149,9 @@ textarea.control { } } -/* Inputs usam border-box: o trilho de 32px tem padding box de 30px (borda - 1px de cada lado). Polegar de 12px em left: 16px => recuos de 3px dos dois - lados (medido no Chromium em 2026-08-02). */ +/* Inputs use border-box: the 32px track has a 30px padding box (1px border + on each side). A 12px thumb at left: 16px => 3px inset on both sides + (measured in Chromium on 2026-08-02). */ .checkbox-row input[type="checkbox"]:checked::before { left: 16px; background: var(--vscode-button-foreground); @@ -162,9 +162,9 @@ textarea.control { outline-offset: 1px; } -/* No Chromium, checkbox casa :focus-visible até em clique de mouse. O anel de - foco só deve aparecer na navegação por teclado: o JS marca a última - modalidade em . */ +/* In Chromium, checkboxes match :focus-visible even on mouse clicks. The + focus ring should only appear on keyboard navigation: the JS records the + last modality in . */ body[data-modality="pointer"] .checkbox-row input[type="checkbox"]:focus-visible { outline: none; } @@ -174,8 +174,8 @@ body[data-modality="pointer"] .checkbox-row input[type="checkbox"]:focus-visible cursor: default; } -/* O :checked acima tem especificidade maior que .invalid-flash sozinho: - sem repetir o seletor, o flash de erro nunca aparece em switch ligado. */ +/* The :checked above has higher specificity than .invalid-flash alone: + without repeating the selector, the error flash never shows on a checked switch. */ .checkbox-row input[type="checkbox"].invalid-flash, .checkbox-row input[type="checkbox"].invalid-flash:checked { border-color: var(--vscode-inputValidation-errorBorder, #f14c4c); diff --git a/media/settingsPanel.js b/media/settingsPanel.js index c8167a1..83296be 100644 --- a/media/settingsPanel.js +++ b/media/settingsPanel.js @@ -4,9 +4,9 @@ const app = document.getElementById('app'); let state = null; - // Modalidade da última interação: o anel de foco dos switches só aparece - // na navegação por teclado (no Chromium, checkbox casa :focus-visible até - // em clique de mouse). + // Last interaction modality: the switch focus ring only appears on + // keyboard navigation (in Chromium, checkboxes match :focus-visible even + // on mouse clicks). document.addEventListener( 'pointerdown', () => document.body.setAttribute('data-modality', 'pointer'), @@ -45,8 +45,8 @@ function field(labelText, control, hintText) { const wrap = el('div', 'field'); const label = el('label', '', labelText); - // O label não pode usar htmlFor (o controle não tem id estável), então o - // clique nele foca o primeiro controle do campo, inclusive dentro do + // The label cannot use htmlFor (the control has no stable id), so + // clicking it focuses the field's first control, even inside the // .select-wrap. label.addEventListener('click', () => { const target = wrap.querySelector('.control, input, select'); @@ -88,8 +88,8 @@ return input; } - // Textarea multilinha com o mesmo debounce do textInput; redimensionável - // verticalmente via CSS. + // Multiline textarea with the same debounce as textInput; vertically + // resizable via CSS. function textareaInput(key, value, placeholder, rows) { const input = el('textarea', 'control'); input.value = value || ''; @@ -112,8 +112,8 @@ return input; } - // Seta customizada em SVG inline criada via DOM: a CSP bloqueia imagens - // (img-src 'none'), então background-image/data URI não funcionariam. + // Custom inline SVG arrow created via DOM: the CSP blocks images + // (img-src 'none'), so background-image/data URI would not work. function selectArrow() { const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('class', 'select-arrow'); @@ -183,12 +183,12 @@ return datalist; } - // O modo de texto livre é indexado pela chave de CONFIG: cada configuração - // alterna entre select e input de forma independente e consistente. + // Free-text mode is keyed by the CONFIG key: each setting toggles between + // select and input independently and consistently. const customMode = {}; - // fkey do campo de texto livre que deve receber foco após a re-renderização - // que troca o select pelo input (escolha de "Custom…"). + // fkey of the free-text field that must receive focus after the re-render + // that swaps the select for the input (choosing "Custom…"). let pendingFocus = null; function modelControl(provider, key, idSuffix, fkey) { @@ -224,9 +224,9 @@ select.addEventListener('change', () => { if (select.value === state.customModelValue) { customMode[key] = true; - // Tira o foco antes de re-renderizar: caso contrário, a restauração de - // foco inseriria o valor sentinela no novo input de texto livre. O - // pendingFocus devolve o foco ao input depois do render. + // Blur before re-rendering: otherwise focus restoration would insert + // the sentinel value into the new free-text input. pendingFocus gives + // focus back to the input after the render. pendingFocus = select.dataset.fkey || null; select.blur(); render(); @@ -248,8 +248,8 @@ section.append(field('Model', control, active.availabilityNote)); if (active.kind === 'cli') { const effortOptions = active.effortOptions.map((o) => [o.value, o.label]); - // O valor salvo fora da lista do modelo selecionado aparece como - // "(current)"; o hint deixa claro que ele pode ser incompatível. + // A saved value outside the selected model's list shows up as + // "(current)"; the hint makes clear it may be incompatible. const currentOpt = active.effortOptions.find((o) => o.value === active.effortSelected); const unsupported = active.effortSelected !== '' && currentOpt && currentOpt.label.endsWith('(current)'); @@ -287,8 +287,8 @@ return section; } - // O modo de texto livre do idioma compartilha o mapa customMode do modelo: - // ambos são indexados pela chave de configuração. + // The language free-text mode shares the model's customMode map: both are + // keyed by the config key. function languageControl() { const key = 'language'; if (customMode[key]) { @@ -328,9 +328,9 @@ select.addEventListener('change', () => { if (select.value === state.customModelValue) { customMode[key] = true; - // Tira o foco antes de re-renderizar: caso contrário, a restauração de - // foco inseriria o valor sentinela no novo input de texto livre. O - // pendingFocus devolve o foco ao input depois do render. + // Blur before re-rendering: otherwise focus restoration would insert + // the sentinel value into the new free-text input. pendingFocus gives + // focus back to the input after the render. pendingFocus = select.dataset.fkey || null; select.blur(); render(); @@ -454,10 +454,10 @@ app.querySelector(`[data-key="${CSS.escape(focusedKey)}"]`); if (again) { again.focus(); - // Em only focus is restored, never the value: the recreated + // select already carries the correct selection from state, and + // reassigning a value captured before the re-render could display an + // option the new state no longer has (e.g. the previous model's effort). if (again.tagName !== 'SELECT' && focusedValue !== null && 'value' in again) { again.value = focusedValue; if (focusedSel && again.setSelectionRange) { @@ -537,7 +537,7 @@ if (ok || !app) return; const input = app.querySelector(`[data-key="${key}"]`); if (input && 'value' in input) { - // Em checkbox o estado visual é o checked, não o value. + // On checkboxes the visual state is checked, not value. if (input.type === 'checkbox') { input.checked = stateValueFor(key) === true; } else { diff --git a/package.json b/package.json index be21753..1c57d57 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,26 @@ "description": "Generate Conventional Commits messages from your staged diff using AI providers (OpenRouter, Kimi, GLM, MiniMax, Claude Code CLI, Codex CLI).", "version": "0.1.0", "publisher": "everton", - "icon": "images/icon.png", + "author": "Éverton Toffanetto", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/everton-dgn/generate-commit-extension.git" + }, + "bugs": { + "url": "https://github.com/everton-dgn/generate-commit-extension/issues" + }, + "homepage": "https://github.com/everton-dgn/generate-commit-extension#readme", + "keywords": [ + "ai", + "commit", + "conventional-commits", + "git", + "llm", + "vscode-extension" + ], + "packageManager": "pnpm@11.18.0", + "icon": "images/icon.png", "engines": { "vscode": "^1.85.0" }, diff --git a/scripts/verify-catalogs.mts b/scripts/verify-catalogs.mts index a7fbd5e..527890b 100644 --- a/scripts/verify-catalogs.mts +++ b/scripts/verify-catalogs.mts @@ -1,7 +1,7 @@ /** - * Verificação ao vivo dos catálogos de modelos (2026-08-02). - * Roda os parsers REAIS da extensão contra as respostas REAIS dos endpoints. - * Uso: pnpm dlx tsx scripts/verify-catalogs.mts + * Live verification of the model catalogs (2026-08-02). + * Runs the extension's REAL parsers against the endpoints' REAL responses. + * Usage: pnpm dlx tsx scripts/verify-catalogs.mts */ import { execFile } from 'node:child_process'; import { parseCodexModelCatalog } from '../src/cliCatalog'; @@ -99,7 +99,7 @@ for (const c of cases) { } } -// Codex CLI: catálogo vivo via `codex debug models` + parser real. +// Codex CLI: live catalog via `codex debug models` + real parser. try { const stdout = await runCodexDebugModels(); const snap = parseCodexModelCatalog(JSON.parse(stdout)); @@ -116,5 +116,5 @@ try { failures += 1; } -console.log(failures === 0 ? '\nTodos os catálogos OK' : `\n${failures} falha(s)`); +console.log(failures === 0 ? '\nAll catalogs OK' : `\n${failures} failure(s)`); process.exit(failures === 0 ? 0 : 1); diff --git a/src/cliCatalog.ts b/src/cliCatalog.ts index 7752df2..df5d384 100644 --- a/src/cliCatalog.ts +++ b/src/cliCatalog.ts @@ -3,20 +3,20 @@ import { runCli } from './cliRun'; import { MODELS_TTL_MS, shouldRefetch } from './modelCatalog'; /** - * Catálogo de modelos do Codex CLI: lê `codex debug models` (subcomando - * verificado na codex-cli 0.146.0 em 2026-08-02), que despeja o catálogo vivo - * em JSON — incluindo, por modelo, os níveis de reasoning suportados. Mantém - * cache por uma hora e degrada para listas estáticas quando a leitura falha. + * Codex CLI model catalog: reads `codex debug models` (subcommand verified + * on codex-cli 0.146.0 on 2026-08-02), which dumps the live catalog as JSON, + * including the supported reasoning levels of each model. Caches for one + * hour and degrades to static lists when the read fails. */ export interface CodexCatalogSnapshot { readonly models: readonly string[]; readonly effortsByModel: Readonly>; - /** Níveis do modelo prioritário do catálogo (o default do CLI). */ + /** Levels of the catalog's top-priority model (the CLI default). */ readonly defaultEfforts: readonly string[]; } -/** Ordem canônica dos níveis de esforço, do mais barato ao mais caro. */ +/** Canonical order of effort levels, from cheapest to most expensive. */ const EFFORT_ORDER: readonly string[] = [ 'none', 'minimal', @@ -28,7 +28,7 @@ const EFFORT_ORDER: readonly string[] = [ 'ultra', ]; -/** Ordena níveis pela sequência canônica; níveis desconhecidos vão ao final, em ordem alfabética. */ +/** Sorts levels by the canonical sequence; unknown levels go last, alphabetically. */ export function orderEffortLevels(levels: readonly string[]): string[] { const known = EFFORT_ORDER.filter((level) => levels.includes(level)); const unknown = levels.filter((level) => !EFFORT_ORDER.includes(level)).sort(); @@ -36,8 +36,8 @@ export function orderEffortLevels(levels: readonly string[]): string[] { } /** - * Fallback estático: união dos níveis vistos no catálogo vivo em 2026-08-02. - * Usado quando o CLI não está instalado ou a leitura falha. + * Static fallback: union of the levels seen in the live catalog on + * 2026-08-02. Used when the CLI is not installed or the read fails. */ export const CODEX_EFFORT_FALLBACK: readonly string[] = [ 'low', @@ -49,11 +49,11 @@ export const CODEX_EFFORT_FALLBACK: readonly string[] = [ ]; /** - * Extrai os slugs e níveis de esforço do JSON de `codex debug models`. - * Apenas entradas com visibility "list" (as visíveis no seletor do CLI) - * entram; duplicatas são removidas mantendo a ordem de prioridade do CLI. - * O default do CLI é o modelo de menor `priority` numérica (campo presente - * no catálogo da codex-cli 0.146.0); sem o campo, vale o primeiro listado. + * Extracts slugs and effort levels from the `codex debug models` JSON. Only + * entries with visibility "list" (those visible in the CLI picker) are kept; + * duplicates are removed preserving the CLI priority order. The CLI default + * is the model with the lowest numeric `priority` (a field present in the + * codex-cli 0.146.0 catalog); without the field, the first listed wins. */ export function parseCodexModelCatalog(json: unknown): CodexCatalogSnapshot { const models: string[] = []; @@ -117,16 +117,16 @@ export class CodexCliCatalog { constructor(private readonly deps: CodexCliCatalogDeps) {} - /** Leitura síncrona do cache; undefined quando ainda não houve busca bem-sucedida. */ + /** Synchronous cache read; undefined until a fetch has succeeded. */ snapshot(): CodexCatalogSnapshot | undefined { return this.cache?.snapshot; } /** - * Níveis de esforço para o modelo selecionado; com o modelo vazio (default - * do CLI) usa os níveis do modelo prioritário do catálogo. Um modelo - * presente mas sem níveis declarados, ou a ausência de catálogo, cai no - * fallback estático. + * Effort levels for the selected model; with an empty model (the CLI + * default) uses the levels of the catalog's top-priority model. A model + * present but with no declared levels, or a missing catalog, falls back to + * the static list. */ effortsFor(model: string): readonly string[] { const snap = this.cache?.snapshot; @@ -138,12 +138,12 @@ export class CodexCliCatalog { return snap.defaultEfforts.length > 0 ? snap.defaultEfforts : CODEX_EFFORT_FALLBACK; } - /** Recarrega o catálogo via `codex debug models`; falhas mantêm o cache anterior. */ + /** Reloads the catalog via `codex debug models`; failures keep the previous cache. */ private inFlight: Promise | undefined; refresh(force = false): Promise { - // Chamadas concorrentes (timer de TTL + config change + saveKey) dividem a - // mesma execução em vez de spawnar CLIs sobrepostos. + // Concurrent calls (TTL timer + config change + saveKey) share the same + // run instead of spawning overlapping CLIs. if (!force && this.inFlight) return this.inFlight; this.inFlight = this.doRefresh(force).finally(() => { this.inFlight = undefined; @@ -168,7 +168,7 @@ export class CodexCliCatalog { signal: this.deps.signal, }); if (result.code !== 0) return false; - // Tolera texto acidental antes do JSON (banner, aviso de versão). + // Tolerates stray text before the JSON (banner, version notice). const start = result.stdout.indexOf('{'); if (start < 0) return false; const snapshot = parseCodexModelCatalog(JSON.parse(result.stdout.slice(start))); @@ -176,7 +176,7 @@ export class CodexCliCatalog { this.cache = { at: this.deps.now(), snapshot }; return true; } catch { - // CLI quebrado, JSON inválido ou abortado: mantém as sugestões anteriores. + // Broken CLI, invalid JSON or aborted: keep the previous suggestions. return false; } } diff --git a/src/cliDetect.ts b/src/cliDetect.ts index 1ad8881..e78d12f 100644 --- a/src/cliDetect.ts +++ b/src/cliDetect.ts @@ -50,7 +50,7 @@ export interface FindBinaryDeps { readonly extraDirs?: readonly string[]; } -/** Interpreta a saída de `command -v ` em um caminho absoluto, se houver. */ +/** Parses the output of `command -v ` into an absolute path, if any. */ export function parseWhichOutput(stdout: string): string | null { for (const line of stdout.split('\n')) { const trimmed = line.trim(); @@ -59,7 +59,7 @@ export function parseWhichOutput(stdout: string): string | null { return null; } -/** Locais comuns de instalação de ferramentas de CLI no macOS/Linux (apps de GUI recebem um PATH mínimo). */ +/** Common CLI tool install locations on macOS/Linux (GUI apps get a minimal PATH). */ export function candidatePaths( name: string, home: string, @@ -81,15 +81,15 @@ const PATH_LOOKUP_TIMEOUT_MS = 5000; const LOGIN_SHELL_TIMEOUT_MS = 8000; /** - * Resolve um binário de CLI: primeiro via PATH herdado, depois via shell de - * login do usuário (corrige lançamentos por GUI no macOS) e, por fim, via - * caminhos absolutos comuns. + * Resolves a CLI binary: first via the inherited PATH, then via the user's + * login shell (fixes GUI launches on macOS) and finally via common absolute + * paths. */ export async function findBinary( name: string, deps?: Partial, ): Promise { - // O nome é passado a um shell abaixo; rejeita qualquer coisa além de um nome simples de binário. + // The name is passed to a shell below; reject anything beyond a simple binary name. if (!/^[A-Za-z0-9._-]+$/.test(name)) return null; const d: FindBinaryDeps = { exec: defaultExec, @@ -105,7 +105,7 @@ export async function findBinary( const path = parseWhichOutput(r.stdout); if (r.code === 0 && path) return path; } catch { - // continua tentando as outras estratégias + // keep trying the other strategies } try { const r = await d.exec(d.shell, ['-lic', 'command -v "$1"', 'sh', name], { @@ -114,7 +114,7 @@ export async function findBinary( const path = parseWhichOutput(r.stdout); if (r.code === 0 && path) return path; } catch { - // continua tentando as outras estratégias + // keep trying the other strategies } for (const path of candidatePaths(name, d.home, d.extraDirs ?? [])) { if (await d.isExecutable(path)) return path; diff --git a/src/cliRun.ts b/src/cliRun.ts index 1441c33..5d7a495 100644 --- a/src/cliRun.ts +++ b/src/cliRun.ts @@ -17,7 +17,7 @@ export interface RunCliOptions { readonly timeoutMs: number; readonly signal: AbortSignal; readonly envRemove?: readonly string[]; - /** Variáveis extras mescladas ao ambiente do filho (ex.: MAX_THINKING_TOKENS=0). */ + /** Extra variables merged into the child environment (e.g. MAX_THINKING_TOKENS=0). */ readonly env?: Readonly>; } @@ -25,16 +25,15 @@ const MAX_STDOUT_CHARS = 2_000_000; const MAX_STDERR_CHARS = 200_000; /** - * Executa uma CLI com o prompt no stdin. Cancelamento e timeout encerram o - * grupo de processos inteiro (SIGTERM, depois SIGKILL após um período de - * tolerância), para que subprocessos criados pela CLI não sobrevivam à - * requisição. + * Runs a CLI with the prompt on stdin. Cancellation and timeout kill the + * whole process group (SIGTERM, then SIGKILL after a grace period) so + * subprocesses spawned by the CLI do not outlive the request. */ export async function runCli(opts: RunCliOptions): Promise { return new Promise((resolve, reject) => { const env = { ...process.env }; Object.assign(env, opts.env); - // A remoção vem por último: uma chave presente nas duas listas é removida. + // Removal comes last: a key present in both lists is removed. for (const key of opts.envRemove ?? []) delete env[key]; const detached = process.platform !== 'win32'; const child = spawn(opts.bin, [...opts.args], { @@ -57,7 +56,7 @@ export async function runCli(opts: RunCliOptions): Promise { try { child.kill(signal); } catch { - // já terminou + // already exited } } }; @@ -101,10 +100,10 @@ export async function runCli(opts: RunCliOptions): Promise { finish(() => resolve({ code, stdout, stderr, timedOut, cancelled })); }); child.stdin.on('error', () => { - // EPIPE: o filho saiu antes de consumir o prompt; tratado pelo close. + // EPIPE: the child exited before consuming the prompt; handled by close. }); - // end() enfileira o payload inteiro e deixa o stream drená-lo, então - // prompts grandes são entregues sem tratamento manual de backpressure. + // end() queues the whole payload and lets the stream drain it, so large + // prompts are delivered without manual backpressure handling. child.stdin.end(opts.stdin, 'utf8'); }); } @@ -132,9 +131,9 @@ const KNOWN_CLI_ERRORS: readonly { pattern: RegExp; label: string }[] = [ ]; /** - * Mapeia o stderr bruto da CLI para um rótulo curto e seguro de uma lista - * fechada. O stderr bruto nunca chega aos logs nem à UI: pode ecoar o prompt - * (o diff) ou segredos. + * Maps raw CLI stderr to a short, safe label from a closed list. Raw stderr + * never reaches the logs or the UI: it can echo the prompt (the diff) or + * secrets. */ export function classifyCliError(stderr: string): string | undefined { for (const { pattern, label } of KNOWN_CLI_ERRORS) { diff --git a/src/commands.ts b/src/commands.ts index b2175da..87e9373 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -28,15 +28,15 @@ export function registerCommands(context: vscode.ExtensionContext): void { ); } -/** Direciona o usuário para o painel de configurações (barra lateral). */ +/** Sends the user to the settings panel (sidebar). */ async function openSettingsPanel(): Promise { await vscode.commands.executeCommand(`${SETTINGS_VIEW_ID}.focus`); } /** - * Uma geração por repositório: iniciar uma nova aborta a anterior e - * somente a execução mais recente pode escrever na input box (respostas - * lentas de execuções antigas são descartadas). + * One generation per repository: starting a new one aborts the previous and + * only the most recent run may write to the input box (late responses from + * stale runs are discarded). */ const activeGenerations = new Map(); let generationSeq = 0; diff --git a/src/config.ts b/src/config.ts index 9d40b63..d3d17f9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -103,7 +103,7 @@ function isHttpsUrl(url: string): boolean { function httpsOnly(url: string, fallback: string, onInvalid: (message: string) => void): string { const trimmed = url.trim().replace(/\/+$/, ''); if (isHttpsUrl(trimmed)) return trimmed; - // Somente o host: a URL bruta pode conter credenciais que não devem chegar ao log. + // Host only: the raw URL may contain credentials that must not reach the log. if (trimmed) onInvalid(`Ignoring non-HTTPS base URL (host: ${hostOnly(url)}); HTTPS is required.`); return fallback; diff --git a/src/diffFilter.ts b/src/diffFilter.ts index f058a6f..6a6e29b 100644 --- a/src/diffFilter.ts +++ b/src/diffFilter.ts @@ -46,7 +46,7 @@ function nameFromHeader(line: string): string { return idx >= 0 ? line.slice(idx + 3) : line; } -/** Divide um diff unificado em chunks por arquivo, indexados pelo caminho pós-imagem. */ +/** Splits a unified diff into per-file chunks, indexed by post-image path. */ export function splitDiffByFile(diff: string): FileDiff[] { const files: FileDiff[] = []; let current: string[] = []; @@ -83,7 +83,7 @@ export interface FilterOptions { readonly maxFileBytes: number; } -/** Descarta lockfiles, arquivos minificados, binários e chunks grandes demais. */ +/** Drops lockfiles, minified files, binaries and oversized chunks. */ export function filterFileDiffs(files: readonly FileDiff[], options: FilterOptions): FilterResult { const kept: FileDiff[] = []; const dropped: DroppedFile[] = []; @@ -107,7 +107,7 @@ export interface TruncateResult { readonly totalFiles: number; } -/** Junta chunks de arquivos até maxChars, cortando nas fronteiras de arquivo. */ +/** Joins file chunks up to maxChars, cutting at file boundaries. */ export function truncateToLimit(files: readonly FileDiff[], maxChars: number): TruncateResult { const totalFiles = files.length; const full = files.map((f) => f.chunk).join('\n'); @@ -121,7 +121,7 @@ export function truncateToLimit(files: readonly FileDiff[], maxChars: number): T const next = used === 0 ? file.chunk.length : used + 1 + file.chunk.length; if (next > maxChars && parts.length > 0) break; if (next > maxChars) { - // Até o primeiro chunk excede o limite: fatia na marra (inclusão parcial). + // Even the first chunk exceeds the limit: hard-slice it (partial inclusion). parts.push(file.chunk.slice(0, maxChars)); included = 1; used = maxChars; diff --git a/src/extension.ts b/src/extension.ts index 2634b0f..0921a1c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,5 +15,5 @@ export function activate(context: vscode.ExtensionContext): void { } export function deactivate(): void { - // nada a descartar além das subscriptions + // nothing to dispose beyond the subscriptions } diff --git a/src/git.ts b/src/git.ts index 4aae6ce..10e91de 100644 --- a/src/git.ts +++ b/src/git.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import type { API, GitExtension, Repository } from './typings/git'; -/** Retorna a API v1 da extensão git nativa, ativando a extensão se necessário. */ +/** Returns the built-in git extension's v1 API, activating the extension if needed. */ export async function getGitApi(): Promise { const extension = vscode.extensions.getExtension('vscode.git'); if (!extension) { @@ -21,9 +21,9 @@ function extractRootUri(arg: unknown): vscode.Uri | undefined { } /** - * Resolve o repositório alvo: a partir do argumento do comando (rootUri - * passado pelo menu scm/inputBox, ou um SourceControl do scm/title), depois o - * único repositório aberto, caso contrário um QuickPick. + * Resolves the target repository: from the command argument (rootUri passed + * by the scm/inputBox menu, or a SourceControl from scm/title), then the + * single open repository, otherwise a QuickPick. */ export async function resolveRepository(api: API, arg: unknown): Promise { const repositories = api.repositories; diff --git a/src/http.ts b/src/http.ts index 2d3874c..661def2 100644 --- a/src/http.ts +++ b/src/http.ts @@ -8,7 +8,7 @@ export interface HttpOptions { const MAX_ERROR_BODY_CHARS = 300; -/** Mapeia códigos de status HTTP para erros de provider distintos e acionáveis. */ +/** Maps HTTP status codes to distinct, actionable provider errors. */ export function mapHttpError( status: number, detail: string, @@ -59,9 +59,9 @@ export function mapHttpError( } /** - * Extrai um detalhe curto e mascarado de um corpo de erro. Qualquer trecho que - * corresponda aos padrões de segredo conhecidos é substituído antes de o texto - * chegar à UI ou aos logs. + * Extracts a short, masked detail from an error body. Any span matching the + * known secret patterns is replaced before the text reaches the UI or the + * logs. */ async function readErrorDetail(res: Response): Promise { try { @@ -84,7 +84,7 @@ async function readErrorDetail(res: Response): Promise { } } -/** Faz POST de JSON apenas sobre HTTPS, com timeout e cancelamento externo. */ +/** POSTs JSON over HTTPS only, with timeout and external cancellation. */ export async function postJson( url: string, headers: Record, @@ -97,7 +97,7 @@ export async function postJson( return requestJson(url, { method: 'POST', headers, body }, opts); } -/** Faz GET de JSON apenas sobre HTTPS (catálogos de modelos e endpoints similares). */ +/** GETs JSON over HTTPS only (model catalogs and similar endpoints). */ export async function getJson( url: string, headers: Record, @@ -116,9 +116,9 @@ interface RequestInit { } async function requestJson(url: string, init: RequestInit, opts: HttpOptions): Promise { - // Compõe o cancelamento do usuário e o timeout manualmente em vez de - // AbortSignal.any (Node 20.3+), que runtimes de extension-host mais antigos - // não fornecem. + // Composes user cancellation and timeout manually instead of + // AbortSignal.any (Node 20.3+), which older extension-host runtimes do + // not provide. const controller = new AbortController(); let timedOut = false; const onUserAbort = (): void => controller.abort(); diff --git a/src/modelCatalog.ts b/src/modelCatalog.ts index e87753c..4c8120e 100644 --- a/src/modelCatalog.ts +++ b/src/modelCatalog.ts @@ -2,27 +2,26 @@ import { getJson } from './http'; import type { ProviderId } from './types'; /** - * Catálogo de modelos: busca a lista de modelos em tempo real no endpoint de - * modelos de cada provider (verificado em 2026-08-02), mantém em cache por uma - * hora e degrada para sugestões estáticas (ou nenhuma) quando a busca falha. - * O campo Model continua aceitando texto livre; o catálogo só alimenta as - * sugestões do datalist. + * Model catalog: fetches the live model list from each provider's models + * endpoint (verified 2026-08-02), caches it for one hour and degrades to + * static suggestions (or none) when the fetch fails. The Model field still + * accepts free text; the catalog only feeds the datalist suggestions. */ export const MODELS_TTL_MS = 60 * 60 * 1000; -/** Sugestões estáticas para providers de CLI (aliases verificados via --help). */ +/** Static suggestions for CLI providers (aliases verified via --help). */ export const CLAUDE_CLI_MODELS: readonly string[] = ['fable', 'opus', 'sonnet', 'haiku']; -/** Resolve o endpoint de modelos de um provider, respeitando base URLs editadas. */ +/** Resolves a provider's models endpoint, honoring edited base URLs. */ export function modelsEndpointFor(id: ProviderId, baseUrl: string): string | null { const base = baseUrl.replace(/\/+$/, ''); switch (id) { case 'openrouter': return `${base}/models`; case 'kimi': - // A base compatível com Anthropic é /anthropic; os modelos ficam - // na API compatível com OpenAI em /v1/models. + // The Anthropic-compatible base is /anthropic; models live on + // the OpenAI-compatible API at /v1/models. return base.endsWith('/anthropic') ? `${base.slice(0, -'/anthropic'.length)}/v1/models` : `${base}/v1/models`; @@ -33,9 +32,9 @@ export function modelsEndpointFor(id: ProviderId, baseUrl: string): string | nul : `${base}/v1/models`; case 'minimax': case 'anthropicCustom': - // MiniMax mantém /anthropic de propósito: sua especificação oficial - // documenta GET /anthropic/v1/models (auth X-Api-Key), ao contrário - // de Kimi/GLM, cujos catálogos ficam no caminho compatível com OpenAI. + // MiniMax keeps /anthropic on purpose: its official spec documents + // GET /anthropic/v1/models (X-Api-Key auth), unlike Kimi/GLM, + // whose catalogs live on the OpenAI-compatible path. return `${base}/v1/models`; case 'claudeCli': case 'codexCli': @@ -44,9 +43,9 @@ export function modelsEndpointFor(id: ProviderId, baseUrl: string): string | nul } /** - * Extrai ids de modelo dos formatos comuns de lista: `{data:[{id}]}`, - * `{models:[{id|name}]}` ou um array simples. Remove duplicatas e mantém a - * ordem do provider. + * Extracts model ids from the common list formats: `{data:[{id}]}`, + * `{models:[{id|name}]}` or a plain array. Removes duplicates and keeps the + * provider's ordering. */ export function parseModelListResponse(json: unknown): string[] { const out: string[] = []; @@ -77,7 +76,7 @@ export function parseModelListResponse(json: unknown): string[] { return out; } -/** Verificação de TTL do cache do catálogo. */ +/** Catalog cache TTL check. */ export function shouldRefetch(fetchedAt: number | undefined, now: number, ttlMs: number): boolean { return fetchedAt === undefined || now - fetchedAt >= ttlMs; } @@ -88,10 +87,9 @@ export interface ModelCatalogConfig { } /** - * Header de auth conforme o contrato do endpoint de CATÁLOGO de cada provider, - * que pode diferir do endpoint de mensagens: o MiniMax lista modelos atrás de - * X-Api-Key mesmo seu endpoint de mensagens aceitando Bearer (verificado em - * 2026-08-02). + * Auth header per each provider's CATALOG endpoint contract, which may differ + * from the messages endpoint: MiniMax lists models behind X-Api-Key even + * though its messages endpoint accepts Bearer (verified 2026-08-02). */ export function catalogAuthHeader( id: ProviderId, @@ -103,7 +101,7 @@ export function catalogAuthHeader( return { 'x-api-key': apiKey }; } -/** Assinatura do cache: refaz a busca quando o endpoint ou o estilo de auth muda. */ +/** Cache signature: refetches when the endpoint or auth style changes. */ export function catalogSignature(id: ProviderId, cfg: ModelCatalogConfig): string { return `${modelsEndpointFor(id, cfg.baseUrl) ?? ''}|${cfg.auth}`; } @@ -127,7 +125,7 @@ export class ModelCatalog { constructor(private readonly deps: ModelCatalogDeps) {} - /** Leitura síncrona: modelos em cache para o endpoint/auth ATUAL, aliases estáticos de CLI ou []. */ + /** Synchronous read: cached models for the CURRENT endpoint/auth, static CLI aliases or []. */ modelsFor(id: ProviderId): readonly string[] { if (id === 'claudeCli') return CLAUDE_CLI_MODELS; const signature = catalogSignature(id, this.deps.getConfig(id)); @@ -135,7 +133,10 @@ export class ModelCatalog { return entry && entry.signature === signature ? entry.models : []; } - /** Atualiza um provider quando o cache está velho (ou quando forçado, ou quando o endpoint/auth mudou); falhas mantêm o cache anterior. */ + /** + * Refreshes a provider when the cache is stale (or forced, or when the + * endpoint/auth changed); failures keep the previous cache. + */ async refresh(id: ProviderId, force = false): Promise { if (id === 'claudeCli' || id === 'codexCli') return false; const cfg = this.deps.getConfig(id); @@ -165,12 +166,14 @@ export class ModelCatalog { } return false; } catch { - // Offline, chave ausente ou endpoint desatualizado: mantém as sugestões anteriores. + // Offline, missing key or stale endpoint: the cache is kept, but old + // suggestions only surface when the current endpoint/auth signature + // matches; after a config change nothing stale is exposed. return false; } } - /** Atualiza todos os providers HTTP; true quando algum catálogo mudou. */ + /** Refreshes all HTTP providers; true when any catalog changed. */ async refreshAll(ids: readonly ProviderId[]): Promise { const results = await Promise.all(ids.map((id) => this.refresh(id))); return results.some(Boolean); diff --git a/src/providers/anthropic.ts b/src/providers/anthropic.ts index 0aef74d..6170eb1 100644 --- a/src/providers/anthropic.ts +++ b/src/providers/anthropic.ts @@ -22,10 +22,10 @@ export interface BuiltAnthropicRequest { } /** - * Monta uma requisição da Anthropic Messages API. Verificado contra a - * documentação oficial (platform.claude.com/docs/en/api/messages) em - * 2026-08-02; o mesmo formato é usado pelos endpoints compatíveis com - * Anthropic de Kimi, GLM e MiniMax. + * Builds an Anthropic Messages API request. Verified against the official + * documentation (platform.claude.com/docs/en/api/messages) on 2026-08-02; + * the same format is used by the Anthropic-compatible endpoints of Kimi, + * GLM and MiniMax. */ export function buildAnthropicRequest( cfg: AnthropicCompatibleConfig, @@ -72,7 +72,7 @@ export interface AnthropicProviderDeps { readonly getConfig: () => AnthropicCompatibleConfig; } -/** Cliente genérico da Anthropic Messages API; os presets (Kimi/GLM/MiniMax) diferem apenas na configuração. */ +/** Generic Anthropic Messages API client; the presets (Kimi/GLM/MiniMax) differ only in config. */ export function createAnthropicCompatibleProvider( id: ProviderId, label: string, diff --git a/src/providers/claudeCli.ts b/src/providers/claudeCli.ts index f9dc4f8..0f24adb 100644 --- a/src/providers/claudeCli.ts +++ b/src/providers/claudeCli.ts @@ -9,17 +9,17 @@ export interface ClaudeCliConfig { } /** - * Flags verificadas contra `claude --help` (Claude Code 2.1.220) em 2026-08-02: + * Flags verified against `claude --help` (Claude Code 2.1.220) on 2026-08-02: * -p, --tools "", --model, --effort, --output-format, --no-session-persistence. - * Não existe flag --fast nesta versão (o modo fast é exclusivo do modo interativo). - * Thinking desligado via MAX_THINKING_TOKENS=0 (env documentada do Claude Code; - * smoke test em 2026-08-02). + * There is no --fast flag in this version (fast mode is exclusive to interactive mode). + * Thinking disabled via MAX_THINKING_TOKENS=0 (documented Claude Code env var; + * smoke test on 2026-08-02). */ export function buildClaudeArgs(cfg: ClaudeCliConfig): string[] { const args = ['-p', '--tools', '', '--output-format', 'text', '--no-session-persistence']; const model = cfg.model.trim(); if (model) args.push('--model', model); - // Com thinking desligado, --effort é irrelevante (não há blocos de raciocínio). + // With thinking disabled, --effort is irrelevant (there are no reasoning blocks). const effort = cfg.disableThinking ? '' : cfg.effort.trim(); if (effort) args.push('--effort', effort); return args; @@ -31,9 +31,9 @@ export interface ClaudeCliDeps { readonly log?: (line: string) => void; } -// Cache com escopo de sessão: createProviders recria as instâncias a cada -// comando, então o caminho resolvido do binário precisa viver no escopo do -// módulo para evitar nova detecção. +// Session-scoped cache: createProviders recreates the instances on every +// command, so the resolved binary path must live in module scope to avoid +// re-detection. let cachedClaudePath: string | null | undefined; export function createClaudeCliProvider(deps: ClaudeCliDeps): Provider { diff --git a/src/providers/codexCli.ts b/src/providers/codexCli.ts index c0b9467..376082d 100644 --- a/src/providers/codexCli.ts +++ b/src/providers/codexCli.ts @@ -12,14 +12,14 @@ export interface CodexCliConfig { } /** - * Flags verificadas contra `codex exec --help` (codex-cli 0.146.0) e a - * referência oficial de configuração (`model_reasoning_effort`: none, low, - * medium, high, xhigh, max, ultra — níveis variam por modelo, lidos ao vivo - * de `codex debug models`) em 2026-08-02. Nota: a flag `-a/--ask-for-approval` - * sumiu do `--help` entre duas leituras no mesmo dia e versão (superfície de - * CLI dinâmica), então a política de aprovação é fixada via chave de configuração. - * `none` desliga o raciocínio (aceito mesmo não constando nos níveis por - * modelo do catálogo; smoke test em 2026-08-02). + * Flags verified against `codex exec --help` (codex-cli 0.146.0) and the + * official configuration reference (`model_reasoning_effort`: none, low, + * medium, high, xhigh, max, ultra; levels vary per model, read live from + * `codex debug models`) on 2026-08-02. Note: the `-a/--ask-for-approval` + * flag vanished from `--help` between two reads on the same day and version + * (dynamic CLI surface), so the approval policy is pinned via config key. + * `none` disables reasoning (accepted even though it is not listed in the + * per-model catalog levels; smoke test on 2026-08-02). */ export function buildCodexArgs(cfg: CodexCliConfig, outputFile: string): string[] { const args = [ @@ -48,9 +48,9 @@ export interface CodexCliDeps { readonly log?: (line: string) => void; } -// Cache com escopo de sessão: createProviders recria as instâncias a cada -// comando, então o caminho resolvido do binário precisa viver no escopo do -// módulo para evitar nova detecção. +// Session-scoped cache: createProviders recreates the instances on every +// command, so the resolved binary path must live in module scope to avoid +// re-detection. let cachedCodexPath: string | null | undefined; export function createCodexCliProvider(deps: CodexCliDeps): Provider { diff --git a/src/providers/openrouter.ts b/src/providers/openrouter.ts index 05aa9ad..32dcfb4 100644 --- a/src/providers/openrouter.ts +++ b/src/providers/openrouter.ts @@ -1,7 +1,7 @@ import { postJson } from '../http'; import { type GenerateRequest, type Provider, ProviderError } from '../types'; -/** Extrai o texto do assistente de um chat completion compatível com OpenAI. */ +/** Extracts the assistant text from an OpenAI-compatible chat completion. */ export function parseOpenAiChatResponse(json: unknown): string { if (typeof json !== 'object' || json === null) { throw new ProviderError('invalidResponse', 'Provider returned a non-JSON response'); diff --git a/src/providers/registry.ts b/src/providers/registry.ts index b6ba6af..b3c067c 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -6,11 +6,11 @@ export interface ProviderMeta { readonly kind: 'http' | 'cli'; readonly needsApiKey: boolean; readonly defaultModel: string; - /** Onde o usuário cria uma chave de API (verificado em 2026-08-02). */ + /** Where the user creates an API key (verified on 2026-08-02). */ readonly keyConsoleUrl?: string; } -/** Ordem de exibição/fallback: CLIs sem configuração primeiro, depois os providers HTTP. */ +/** Display/fallback order: zero-config CLIs first, then the HTTP providers. */ export const PROVIDERS: readonly ProviderMeta[] = [ { id: 'claudeCli', label: 'Claude Code CLI', kind: 'cli', needsApiKey: false, defaultModel: '' }, { id: 'codexCli', label: 'Codex CLI', kind: 'cli', needsApiKey: false, defaultModel: '' }, @@ -65,8 +65,8 @@ export function isProviderId(value: string): value is ProviderId { } /** - * Escolhe o provider a usar: o configurado quando disponível, caso contrário - * o primeiro provider disponível na ordem de exibição; null quando nenhum está disponível. + * Picks the provider to use: the configured one when available, otherwise + * the first available provider in display order; null when none is available. */ export function resolveProviderChoice( configured: string, diff --git a/src/providersRuntime.ts b/src/providersRuntime.ts index bb315cb..b7c7d98 100644 --- a/src/providersRuntime.ts +++ b/src/providersRuntime.ts @@ -86,7 +86,7 @@ export interface KeyStatus { readonly hasKey: boolean; } -/** Quais providers baseados em chave têm uma chave armazenada (nunca expõe valores). */ +/** Which key-based providers have a stored key (never exposes values). */ export async function collectKeyStatus(context: vscode.ExtensionContext): Promise { const result: KeyStatus[] = []; for (const meta of PROVIDERS) { @@ -103,10 +103,10 @@ export interface KeyValidation { } /** - * Valida uma chave de API candidata com uma requisição mínima. Apenas falhas - * de autenticação rejeitam a chave: qualquer resposta do endpoint (cobrança, - * rate limit, erro de servidor, até um payload rejeitado) prova que a chave - * foi aceita, enquanto erros de conectividade não provam nada. + * Validates a candidate API key with a minimal request. Only authentication + * failures reject the key: any endpoint response (billing, rate limit, + * server error, even a rejected payload) proves the key was accepted, while + * connectivity errors prove nothing. */ export async function validateApiKey(id: ProviderId, apiKey: string): Promise { try { diff --git a/src/secretsScan.ts b/src/secretsScan.ts index af6d863..da0d49e 100644 --- a/src/secretsScan.ts +++ b/src/secretsScan.ts @@ -19,7 +19,7 @@ export interface Finding { readonly type: SecretType; } -/** Rótulos legíveis para humanos; nunca incluem nenhum valor de segredo encontrado. */ +/** Human-readable labels; they never include any found secret value. */ export const SECRET_TYPE_LABELS: Readonly> = { 'openrouter-key': 'OpenRouter API key (sk-or-...)', 'anthropic-key': 'Anthropic API key (sk-ant-...)', @@ -69,8 +69,8 @@ function fileNameFindings(fileName: string): Finding[] { } /** - * Varre o diff em busca de padrões que parecem segredos. Os achados nunca - * carregam o valor encontrado, apenas o arquivo e a categoria do segredo. + * Scans the diff for patterns that look like secrets. Findings never carry + * the matched value, only the file and the secret category. */ export function scanDiff(files: readonly FileDiff[]): Finding[] { const seen = new Set(); @@ -95,7 +95,7 @@ export function scanDiff(files: readonly FileDiff[]): Finding[] { return findings; } -/** Substitui qualquer coisa que corresponda aos padrões de segredos conhecidos por "[redacted]". */ +/** Replaces anything matching the known secret patterns with "[redacted]". */ export function redactSecrets(text: string): string { let out = text; for (const pattern of CONTENT_PATTERNS) { diff --git a/src/settingsModel.ts b/src/settingsModel.ts index 00af018..e284d95 100644 --- a/src/settingsModel.ts +++ b/src/settingsModel.ts @@ -1,11 +1,11 @@ -/** Interpreta uma configuração de inteiro positivo; undefined quando inválida ou abaixo do mínimo. */ +/** Parses a positive-integer setting; undefined when invalid or below the minimum. */ export function parseIntSetting(input: string, min: number): number | undefined { const value = Number(input.trim()); if (!Number.isInteger(value) || value < min) return undefined; return value; } -/** Vazio restaura o padrão do provider; caso contrário, exige uma URL HTTPS válida. */ +/** Empty restores the provider default; otherwise requires a valid HTTPS URL. */ export function isValidBaseUrl(input: string): boolean { const trimmed = input.trim(); if (trimmed === '') return true; @@ -35,7 +35,7 @@ export const LANGUAGE_OPTIONS: readonly LanguageOption[] = [ { code: 'ru', label: 'Русский' }, ]; -// ---------- validação de mensagens do painel de configurações ---------- +// ---------- settings panel message validation ---------- export type SettingKind = 'string' | 'integer' | 'boolean' | 'enum' | 'baseUrl'; @@ -46,10 +46,9 @@ export interface SettingSpec { } /** - * Whitelist de todas as chaves que o painel de configurações pode gravar via - * config.update. As chaves são relativas à seção generateCommit; chaves com - * escopo de provider usam a forma ".". Qualquer coisa fora deste - * mapa é rejeitada. + * Whitelist of every key the settings panel may write via config.update. + * Keys are relative to the generateCommit section; provider-scoped keys use + * the "." form. Anything outside this map is rejected. */ export const PANEL_SETTINGS: Readonly> = { provider: { @@ -86,13 +85,13 @@ export type ValidatedValue = string | number | boolean; export type ValidationResult = { ok: true; value: ValidatedValue } | { ok: false }; /** - * Valida um valor vindo da webview do painel de configurações contra a - * whitelist. Nunca confie em mensagens da webview: chaves desconhecidas e - * tipos incompatíveis são rejeitados. + * Validates a value coming from the settings panel webview against the + * whitelist. Never trust webview messages: unknown keys and mismatched + * types are rejected. */ export function validateSettingValue(key: string, value: unknown): ValidationResult { - // Object.hasOwn bloqueia membros herdados (__proto__, constructor, ...), - // que de outra forma passariam na verificação de verdade abaixo. + // Object.hasOwn blocks inherited members (__proto__, constructor, ...), + // which would otherwise pass the truthiness check below. const spec = Object.hasOwn(PANEL_SETTINGS, key) ? PANEL_SETTINGS[key] : undefined; if (!spec) return { ok: false }; switch (spec.kind) { @@ -116,10 +115,10 @@ export function validateSettingValue(key: string, value: unknown): ValidationRes case 'string': { if (typeof value !== 'string') return { ok: false }; const trimmed = value.trim(); - // A sentinela Custom… é um controle de UI, nunca um valor persistível: - // como id de modelo ela viraria um modelo inexistente, e como idioma ela - // duplicaria o value da opção Custom… no select, tornando o modo de - // texto livre inalcançável. + // The Custom… sentinel is a UI control, never a persistable value: + // as a model id it would become a nonexistent model, and as a language + // it would duplicate the Custom… option's value in the select, making + // free-text mode unreachable. if ((key.endsWith('.model') || key === 'language') && trimmed === CUSTOM_MODEL_VALUE) { return { ok: false }; } @@ -128,7 +127,7 @@ export function validateSettingValue(key: string, value: unknown): ValidationRes } } -/** Restringe os ids de provider que aceitam API key (alvos do secret storage). */ +/** Narrows the provider ids that accept an API key (secret storage targets). */ export function isKeyBackedProvider( id: string, ): id is 'openrouter' | 'kimi' | 'glm' | 'minimax' | 'anthropicCustom' { @@ -141,9 +140,9 @@ export function isKeyBackedProvider( ); } -// ---------- opções do dropdown de modelo ---------- +// ---------- model dropdown options ---------- -/** Opção sentinela no select de modelo que alterna para o modo de texto livre. */ +/** Sentinel option in the model select that switches to free-text mode. */ export const CUSTOM_MODEL_VALUE = '__custom'; export interface ModelOption { @@ -152,10 +151,10 @@ export interface ModelOption { } /** - * Opções do dropdown de modelo: primeiro a opção vazia de padrão do provider, - * o valor atual quando não está no catálogo (para um modelo customizado nunca - * sumir de repente), cada entrada do catálogo exceto qualquer colisão literal - * com a sentinela, e a sentinela Custom… no final. + * Model dropdown options: first the empty provider-default option, the + * current value when it is not in the catalog (so a custom model never + * suddenly disappears), each catalog entry except any literal collision + * with the sentinel, and the Custom… sentinel at the end. */ export function buildModelOptions( models: readonly string[], @@ -173,8 +172,8 @@ export function buildModelOptions( } /** - * Níveis de esforço do Claude Code CLI, verificados contra `claude --help` - * (Claude Code 2.1.220) em 2026-08-02. + * Claude Code CLI effort levels, checked against `claude --help` + * (Claude Code 2.1.220) on 2026-08-02. */ export const CLAUDE_CLI_EFFORT_LEVELS: readonly string[] = [ 'low', @@ -185,9 +184,9 @@ export const CLAUDE_CLI_EFFORT_LEVELS: readonly string[] = [ ]; /** - * Opções do dropdown de esforço: primeiro a opção vazia de padrão do CLI, - * o valor atual quando não está na lista (para um nível desconhecido nunca - * sumir de repente) e cada nível suportado. + * Effort dropdown options: first the empty CLI-default option, the current + * value when it is not in the list (so an unknown level never suddenly + * disappears) and each supported level. */ export function buildEffortOptions( levels: readonly string[], @@ -203,14 +202,14 @@ export function buildEffortOptions( return { options, selected: current }; } -// ---------- protocolo de mensagens do painel de configurações ---------- +// ---------- settings panel message protocol ---------- export type PanelMessage = | { type: 'ready' } | { type: 'update'; key: string; value: unknown } | { type: 'saveKey'; provider: string; value: string; force: boolean }; -/** Interpreta e sanitiza um postMessage bruto vindo da webview do painel de configurações. */ +/** Parses and sanitizes a raw postMessage from the settings panel webview. */ export function parseMessage(raw: unknown): PanelMessage | undefined { if (typeof raw !== 'object' || raw === null) return undefined; const msg = raw as { diff --git a/src/settingsPanel.ts b/src/settingsPanel.ts index dc4bb97..eddb73a 100644 --- a/src/settingsPanel.ts +++ b/src/settingsPanel.ts @@ -62,11 +62,10 @@ interface PanelState { } /** - * Painel de configurações da barra lateral: uma WebviewView estritamente - * isolada em sandbox (CSP default-src 'none', apenas mídia local empacotada, - * sem conteúdo remoto, sem código inline). Mensagens da webview são validadas - * contra uma whitelist antes de qualquer config.update, e valores de segredos - * nunca fluem de volta para o DOM. + * Sidebar settings panel: a strictly sandboxed WebviewView (CSP default-src + * 'none', only packaged local media, no remote content, no inline code). + * Webview messages are validated against a whitelist before any + * config.update, and secret values never flow back into the DOM. */ export class SettingsPanelProvider implements vscode.WebviewViewProvider { private view?: vscode.WebviewView; @@ -120,9 +119,9 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { view.onDidDispose( () => { this.view = undefined; - // A view pode ser reaberta depois: sem limpar aqui, cada reabertura - // empilharia um novo interval e ticks antigos continuariam spawnando - // buscas de catálogo (inclusive processos CLI) com o painel fechado. + // The view can be reopened later: without clearing here, each reopen + // would stack a new interval and old ticks would keep spawning + // catalog fetches (including CLI processes) with the panel closed. if (this.refreshTimer !== undefined) { clearInterval(this.refreshTimer); this.refreshTimer = undefined; @@ -134,7 +133,7 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { view.webview.onDidReceiveMessage( (message: unknown) => { this.handleMessage(message).catch((err: unknown) => { - // Nunca deixar o formulário travado: reportar falhas inesperadas de volta. + // Never leave the form stuck: report unexpected failures back. logMeta('panel.messageError', { detail: err instanceof Error ? err.name : 'unknown' }); const msg = (typeof message === 'object' && message !== null ? message : {}) as { type?: unknown; @@ -162,13 +161,12 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { undefined, this.context.subscriptions, ); - // Busca os catálogos de modelos ao vivo em segundo plano; o formulário se - // re-renderiza com sugestões quando eles chegam (falhas mantêm os campos - // somente de texto livre). + // Fetches the live model catalogs in the background; the form re-renders + // with suggestions when they arrive (failures keep existing suggestions + // when a usable catalog exists; fields stay free-text only otherwise). this.refreshCatalogs(); - // Mantém os catálogos atualizados enquanto o painel estiver aberto; o - // portão de TTL em refresh() torna isso um no-op até que uma entrada - // realmente expire. + // Keeps the catalogs fresh while the panel is open; the TTL gate in + // refresh() makes this a no-op until an entry actually expires. if (this.refreshTimer !== undefined) clearInterval(this.refreshTimer); this.refreshTimer = setInterval(() => this.refreshCatalogs(), MODELS_TTL_MS); } @@ -250,9 +248,8 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { private async pushState(): Promise { if (!this.view) return; - // buildState é lento (sondagem de disponibilidade): somente a chamada - // mais recente pode publicar, para que mudanças rápidas de configuração - // nunca renderizem um snapshot desatualizado. + // buildState is slow (availability probing): only the latest call may + // publish, so rapid config changes never render a stale snapshot. const seq = ++this.stateSeq; const state = await this.buildState(); if (!this.view || seq !== this.stateSeq) return; @@ -270,7 +267,7 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { const result = validateSettingValue(message.key, message.value); if (!result.ok) { logMeta('settings.rejected', { key: message.key }); - // Avisa o formulário para que o campo reverta em vez de divergir silenciosamente. + // Notify the form so the field reverts instead of silently diverging. if (this.view) { await this.view.webview.postMessage({ type: 'updateResult', @@ -304,9 +301,8 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { }); return; } - // Force = override explícito do usuário após uma validação malsucedida - // (rede instável ou endpoint fora do ar não devem bloquear totalmente - // o salvamento). + // Force = explicit user override after a failed validation (flaky + // network or a down endpoint must not fully block saving). if (force) { await this.context.secrets.store(secretKeyFor(provider), key); logMeta('secrets.stored', { provider, validated: false }); @@ -335,9 +331,8 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { }); if (validation.ok) { await this.pushState(); - // Uma nova chave pode desbloquear o catálogo de modelos do provider: - // busca-o agora (forçado, ignorando o TTL) em vez de esperar a próxima - // abertura. + // A new key may unlock the provider's model catalog: fetch it now + // (forced, ignoring the TTL) instead of waiting for the next open. this.catalog .refresh(provider, true) .then((changed) => { diff --git a/src/types.ts b/src/types.ts index ef13de6..62ac535 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,15 +12,15 @@ export type ProviderKind = 'http' | 'cli'; export interface GenerateRequest { readonly systemPrompt: string; readonly userPrompt: string; - /** Id do modelo resolvido; string vazia significa "padrão do provider/CLI". */ + /** Resolved model id; an empty string means "provider/CLI default". */ readonly model: string; - /** Esforço de raciocínio para providers CLI; string vazia significa "padrão do CLI". */ + /** Reasoning effort for CLI providers; an empty string means "CLI default". */ readonly effort: string; - /** Teto de saída para providers HTTP; padrão 512. */ + /** Output cap for HTTP providers; defaults to 512. */ readonly maxTokens?: number; readonly timeoutMs: number; readonly signal: AbortSignal; - /** Diretório de trabalho para providers CLI (raiz do repositório). */ + /** Working directory for CLI providers (the repository root). */ readonly cwd?: string; } @@ -48,7 +48,7 @@ export class ProviderError extends Error { constructor( readonly kind: ProviderErrorKind, message: string, - /** Dica acionável exibida ao usuário junto com a mensagem. */ + /** Actionable hint shown to the user alongside the message. */ readonly action?: string, ) { super(message); diff --git a/tests/diffFilter.test.ts b/tests/diffFilter.test.ts index 7df93cf..03014dd 100644 --- a/tests/diffFilter.test.ts +++ b/tests/diffFilter.test.ts @@ -80,7 +80,7 @@ describe('filterFileDiffs', () => { }); it('measures the limit in bytes, not UTF-16 units', () => { - // Emoji de 4 bytes: o chunk é curto em .length, mas excede o limite em bytes. + // 4-byte emoji: the chunk is short in .length but exceeds the limit in bytes. const emoji = chunk('src/emoji.ts', [`const s = "${'😀'.repeat(400)}";`]); const files = splitDiffByFile(emoji); const chunkLength = files[0]?.chunk.length ?? 0; diff --git a/tests/settingsModel.test.ts b/tests/settingsModel.test.ts index 6f96d0f..94831d7 100644 --- a/tests/settingsModel.test.ts +++ b/tests/settingsModel.test.ts @@ -37,7 +37,7 @@ describe('isValidBaseUrl', () => { expect(isValidBaseUrl('api.example.com')).toBe(false); expect(isValidBaseUrl('https://')).toBe(false); expect(isValidBaseUrl('https://?x=1')).toBe(false); - expect(isValidBaseUrl('https:// espaço')).toBe(false); + expect(isValidBaseUrl('https:// with space')).toBe(false); }); }); @@ -120,7 +120,7 @@ describe('validateSettingValue', () => { 'timeoutSeconds', ]; for (const key of expected) expect(PANEL_SETTINGS[key]).toBeDefined(); - // Campos por provider que o painel expõe na seção Provider. + // Per-provider fields the panel exposes in the Provider section. const perProvider: Record = { openrouter: ['model', 'baseUrl'], kimi: ['model', 'baseUrl'], diff --git a/tsconfig.json b/tsconfig.json index fa10c9f..ad99717 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,5 +15,5 @@ "sourceMap": true, "outDir": "dist" }, - "include": ["src", "tests", "scripts", "vitest.config.ts"] + "include": ["src", "tests", "scripts", "vitest.config.mts"] } diff --git a/vitest.config.ts b/vitest.config.mts similarity index 100% rename from vitest.config.ts rename to vitest.config.mts