O Tab Domain Executor (TDE) é uma extensão para Google Chrome que oferece uma forma deliberada, segura e reversível de fechar abas agrupadas por domínio. Não é um gerenciador automático — é uma ferramenta de execução controlada, projetada para quando o usuário sabe exatamente o que quer fazer.
Navegadores modernos acumulam dezenas (ou centenas) de abas espalhadas em múltiplas janelas. Fechar abas manualmente é tedioso, propenso a erros e, na maioria das vezes, irreversível. As extensões existentes ou automatizam demais (tirando o controle do usuário) ou são complexas demais para uma tarefa simples.
O TDE foi construído sobre três pilares que guiam cada decisão de design:
Este documento cobre a especificação completa do TDE v1 — desde a motivação e casos de uso até a arquitetura técnica e guia de desenvolvimento. Use a barra lateral para navegar entre as seções.
O TDE é uma extensão Chrome (Manifest V3) construída com TypeScript e Webpack. Seu design segue uma arquitetura modular em camadas onde a UI nunca executa ações diretamente — ela apenas solicita ao background, que orquestra os módulos especializados.
Toda interação no TDE segue sempre o mesmo caminho de 5 etapas:
youtube.com).
O backend é composto por 5 módulos independentes, cada um com uma responsabilidade única:
null para URLs inválidas.DomainGroup[] e WindowGroup[].chrome.tabs.create().domain adicionado
Instale o TDE em menos de 5 minutos. São apenas três passos: instalar dependências, buildar e carregar no Chrome.
cd tab-domain-executor
dist/ pronta para uso.npm install npm run build
npm run dev para modo watch — a extensão é recompilada automaticamente a cada alteração.
dist/ gerada no passo anterior.chrome://extensions na barra de endereços
dist/
O TDE é uma ferramenta focada. Cada funcionalidade foi desenhada para aumentar o controle do usuário sem adicionar complexidade desnecessária.
- Lê todas as abas abertas em todas as janelas via
getAllTabs()→chrome.tabs.query({}) - Normaliza URLs via
normalizeUrl()extraindo o domínio base (ex:youtube.com) - Agrupa abas por domínio via
groupByDomain()e por janela viagroupByWindow() - Exibe quantidade de abas e janelas por domínio
- Busca por nome de domínio na lista
- Ordenação: mais abas, menos abas, A→Z, Z→A, mais recente, mais antigo
- Escolha entre "Todas as janelas" ou "Escolher janelas"
- Cards de janelas com contagem de abas por janela
- Seleção múltipla de janelas por checkbox
- Botão "Continuar" exibe o total de abas que serão afetadas
- Navegação de volta sem perda de contexto
- Lista todas as abas do escopo definido com título, URL e favicon
- Fechamento individual por aba via
closeSingleTab()→chrome.tabs.remove(tabId) - Fechamento em lote de todas as abas do escopo via
closeBulkTabs()→chrome.tabs.remove([...ids]) - Fechamento em lote armazena log via
storeRecoveryLog(), válido por 15 min - Feedback visual claro após a execução
- Log
RecoveryLogem memória da última ação em lote, TTL viaRECOVERY_TTL_MS(15 minutos) - Exibe botão "Recuperar" na tela inicial com tempo restante via
hasRecoverableAction() - Recria as abas nas janelas originais via
restoreTabs()→chrome.tabs.create() - Log apagado via
clearRecoveryLog()após recuperação ou expiração dosetTimeout - Não disponível para fechamentos individuais (
closeSingleTabnão chamastoreRecoveryLog)
O TDE é deliberadamente simples. As seguintes funcionalidades foram excluídas por design:
Os requisitos funcionais definem o comportamento esperado do sistema. Estão organizados em 4 grupos alinhados ao fluxo de uso.
| ID | Descrição | Prioridade |
|---|---|---|
| RF-01 | O sistema deve ler todas as abas abertas em todas as janelas via chrome.tabs.query({}) somente quando solicitado pelo usuário. | Alta |
| RF-02 | O sistema deve normalizar a URL de cada aba via normalizeUrl() (url-normalizer.ts) extraindo o domínio base (ex: youtube.com), retornando null para URLs inválidas. | Alta |
| RF-03 | O sistema deve agrupar as abas por domínio via groupByDomain(), retornando DomainGroup[] com tabCount, windowCount e windowIds. Os campos minTabId e maxTabId são calculados no dispatcher a partir dos IDs das abas para suportar ordenação por recência (RF-05). | Alta |
| RF-04 | O sistema deve exibir os domínios como cards ordenáveis com busca por texto. | Alta |
| RF-05 | O sistema deve suportar ordenação por: mais abas, menos abas, A→Z, Z→A, mais recente, mais antigo. | Média |
| RF-06 | O sistema deve ignorar abas sem domínio válido. URLs com prefixo chrome://, about: ou chrome-extension:// retornam null do normalizador e são excluídas do agrupamento. | Alta |
| ID | Descrição | Prioridade |
|---|---|---|
| RF-07 | Após selecionar um domínio, o sistema deve oferecer duas opções: "Todas as janelas" ou "Escolher janelas". | Alta |
| RF-08 | Se "Todas as janelas" (TabScope = 'all'), o sistema deve avançar diretamente para a listagem via filterTabsByScope(tabs, domain, 'all') sem etapa adicional. | Alta |
| RF-09 | Se "Escolher janelas" (TabScope = 'windows'), o sistema deve exibir cards gerados por groupByWindow() retornando WindowGroup[], com seleção múltipla por checkbox (campo selected). | Alta |
| RF-10 | O botão "Continuar" deve exibir o total de abas que serão afetadas e estar desabilitado se nenhuma janela for selecionada. | Alta |
| ID | Descrição | Prioridade |
|---|---|---|
| RF-11 | O sistema deve listar as abas do escopo definido com título, URL completa e favicon. | Alta |
| RF-12 | O sistema deve permitir fechar uma aba individualmente via closeSingleTab(tabId) → chrome.tabs.remove(tabId). Esta ação não chama storeRecoveryLog e é irreversível. | Alta |
| RF-13 | O sistema deve fechar todas as abas do escopo em lote via closeBulkTabs(tabIds) → chrome.tabs.remove([...ids]), seguido de storeRecoveryLog(tabs) em memória. | Alta |
| RF-14 | Após o fechamento, o sistema deve exibir feedback visual confirmando a ação e informando a disponibilidade de recuperação. | Alta |
| ID | Descrição | Prioridade |
|---|---|---|
| RF-15 | O sistema deve armazenar em memória um RecoveryLog com tabs: TabMinimal[] (url + windowId), timestamp e ttl das abas fechadas em lote. | Alta |
| RF-16 | O log de recuperação deve expirar automaticamente após RECOVERY_TTL_MS (15 × 60 × 1000 ms) via setTimeout em recovery-manager.ts. | Alta |
| RF-17 | Na tela inicial, se houver log válido, deve ser exibido botão "Recuperar" com tempo restante. | Alta |
| RF-18 | Ao recuperar, o sistema deve recriar todas as abas via restoreTabs(tabs) → chrome.tabs.create() nas janelas originais e apagar o log via clearRecoveryLog() imediatamente. | Alta |
Os requisitos não funcionais definem as restrições de qualidade que o sistema deve atender independentemente do comportamento funcional.
- RNF-01 A análise de abas deve completar em menos de 2 segundos para até 200 abas abertas.
- RNF-02 A resposta a ações do usuário (cliques, digitação) não deve apresentar latência perceptível (< 100ms).
- RNF-03 O fechamento em lote não deve bloquear a UI durante a execução.
- RNF-04 Nenhum dado de navegação deve ser transmitido para servidores externos.
- RNF-05 O log de recuperação deve existir exclusivamente em memória — variável
let recoveryLog: RecoveryLog | nullemrecovery-manager.ts(semlocalStorageouchrome.storage). - RNF-06 A extensão deve operar com as permissões mínimas necessárias:
"permissions": ["tabs"]emmanifest.json.
- RNF-07 O fluxo completo (análise → fechamento) deve ser executável em menos de 5 interações.
- RNF-08 O usuário deve conseguir entender o estado atual da interface sem nenhuma instrução prévia.
- RNF-09 O popup deve ter largura fixa de 400px — definida por
width: 400pxemmain.css.
- RNF-10 Cada módulo deve ter responsabilidade única e ser testável de forma isolada. A suíte de testes deve cobrir todos os módulos de negócio individualmente (unit) e o fluxo de mensagens completo (integration).
- RNF-11 Todo o código deve ser escrito em TypeScript com tipagem estrita —
"strict": trueconfigurado emtsconfig.json. - RNF-12 A UI não deve chamar a Chrome API diretamente — toda comunicação passa pelo background via
chrome.runtime.sendMessage. - RNF-19 A cobertura de testes deve respeitar os thresholds mínimos configurados em
jest.config.js: 80% linhas, 80% statements, 75% funções e 70% branches.
- RNF-13 A extensão deve seguir a especificação Manifest V3 do Chrome.
- RNF-14 Deve ser compatível com Google Chrome versão 88 ou superior.
- RNF-15 Sem dependências externas de runtime — apenas APIs nativas do Chrome e do navegador.
- RNF-16 Erros em qualquer módulo devem ser capturados e retornados como
ActionResult.success = false(tipoActionResultemcommon.types.ts), sem propagar exceções para a UI. - RNF-17 O log de recuperação deve expirar graciosamente após 15 minutos sem intervenção do usuário.
- RNF-18 A ausência de favicon não deve impedir a renderização de uma aba na lista.
Os casos de uso descrevem as interações entre o Usuário (único ator do sistema) e o TDE. Há 4 casos de uso principais.
- Usuário clica em "Analisar Abas".
- Sistema exibe a lista de domínios agrupados.
- Usuário seleciona o domínio desejado.
- Sistema exibe opções de escopo.
- Usuário escolhe "Todas as janelas".
- Sistema lista todas as abas do domínio.
- Usuário clica em "Fechar todas as abas".
- Sistema fecha as abas e exibe confirmação com opção de recuperação.
- Usuário seleciona o domínio e escolhe "Escolher janelas".
- Sistema exibe cards de janelas com a contagem de abas de cada uma.
- Usuário marca as janelas desejadas via checkbox.
- Botão "Continuar" exibe o total de abas que serão afetadas.
- Usuário clica em "Continuar".
- Sistema lista as abas das janelas selecionadas.
- Usuário clica em "Fechar todas as abas".
- Sistema fecha apenas as abas das janelas escolhidas.
- Usuário não marca nenhuma janela.
- Botão "Continuar" permanece desativado.
- Usuário deve selecionar ao menos uma janela para prosseguir.
- Usuário localiza a aba desejada na lista.
- Usuário clica no botão × ao lado da aba.
- Sistema remove a aba imediatamente da lista e fecha no Chrome.
- Usuário abre a extensão. A tela inicial exibe o botão "Recuperar" com tempo restante.
- Usuário clica em "Recuperar".
- Sistema recria todas as abas fechadas nas janelas originais.
- Log de recuperação é apagado imediatamente.
- Sistema exibe confirmação do número de abas restauradas.
- 15 minutos se passaram desde o fechamento.
- O botão "Recuperar" não é exibido na tela inicial.
- A recuperação não está mais disponível.
Diagramas visuais dos casos de uso e do fluxo de estados da interface.
Relacionamento entre o único ator (Usuário) e os 4 casos de uso do sistema.
Estados da UI e transições possíveis durante a interação do usuário.
chrome.tabs.query({}).tab-reader.tsurl-normalizer.tstab-grouper.tsaction-executor.tsrecovery-manager.tsmessage-dispatcher.ts
tab-reader.tstab-grouper.tsaction-executor.tsrecovery-manager.tsmessage-dispatcher.ts
chrome.tabs.remove(). Sem log de recuperação.action-executor.tsmessage-dispatcher.ts
chrome.runtime.sendMessage.chrome.tabs.create(). Log apagado imediatamente.recovery-manager.tsaction-executor.tsmessage-dispatcher.ts
Arquitetura em camadas com dependências unidirecionais — camadas externas dependem das internas. A UI nunca acessa a Chrome API diretamente; toda comunicação passa pelo Background Service Worker via chrome.runtime.sendMessage.
popup/background/modules/shared/manifest.json
tabs), service worker, action/popup e ícones da extensão.background/
background/background.ts
chrome.runtime.onMessage e inicializa o dispatcher.background/message-dispatcher.ts
modules/
modules/tab-reader/tab-reader.ts
chrome.tabs e chrome.windows. Retorna dados brutos sem transformação.modules/normalizer/url-normalizer.ts
chrome://, chrome-extension:// e about:; retorna null para URLs inválidas.modules/grouper/tab-grouper.ts
DomainGroup[] (por domínio) e WindowGroup[] (por janela dentro de cada domínio).modules/executor/action-executor.ts
chrome.tabs.modules/recovery/recovery-manager.ts
setTimeout.popup/
popup/popup.html
popup.ts.popup/popup.ts
popup/styles/main.css
shared/
shared/types/
Tab, DomainGroup, WindowGroup, ActionResult, RecoveryLog, mensagens e estado da UI. Sem lógica.shared/constants/ · shared/utils/
popup.ts| Módulo | Arquivo | Responsabilidade | Chrome API |
|---|---|---|---|
| UI Layer | popup.ts |
Renderiza fluxo, captura eventos, envia mensagens | Não |
| Dispatcher | message-dispatcher.ts |
Roteamento, validação e orquestração de mensagens | Não diretamente |
| Tab Reader | tab-reader.ts |
Leitura de abas e janelas — abstração da API | Sim |
| Normalizer | url-normalizer.ts |
Extrai domínio base de URLs (string | null) |
Não |
| Grouper | tab-grouper.ts |
Agrupa abas em DomainGroup[] e WindowGroup[] |
Não |
| Executor | action-executor.ts |
Fecha e restaura abas via chrome.tabs |
Sim |
| Recovery | recovery-manager.ts |
Log temporário em memória (TTL 15 min) | Não |
O TDE não tem nenhuma dependência de runtime — apenas devDependencies. Tudo que chega ao usuário final é código puro bundlado pelo Webpack.
chrome.tabs, chrome.windows e chrome.runtime. Permissão "tabs" no manifest.
ES2020 com strict: true. Todos os módulos totalmente tipados.
background.ts e popup.ts. Output em dist/.
manifest.json, HTML, CSS e assets para dist/ sem transformação.
node. Configurado em config/jest.config.js.
tsconfig.json do projeto.
describe, it, expect, jest.fn() e afins.
@typescript-eslint. Configurado em config/eslint.config.js.
config/.prettierrc.
O
package.json contém apenas devDependencies. O bundle final entregue ao Chrome é composto apenas por código JavaScript puro gerado pelo Webpack — sem bibliotecas externas na extensão.
Guia para configurar o ambiente de desenvolvimento, entender a estrutura do projeto e contribuir com o código.
| Comando | Descrição |
|---|---|
npm run dev | Build em modo desenvolvimento com watch — recompila ao salvar |
npm run build | Build de produção em dist/ (minificado) |
npm test | Executa todos os testes (unit + integration) |
npm run test:watch | Testes em modo watch |
npm run test:coverage | Testes com relatório de cobertura |
npm run test:integration | Executa apenas os testes de integração |
npm run lint | Verifica erros de lint no TypeScript |
npm run lint:fix | Corrige automaticamente erros de lint |
npm run format | Formata o código com Prettier |
npm run format:check | Verifica formatação sem modificar os arquivos |
npm run clean | Remove dist/ e coverage/ |
npm run dev
src/
O Webpack recompila automaticamente ao salvar. Os arquivos em dist/ são atualizados.
Acesse chrome://extensions e clique no ícone 🔄 ao lado do TDE para recarregar.
Clique no ícone da extensão para abrir o popup e testar o comportamento esperado.
npm test
| Suite | Tipo | O que cobre | Testes |
|---|---|---|---|
normalizer.test.ts | Unit | normalizeUrl, extractDomain — URLs válidas, www., chrome:// | 4 |
tab-reader.test.ts | Unit | getAllTabs, getAllWindows, getTabsByWindowId — mock de chrome.tabs e chrome.windows | 6 |
grouper.test.ts | Unit | groupByDomain, groupByWindow, filterTabsByScope — agrupamento, ordenação, filtragem por escopo | 14 |
executor.test.ts | Unit | closeSingleTab, closeBulkTabs, restoreTabs — sucesso, falha e sucesso parcial | 10 |
recovery.test.ts | Unit | storeRecoveryLog, getRecoveryLog, hasRecoverableAction, clearRecoveryLog — TTL com fake timers | 12 |
dispatcher.test.ts | Integration | Todos os 7 MessageType via handle() — validação de payload, roteamento e respostas compostas | 16 |
| Total | 62 | ||
npm run test:coverage)ES2020Chrome 88+ suporta plenamentetrueTipagem estrita obrigatóriaESNextBundled pelo WebpacktruePara debugging em dev./srcAlias @/ aponta para src/["chrome", "node", "jest"]Tipos globais disponíveis