Tentacruel
Multiplexador de worktrees para o Claude Code.
Cada sessão de chat = um git worktree novo + uma branch nova + um agente Claude Code
com cwd fixado naquele worktree. Três conversas no mesmo repositório nunca
enxergam os arquivos umas das outras, porque cada uma está fisicamente em um
diretório diferente.
App web local, single-user, sem auth. Roda em localhost.
┌──────┬─────────────────────────────┬──────────────┐
│ rail │ chat da sessão │ Arqs | Diff │
│ de │ (streaming + input fixo) │ + commit / │
│ sess.│ │ push │
└──────┴─────────────────────────────┴──────────────┘
Instalação
uv tool install tentacruel
tentacruel
O Tentacruel é uma aplicação Python que serve a própria interface, então o
"executável" é um pacote e não um .app.
Não há assinatura nem notarização da Apple envolvidas, e isso é escolha e não
omissão. Um .app baixado de um site precisa de conta de desenvolvedor paga
e de notarização; sem elas, o Gatekeeper avisa sobre malware em toda instalação.
Um pacote instalado por uma ferramenta que a pessoa já usa não passa por esse
caminho — e o público deste app já instalou o CLI do Claude Code pelo terminal.
Como app do macOS, com ícone
tentacruel install-app
Cria o Tentacruel.app em ~/Applications — ele aparece no Launchpad e no
Spotlight, e pode ir para o Dock. Clicar nele sobe o servidor e abre uma janela
própria, sem barra de endereço: o WebKit é o do próprio macOS, então nada vem
embutido e nenhum navegador precisa estar instalado.
Isso continua sem envolver a Apple, e não por descuido. O Gatekeeper age sobre o atributo de quarentena, que o navegador põe no que você baixa de um site. Um bundle gerado na sua máquina pelo pacote já instalado nunca recebe esse atributo: não há o que notarizar, nem conta de desenvolvedor a pagar.
~/Applications e não /Applications de propósito — a pasta do sistema exige
autenticação de administrador, e instalação que pede senha é instalação que
parte das pessoas abandona.
Para remover, apague a pasta:
rm -rf ~/Applications/Tentacruel.app
E tentacruel --window abre a mesma janela sem criar app nenhum, se você quiser
só experimentar.
Pré-requisitos
| O quê | Por quê | Como conferir |
|---|---|---|
| macOS | único alvo do MVP | — |
| Claude Code instalado e logado | o agente usa a sua assinatura; não há ANTHROPIC_API_KEY |
claude --version e, se preciso, claude → /login |
| git 2.30+ | worktrees são o produto inteiro | git --version |
| uv | é quem instala, e traz o Python junto | uv --version |
Node não está na lista de propósito: a interface já vem compilada dentro do pacote. Ele só é necessário para trabalhar no código, e está em Desenvolvimento.
O agente autentica pelo login existente do Claude Code. Tentacruel nunca lê, pede ou
configura ANTHROPIC_API_KEY.
Aviso de atualização
O app consulta o índice de pacotes no máximo a cada 6 horas e, quando há versão mais nova, ela aparece em Configurações → Sobre. Toda falha é silenciosa: não saber se há atualização não é problema que mereça ir para a tela de alguém.
TENTACRUEL_CHECK_FOR_UPDATES=false desliga — e desligado, este app não fala
com nada além do serviço de coordenação do próprio usuário.
Uso em 60 segundos
- Registrar um repo — ícone de pasta no rodapé do rail → Escolher pasta no Finder (abre o diálogo nativo do macOS) ou cole o caminho absoluto. Qualquer subdiretório serve; o Tentacruel resolve a raiz.
- Criar uma sessão — botão
+(ou⌘N): nome, branch base e modo de permissão. O nome vira o slug:Refatorar Auth→ branchwt/refatorar-auth, worktree.tentacruel/worktrees/refatorar-auth. - Conversar — cada painel é um agente independente. Vários podem trabalhar em paralelo; a bolinha no rail pulsa enquanto o agente executa.
- Revisar — aba Diff à direita mostra o que ainda não foi commitado e o que já está commitado na branch da sessão. Commit e push manuais ficam no rodapé do painel.
- Encerrar — três passos, cada um um pouco mais longe que o anterior: Arquivar (para o agente, preserva tudo — e Reabrir traz de volta), Apagar o worktree (some com o diretório e, por consequência, arquiva a sessão) e Excluir (apaga a sessão do Tentacruel). A branch sempre permanece no git.
Atalhos
| Atalho | Ação |
|---|---|
⌘1…⌘9 |
troca para a n-ésima sessão ativa |
⌘N |
nova sessão |
⏎ / ⇧⏎ |
envia / quebra linha no composer |
⌘Né reservado pelo Chrome e pelo Safari para "nova janela" e pode não chegar à página. O botão+do rail faz o mesmo.
Como o isolamento funciona
Ao criar uma sessão o backend executa:
git -C <repo> worktree add <repo>/.tentacruel/worktrees/<slug> -b wt/<slug> <base>
e instancia o agente com cwd = <worktree>. Três camadas garantem o isolamento:
- Filesystem — worktrees são checkouts separados. O agente da sessão A literalmente não tem os arquivos da sessão B no seu diretório.
cwddo SDK — o processo do Claude Code nasce dentro do worktree; caminhos relativos resolvem lá.- Permissões — em
acceptEdits, o CLI recusa escritas fora do diretório de trabalho (verificado: uma tentativa de escrever em~/foi negada).
.tentacruel/ é adicionado automaticamente ao .git/info/exclude do repositório —
o seu .gitignore nunca é tocado.
Atenção:
.git/info/excluderesolve o git, mas não ferramentas que varrem o diretório por conta própria. Rodarpytest,ruff,eslintou um watcher na raiz do repo passa a enxergar cópias dos seus arquivos em.tentacruel/worktrees/*. Se isso incomodar, exclua o diretório na config da ferramenta — por exemplonorecursedirs = [".tentacruel"]no pytest,extend-exclude = [".tentacruel"]no ruff,ignores: [".tentacruel"]no ESLint. Se preferir os worktrees fora do repo, aponteTENTACRUEL_WORKSPACE_DIRNAMEpara outro nome ou ajusteworktrees_root()emservices/worktree.py— o caminho é calculado num lugar só.
Seletor de pasta
O botão Escolher pasta no Finder existe porque o browser não consegue
entregar um caminho absoluto — nem webkitdirectory nem a File System Access
API expõem isso. Como o backend roda na sua própria máquina, ele abre o diálogo
nativo via osascript (choose folder) e devolve o POSIX path.
Consequências:
- Só funciona no macOS. Fora dele o endpoint devolve
501e sobra o campo de texto. - Uma janela por vez: um segundo pedido recebe
409enquanto a primeira estiver aberta. - Se você fechar a aba com o diálogo aberto, ele fica órfão até o timeout de 3 minutos. Basta responder à janela do Finder que ainda está na tela.
Configurações
Ícone de ajustes no rodapé do rail. Tudo fica no banco, então sobrevive a trocas de browser.
| Seção | O que faz |
|---|---|
| Repositórios | registra e remove repos, e define provedor/modelo/esforço padrão por repositório — herdados por sessões novas daquele repo |
| Provedores de IA | lista os CLIs: instalado ou não, versão, caminho, como logar. Botão Instalar roda o comando com a saída transmitida ao vivo |
| Aparência | tema, densidade e layout (posição do rail e do painel) |
| Dependências | git, Node, uv e npm com versão, caminho e o porquê de cada um |
| Idioma | português, inglês e espanhol |
| Sobre | versão, caminhos, atalhos e a notificação de fim de tarefa |
Temas
Cinco temas — Escuro, Carvão puro (AMOLED), Brasa, Claro e Papel/Sépia — são
conjuntos de tokens, não folhas de estilo paralelas. Cada um é um bloco
[data-theme="…"] em frontend/src/styles/tokens.css que sobrescreve as
variáveis; nenhum componente sabe que temas existem.
O seletor prova isso: cada miniatura carrega data-theme próprio, então é
o tema que anuncia — desenhada com os mesmos tokens que o app usa. Não há uma
segunda definição de cor para manter em dia.
Adicionar um tema = adicionar um bloco em tokens.css e uma linha na lista de
AppearanceSettings.
Densidade
Um multiplicador (--wt-density) alimenta espaçamento, tipografia e as
dimensões de layout. De 70% a 140%, em passos de 5%.
Idioma
Português (Brasil), inglês e espanhol. O catálogo de pt-BR é a fonte: os outros idiomas são tipados contra ele, então uma chave faltando quebra o build em vez de virar texto vazio em produção.
Adicionar um idioma são dois passos: copiar frontend/src/i18n/locales/en.ts,
traduzir os valores, e somar uma entrada em LOCALES (frontend/src/i18n/index.ts).
Nenhuma tela muda.
O que ficou de fora: o Carcará oferece 18 idiomas. Eu parei em três — os que consigo revisar. Escrever mais 15 traduções que não sei avaliar geraria texto de qualidade desconhecida na sua interface; a infraestrutura está pronta e eu adiciono qualquer idioma que você pedir, sinalizando quais não posso revisar.
Instalação de provedores
O botão Instalar abre /ws/install/{provider} e transmite a saída do
comando. O comando vem do registry no backend — o cliente só diz qual
provedor, nunca o que rodar.
Instaladores rodam sem terminal interativo (stdin fechado, CI=1,
npm_config_yes=true). Um instalador que insista em perguntar falha rápido em
vez de travar para sempre; nesse caso o comando aparece na tela para você rodar
no seu terminal. Timeout de 15 minutos, e o processo morre por process group.
Provedor e modelo por worktree
Cada sessão escolhe seu próprio provedor de IA, modelo e nível de esforço — no modal de criação e no header, trocável a qualquer momento.
| Controle | O que faz | Como aplica |
|---|---|---|
| Provedor | qual backend de IA roda neste worktree | trocar reinicia o agente (históricos não são portáveis entre provedores) |
| Modelo | Padrão, opus, sonnet, haiku, fable |
ao vivo, sem reiniciar nem perder a conversa (set_model() do SDK) |
| Esforço | auto, low…max |
reinicia o agente e retoma via resume, então o contexto sobrevive |
São aliases, não ids fixos: opus sempre aponta para o Opus mais recente, então
a lista não envelhece. Um id completo (claude-opus-4-5-20251101) também é
aceito. Padrão = o que o seu CLI já estiver configurado para usar.
Rodar haiku numa sessão de documentação e opus numa refatoração, em
paralelo no mesmo repo, é o caso de uso.
Autenticação: sempre a sua assinatura
Nenhum provedor usa API key. Cada backend dirige o CLI oficial do provedor,
que já carrega o seu login — hoje é o claude (assinatura do Claude Code). Um
provedor futuro seguirá a mesma regra: o CLI dele, o login dele, a sua conta.
O Tentacruel nunca lê, pede ou repassa ANTHROPIC_API_KEY ou equivalente.
O catálogo em GET /api/providers diz, por provedor, se o CLI está instalado
(available) e como logar (auth_hint, install_hint). O que não está
instalado aparece desabilitado na UI, com a instrução de instalação no tooltip.
Adicionando um provedor (o que falta)
A arquitetura está pronta, mas só o Claude Code está implementado — nenhum outro CLI de agente estava instalado nesta máquina para eu construir e testar contra. Para somar um:
- Escreva
services/agents/<provider>.pyimplementando o protocoloAgentBackend(start,send,interrupt,set_permission_mode,set_model,stop) e emitindo os dataclasses deagents/base.py. - Adicione um
ProviderSpecemservices/agents/registry.py.
Só isso. O manager, as rotas, o WebSocket, o banco e o frontend inteiro leem o
catálogo — nenhum deles precisa mudar. O ponto crítico de qualquer adapter é
fixar o cwd no worktree: é o que sustenta o isolamento.
Navegador de arquivos
Aba Arquivos no painel lateral — a primeira, e a que o painel abre. É o que se procura primeiro; o diff só interessa depois que o agente escreveu alguma coisa. Árvore preguiçosa — um diretório por chamada — então um repositório grande custa o mesmo que um pequeno.
Ditado por voz
O microfone na caixa de mensagem grava, transcreve e põe o texto no campo — não envia. Transcrição erra, e ler antes de mandar é o ponto.
Quem transcreve é um CLI da sua máquina, pelo mesmo princípio dos provedores
de IA: o Tentacruel dirige o que já está instalado em vez de embutir motor
próprio. Embutir o openai-whisper arrastaria o torch junto; o faster-whisper
arrastaria o CTranslate2 — e os dois baixam modelo no primeiro uso. Um
multiplexador de worktrees não tem por que pesar um giga por causa de um
microfone.
services/transcribe.py conhece dois:
| CLI | instalar | observação |
|---|---|---|
whisper-cli (whisper.cpp) |
brew install whisper-cpp |
o mais rápido em Apple Silicon, mas precisa de um modelo ggml — procurado em ~/.cache/whisper.cpp e nos diretórios do brew |
whisper (OpenAI) |
brew install openai-whisper |
mais lento, porém baixa o próprio modelo: funciona assim que instala |
Sem nenhum deles pronto, o microfone não aparece — botão que não funciona é pior que botão ausente. Configurações → Transcrição de voz mostra o estado de cada um e o comando para instalar.
A caixa ganha borda de destaque quando o campo está em foco — cor primária com um anel fino, em vez de só um cinza mais forte.
O áudio é gravado como WAV mono de 16 kHz no próprio navegador
(lib/recorder.ts), que é exatamente o que o whisper.cpp exige e o que o
whisper aceita — por isso nem o app nem você precisam de ffmpeg. No servidor
ele vai para um arquivo temporário, passa pelo CLI e some: recado de voz não é
coisa para deixar no disco.
Anexos
A caixa de mensagem é uma pilha, e as medidas fecham:
4px padding do cartão (--wt-composer-padding)
54px campo de texto (--wt-composer-field-height, com 8px de padding)
32px faixa de controles (--wt-composer-bottom-height, um botão de ícone)
4px padding do cartão
────
94px (--wt-composer-height)
O campo tem caixa própria e mais folgada que o resto — 8px contra 4px —
porque é a parte em que se escreve. Entre ele e os controles não há régua:
os dois são a mesma caixa. A régua de 0.5px existe só acima da faixa de anexos,
que separa coisas de naturezas diferentes; essa faixa tem 28px
(--wt-strip-height), a mesma da linha de stats acima do cartão, e rola para o
lado em vez de empurrar o texto para baixo.
Todo botão de ícone do app é 32×32 (--wt-icon-button-size) com um glifo
de 18px (--wt-icon-glyph-size) centralizado nele. O respiro é o que
sobra — 7px de cada lado — então não existe padding para manter em sincronia
com nada.
A medida vive na variante icon do botão, não nos pontos de uso: IconButton
não tem prop de tamanho, e o iconLg que existia foi eliminado.
Cole (⌘V), arraste ou escolha pelo clipe. Tudo vai para
<worktree>/.tentacruel/attachments/, e essa escolha responde três perguntas de uma
vez: o agente alcança o arquivo, porque está dentro do próprio cwd — a
mesma fronteira de todo o resto, sem exceção aberta para upload; o git nunca
vê, porque .tentacruel/ já está no .git/info/exclude; e some junto com o
worktree, porque mora dentro dele.
Imagem viaja inline, como bloco base64 na mensagem do usuário: o agente enxerga a figura sem gastar uma chamada de ferramenta nela. Qualquer outro arquivo entra como caminho, para o agente ler com a mesma ferramenta que usa para o resto do worktree.
As dimensões saem do cabeçalho do próprio arquivo (PNG, JPEG, GIF, WebP) — o
app não carrega biblioteca de imagem para dizer 234×49. O caminho é
relativo ao worktree, então a miniatura no chip é servida pelo endpoint de
arquivo bruto que já existia: nenhum endpoint novo para desenhar o anexo.
O cliente manda caminhos; quem manda é o disco. Um caminho inventado, ou que aponte para fora do worktree, é descartado antes de virar mensagem.
O deslize
Todo elemento que muda de tamanho em passo discreto desliza em 400ms
(--wt-slide-duration), por dois primitivos com papéis distintos:
Collapse— para o que aparece e some: uma faixa de grid de0fra1fr, linhas para altura e colunas para largura. É o único jeito de transicionar até um tamanho vindo do conteúdo sem medir em JS antes.ResizeSlide— para o que fica e muda de tamanho. O CSS não dá conta aqui: transição dispara em mudança de valor computado, e conteúdo crescendo sobwidth: autonunca muda o valor — continuaauto. Então ele fixa a largura antiga, solta até a nova e devolve paraauto.
A diferença importa: um elemento que já está na tela e muda de tamanho deve mover só a diferença. Fazê-lo sumir e renascer com a transição é um efeito diferente, e pior.
Fechar uma aba também desliza. O React remove um elemento no instante em que ele
deixa de ser renderizado, então sair com deslize é pedir para sair, animar, e
só então ir de fato: o botão de fechar marca a aba como saindo, ela encolhe
até zero, e o onClose — que apaga o chat ou fecha o arquivo — dispara no fim.
Desliza: a faixa de anexos, a régua abaixo dela, as pastas da árvore, o raciocínio e a saída de ferramenta, o botão de "commits atrás", o lápis da aba, e a aba de chat ao entrar e sair da renomeação.
Não desliza, de propósito: o balão de mensagem enquanto o agente escreve e o campo de texto enquanto você digita. Os dois mudam de tamanho de forma contínua, token a token e tecla a tecla; uma transição de 400ms ali faria o texto ficar permanentemente atrás do cursor. Animar mudança discreta dá a sensação de acabamento que você quer; animar mudança contínua faz o app parecer lento.
Pastas abrem e fecham deslizando em 400ms. O diretório é carregado antes de expandir: as linhas precisam já estar dentro do invólucro fechado para o slide ter o que mover — expandir primeiro faria a primeira abertura saltar enquanto todas as seguintes deslizam.
Clicar abre uma aba, e o que aparece depende do arquivo:
| tipo | o que abre |
|---|---|
| texto | editor, com contador de linhas e ⌘S |
imagem (.png, .jpg, .gif, .webp, .avif, .svg, .ico…) |
prévia sobre xadrez de transparência, com tamanho e dimensões |
.pdf |
o leitor nativo do navegador |
áudio (.mp3, .wav, .ogg, .m4a…) e vídeo (.mp4, .mov, .webm…) |
player com controles |
| resto | tipo e tamanho, sem fingir que dá para exibir |
O texto viaja dentro do JSON; mídia não. A página recebe só tipo, tamanho e
uma URL, e o navegador busca os bytes em /file/raw — por isso um vídeo de
20 MB abre sem inchar resposta nenhuma. O limite de 1 MB é dos arquivos que
entram na página; mídia vai até 64 MB.
Um .svg é desenho e o código que o desenha, então essa aba oferece os
dois: um botão alterna entre prévia e fonte, e na fonte o ⌘S volta. Um PDF
pequeno também decodifica como ASCII, mas "editar" ali é um jeito de corromper
o arquivo — quem decide é o tipo declarado, nunca a aparência dos bytes.
Só o que a tabela declara sai com o próprio Content-Type. Qualquer outra
coisa vai como application/octet-stream e attachment, então um .html
perdido no worktree nunca vira página rodando na origem do app.
- O git decide o que é ruído. A listagem passa por
git check-ignore, entãonode_modules,diste afins não aparecem..gitnunca é listado. - Arquivos alterados vêm marcados com o código do
git status(M,??,A), coloridos na árvore — o mesmo estado que a aba Diff mostra. - Clicar num arquivo abre ele como uma aba, ao lado das conversas, e o
arquivo é editável:
⌘S(ouCtrl+S) salva,Tabinsere dois espaços, e a aba mostra um ponto enquanto houver alteração não salva. O rascunho vive no estado da tela, então trocar de aba e voltar não perde o que você escreveu. - Ícones por tipo de arquivo vêm do Material Icon Theme (o mesmo do VS
Code). Só os SVGs mapeados em
ui/fileIcons.tsentram no bundle, e o Vite os transforma em data URI — nenhuma requisição extra em tempo de execução.
O editor é sem coloração de sintaxe — destacar sintaxe significaria embarcar um pacote de gramáticas. Ele tem numeração de linha e edição de texto puro.
Salvar tem as mesmas guardas de ler: o caminho é resolvido contra a raiz do worktree, arquivos binários são recusados, e não é possível criar arquivo novo por aqui — o editor abre sobre um arquivo que já existe.
O mesmo limite do agente
Todo caminho vindo do cliente é resolvido e conferido contra a raiz do worktree
antes de ser tocado. ../, caminho absoluto e symlink que aponta para fora são
recusados — inclusive o worktree da sessão vizinha. A sessão só enxerga o que o
agente dela enxerga, e há teste para cada uma dessas rotas de fuga.
Campo de mensagem
No estilo do Claude: um cartão arredondado único que segura o texto e os controles. Nada em volta pinta fundo ou borda — o composer flutua sobre o chat em vez de ficar numa barra preenchida.
Dentro do cartão, à direita, ficam o seletor de modelo e o de esforço, mais o botão de enviar. O header da sessão guarda o que é escolha de sessão (provedor e modo de permissão); o modelo mora onde você escreve, como no Claude. Um lugar por conceito — o modelo não aparece duas vezes.
Abrir um worktree que já existe
Nem todo worktree nasce aqui. Um pode ter vindo do git worktree add na mão, de
outra ferramenta (o --worktree do próprio Claude Code cria em
.claude/worktrees/), ou ter ficado para trás quando uma sessão foi excluída
sem apagar o diretório. A caixa de nova sessão oferece adotar esses.
O seletor "Como começar" só aparece quando há algo a adotar. Ao escolher Abrir um existente, a branch base some — um worktree que já existe já está numa branch, não há de onde cortá-lo — e o nome vira opcional: sem nome, usa o do diretório.
Adotar não provisiona nada: o diretório, a branch e o que está dentro já existem. Só ganham uma sessão para um agente ser apontado a eles. Ficam de fora da lista os worktrees que já têm sessão e o checkout principal do repositório — apontar um agente para a cópia de trabalho do usuário é exatamente o que este app existe para evitar.
Voltar para o fim
Quando a conversa não está no fim, um botão redondo aparece no rodapé dela e leva de volta à última mensagem. Some sozinho quando você chega lá, e enquanto está escondido não recebe clique — senão engoliria o toque na mensagem embaixo dele.
A mesma medida — "estou a menos de 80px do fim?" — governa duas coisas: esse
botão e o auto-scroll que segue o agente enquanto ele escreve. A diferença é que
o auto-scroll lê de um ref, para não re-renderizar a cada mensagem, e o botão
lê de um estado, porque precisa re-renderizar.
Manter o worktree em dia
Um worktree cortado de develop fica para trás enquanto o agente trabalha, e
quanto mais tempo passa, pior o merge no fim. Isso acontece sozinho: quando
a base ganha commits, o worktree os recebe — a branch e os arquivos no disco ao
mesmo tempo, porque o merge roda dentro do worktree.
Merge pequeno quase nunca conflita, e é essa a aposta: o que a sincronização contínua elimina é a divergência que envelheceu. A divergência concorrente — duas pessoas na mesma função ao mesmo tempo — nenhuma ferramenta resolve; para essa existe o alerta de colisão.
O header mostra o estado, e são quatro:
| estado | o que significa |
|---|---|
| (nada) | em dia com a base |
↓3 atrás |
há commits a trazer; o botão ainda faz na mão, parando os agentes antes |
aguardando |
há trabalho não commitado ou um agente no meio de um turno |
em conflito |
o merge foi tentado e desfeito inteiro — e há um botão para pedir ao agente daquele worktree que resolva |
Ele espera, não interrompe. O caminho manual para os agentes antes de
mesclar, que é certo para algo que você apertou. Para algo que dispara sozinho,
matar um turno no meio por um merge de rotina não é — então a sessão fica
aguardando e sincroniza quando ficar quieta.
Três coisas disparam a verificação, e duas delas já eram eventos que existiam:
o fim de um turno, um commit (que também avisa quem foi cortado daquela
branch — é isso que faz empilhamento funcionar sem remote nenhum), e um
relógio a cada TENTACRUEL_SYNC_INTERVAL segundos (60 por padrão, 0
desliga). O relógio existe porque um git pull na main no seu terminal não
produz evento nenhum que este app consiga ouvir — e esse é o caso comum, não o
exótico.
O número é rev-list --left-right --count <base>...HEAD — o mesmo cálculo que
alimenta o ↓ do painel. O botão só aparece quando há algo a trazer.
Merge, não rebase. Rebase reescreveria commits que o agente talvez já tenha enviado, e um rebase interrompido no meio deixa um worktree — em que um agente está prestes a escrever — num estado que ninguém pediu. Merge ou acontece inteiro, ou é desfeito inteiro.
Conflito desfaz o merge. Se der conflito, o merge --abort roda e os
arquivos que bateram são nomeados. Deixar marcadores de conflito num worktree
que um agente vai ler é entregar a ele um arquivo que parece código e não é.
Mudanças não commitadas bloqueiam. O merge reescreve arquivos; trabalho em voo seria pego no meio. O botão fica desabilitado e o tooltip diz o porquê.
No caminho manual, os agentes param antes — pela mesma razão da troca de
branch: os arquivos vão mudar debaixo do cwd deles, e quem apertou o botão
pediu por isso. O daemon faz o contrário: nunca para ninguém, e por isso só age
quando já não há ninguém trabalhando.
Quem mais está neste arquivo
Sincronizar não resolve duas pessoas dentro do mesmo arquivo ao mesmo tempo — só duas pessoas resolvem. O que dá para fazer é dizer cedo, enquanto ainda é uma conversa e não um merge.
O header mostra um chip quando outra sessão do mesmo repositório está mexendo nos mesmos arquivos, e o tooltip nomeia a sessão e os arquivos. Conta o que foi commitado na branch e o que ainda está solto no worktree: um arquivo já commitado colide tão forte quanto um que está no meio da edição.
Os dois lados são avisados. Aviso que só uma das duas pessoas vê começa discussão, não conversa.
A conta é barata porque todos os worktrees estão no mesmo disco sob um único banco de objetos: dois comandos git por sessão viva, refeitos nos mesmos momentos em que a sincronização é verificada.
Sessões e chats
Uma sessão é um worktree. Um chat é uma conversa com um agente rodando dentro dele. Várias conversas podem dividir o mesmo worktree — como várias abas de terminal no mesmo diretório — cada uma com seu próprio agente, histórico, modelo e modo de permissão.
O isolamento continua onde sempre esteve: entre sessões, no filesystem. Dentro de uma sessão, os chats compartilham os arquivos de propósito.
Chat nasce com nome automático (Chat 2, Chat 3…), que não diz nada quando
há quatro deles. O lápis na aba aberta renomeia no lugar — e duplo clique
também, para quem já sabe. Enter ou sair do campo salva, Escape descarta, e
nome em branco mantém o antigo em vez de deixar uma aba sem nome. O campo é do
tamanho do que está escrito, então a aba não pula nem abre buraco enquanto se
digita.
O lápis aparece só na aba aberta: em todas seriam quatro elementos por chat disputando espaço com os nomes, e ali ele fica onde o olho já está.
O header da sessão
A coluna do chat abre com duas faixas:
| faixa | altura | conteúdo |
|---|---|---|
| 1 | 64px fixos | ● nome da sessão · repositório · branch · caminho do worktree |
| 2 | a das abas | as abas desta sessão — conversas (duplo clique renomeia) e arquivos abertos — com + para abrir outra conversa |
A segunda faixa acompanha as abas em vez de ocupar outros 64px, então não sobra espaço morto acima delas. Só a faixa 1 tem altura fixa.
As opções da sessão ficam no canto superior direito da conversa, posicionadas de forma absoluta: não reservam espaço, o thread ocupa a altura inteira da coluna e elas ficam por cima.
À esquerda da barra fica o seletor de branch: qual branch este worktree
está segurando. Trocar move o checkout — os agentes são parados antes, porque
eles têm um cwd cujo conteúdo está prestes a mudar, e um turno que começa numa
branch e termina em outra é um bug que ninguém consegue ler.
Uma branch que outro worktree já segura não aparece: o git recusa a mesma
branch em dois worktrees, e linha que não dá para escolher é ruído num seletor.
O backend ainda a reporta, com taken_by, para quem precise saber. Quem decide se a troca é segura é o
próprio git switch — ele carrega mudanças limpas junto e recusa quando elas
seriam perdidas. Reimplementar esse julgamento aqui seria só uma segunda cópia,
pior, dele.
Não ocupar espaço é o que criava o encavalamento — o grupo tem 355px e a coluna
de leitura tem 848px centralizados, então em qualquer largura ele cobria parte
do texto. Duas coisas resolvem, sem devolver espaço ao layout: a conversa
reserva --wt-floating-clearance (52px) no topo, para nenhuma mensagem
começar debaixo dos botões, e a barra desenha um fade sob si — sólido na
altura dos botões, esvaindo no fim — para o que rola por baixo ler como
passando sob vidro em vez de bater neles. Sem cartão nem moldura própria
— os dois dropdowns (provedor e modo de permissão) são bare, do mesmo jeito.
Os três ícones à direita são uma escada, e o ícone diz qual degrau:
| ícone | ação | o que acontece |
|---|---|---|
| caixa de arquivo | Arquivar / Reabrir | para o agente; worktree e branch ficam. Reabrir é o caminho de volta que o arquivamento promete — só possível enquanto o diretório existir |
| pasta com X | Apagar o worktree do disco | git worktree remove --force. A sessão passa a arquivada junto: sem diretório não há onde o agente rodar, e a caixa de confirmação diz isso |
| lixeira | Excluir sessão | tira sessão e histórico do Tentacruel, e apaga o worktree |
Nenhum deles é um menu de reticências — ··· promete "mais opções", e usá-lo
para apagar diretório era mentira de ícone.
Só o chat tem as duas. O rail e o painel mantêm uma faixa de 64px — o rail com o botão de nova sessão, o painel com as abas Arquivos / Diff / Setup.
O rodapé segue a mesma régua: o rail fecha com o botão de Configurações e o
painel com a linha de commit/push, ambos em 64px, então a borda inferior
atravessa as colunas sem degrau. O estado da branch (↑1, worktree limpo)
subiu para o topo do painel, ao lado do diff que ele descreve.
A altura é fixa em 64px, sem escalar com a densidade, e vive só em
--wt-header-height. O rail tem 64px de largura, também fixa
(--wt-rail-width), então o canto superior esquerdo é um quadrado de 64×64.
Cada entrada do rail é um quadrado de 46px (--wt-rail-item-size) com o
tile do repositório de 38px (--wt-rail-avatar-size) centralizado nele —
4px de folga nos quatro lados. Os dois são pixels fixos, não passos de
densidade: é um ícone num tamanho escolhido, não texto que acompanha a escala.
Barra de status da sessão
Duas linhas abraçam o campo de mensagem, ambas sem fundo e na mesma coluna
dele, com dados reais do agente em execução. A de cima lê como uma frase —
Sessão: 13% - Contexto: 5% - Semana: 7% - Fable: 1% — e a porcentagem só
ganha cor quando a janela começa a encher:
| Campo | Onde | De onde vem |
|---|---|---|
| % de contexto | acima | client.get_context_usage(), medido contra o limiar de auto-compact — é ele que decide quando a conversa é cortada |
| cota da sessão (5h), da semana e por modelo | acima | a leitura de /usage que o próprio Claude Code guarda em ~/.claude.json |
| usuário e plano | abaixo | oauthAccount do ~/.claude.json — nome, email, organização e plano (default_claude_max_20x → Max 20x) |
| worktree e branch | abaixo | a própria sessão; clique copia |
Contexto e cota falam do turno que está prestes a ser enviado, então ficam encostados na caixa de texto. Usuário e caminho descrevem o ambiente, e ficam embaixo.
De onde saem as porcentagens de cota. O
RateLimitEventdo SDK entregautilizationcomonullpara a maioria das contas — sóstatuseresetsAt. Por esse canal, porcentagem não existe.Mas o
/usagedo Claude Code tem os números, e guarda a resposta em~/.claude.json, sobcachedUsageUtilization— o mesmo arquivo de onde o Tentacruel já lê o perfil. Entãoservices/agents/limits.pylê esse cache e, quando a leitura envelhece (5 min), rodaclaude --print /usagepara o próprio CLI atualizá-lo. O comando é resolvido localmente: não gasta token de modelo. ORateLimitEventdeixou de ser fonte e virou gatilho — ele avisa que a cota mexeu, e aí o número é relido.Nenhuma credencial passa pelo Tentacruel: quem fala com a Anthropic é o CLI, com o login que já estava lá. E toda leitura carrega
limits_fetched_at, que aparece no tooltip — número velho nunca se passa por número vivo.A atualização roda fora do caminho quente: uma tarefa em segundo plano que transmite pelo WebSocket quando termina, porque abrir o CLI leva ~4s e um turno não pode acabar 4 segundos mais tarde por causa de um enfeite.
A leitura da conta é só de exibição: nome, email, plano e organização. O Tentacruel não lê, não guarda e não transmite credencial nenhuma — é o ponto de dirigir o CLI do provedor em vez de usar API key.
Avatar da sessão
O rail é só ícone — sem legenda embaixo, que quebrava o layout com nomes longos. Cada item mostra:
- a logo do repositório, se ele tiver uma. A busca não exige nome exato:
qualquer imagem cujo nome contenha
favicon,icon,apple-touch-iconoulogoconta —trakk_favicon.jpegelogo_whitemode.pngsão encontrados. Diretórios varridos: raiz,public/,static/,app/,src/app/,assets/,src/assets/,.github/,docs/e afins — maispublic/,static/,app/,src/app/dentro de cada pasta de primeiro nível, o que cobre monorepos (front-web/public/favicon.ico,front-admin/src/app/icon.png). Desempate: diretório mais próximo da raiz →faviconantes delogo(um wordmark a 32px é ilegível) → nome exato antes de decorado (logo.svgganha delogo-dark.svg) →svg>png>ico>webp>jpg. Sem logo, cai na inicial do nome da sessão. - a inicial da sessão num chip no canto superior esquerdo, porque várias sessões do mesmo repo mostrariam a mesma logo.
- uma borda colorida derivada do nome da sessão, e o anel azul na ativa.
- o status na bolinha do canto inferior (pulsando quando o agente trabalha).
Passe o mouse para ver nome, branch, caminho do worktree, status, repo e o
atalho ⌘n.
A busca é limitada de propósito: varrer a árvore inteira entraria em
node_modules. node_modules, dist, build, .venv e afins ficam de fora,
assim como arquivos acima de 2 MB e symlinks que apontam para fora do repo.
.tentacruelinclude
Worktree é checkout limpo: arquivos ignorados pelo git (.env e afins) não
existem nele. Crie um .tentacruelinclude na raiz do repo — um glob por linha,
# para comentário (um .wtmuxinclude de antes do rename continua sendo lido
quando o novo não existe):
# segredos e config local
.env
.env.local
config/*.local.json
certs
Cada match é copiado para o worktree novo preservando o caminho relativo.
Padrões absolutos (/etc/...), com .. ou apontando para .git são ignorados
por segurança.
.tentacruel/setup.sh
Se existir, roda dentro do worktree novo logo após a criação — para
npm install, uv sync, symlinks, o que for. A saída aparece ao vivo na aba
Setup. Variáveis disponíveis: TENTACRUEL_REPO, TENTACRUEL_WORKTREE, TENTACRUEL_SLUG,
TENTACRUEL_BRANCH.
#!/usr/bin/env bash
set -euo pipefail
echo "provisionando $TENTACRUEL_SLUG"
uv sync
(cd frontend && npm ci)
Se o script falhar, a sessão continua utilizável e o erro fica visível no log.
Arquitetura
src/tentacruel/
config.py Settings (uma fonte para portas, caminhos, timeouts)
models.py SQLModel: Repo, Session, Chat, Message
db.py engine + sessões do SQLite (WAL) + migração no boot
schemas.py TODO shape que cruza HTTP/WS — espelhado em types.ts
main.py app FastAPI + estáticos + fallback SPA
cli.py `tentacruel` (console script)
api/ rotas finas: repos.py, sessions.py, ws.py, deps.py
services/
agents/
base.py contrato AgentBackend + emissões normalizadas
claude.py ÚNICO lugar que fala com o Claude Agent SDK
account.py perfil logado no CLI (só exibição, nunca credencial)
limits.py cotas da assinatura: lê o cache do `/usage` do CLI
registry.py catálogo de provedores (um provider novo = uma entrada)
git.py ÚNICO lugar que roda o binário `git`
picker.py ÚNICO lugar que roda `osascript` (seletor de pasta nativo)
repo_icon.py encontra a logo/favicon do repo para o avatar da sessão
files.py navegação e leitura de arquivos do worktree
attachments.py o que o usuário anexa: onde guarda e como chega ao modelo
transcribe.py ÚNICO lugar que chama um transcritor de voz
settings_store.py preferências do usuário (uma chave, um schema)
toolcheck.py detecta git/node/uv/npm e versões
installer.py instala um CLI de provedor, com streaming
proc.py teardown de subprocesso por process group
worktree.py ciclo de vida do worktree (sem FastAPI/DB/SDK)
agent.py ÚNICO lugar que fala com o Claude Agent SDK
tool_summary.py "editou src/x.py" — uma definição só
manager.py orquestra DB + worktree + agente + broadcast
repo.py registro de repositórios
hub.py fan-out de WebSocket
frontend/src/
styles/tokens.css ÚNICA fonte de cor, raio, tipografia, densidade
components/ui/ primitivos — o único lugar que desenha
components/ compostos do domínio (só primitivos + layout)
pages/ composição e estado
lib/ cn, api, ws, types, diff, format
state/sessions.tsx estado global + sockets
tests/ pytest (worktree service + API + WebSocket)
Regras estruturais (as mesmas dos dois lados):
- Nenhum comando
gitfora deservices/git.py. - Todo subprocesso nasce com
start_new_session=Truee morre por process group (services/proc.py). Matar só o filho direto não basta: um neto (npm installdisparado pelosetup.sh, um credential helper do git) herda o pipe de stdout e mantém ocommunicate()do pai bloqueado — o timeout não serviria para nada. - Nenhuma chamada a SDK de IA fora de
services/agents/<provider>.py. O resto do app só conhece as emissões normalizadas deagents/base.py. services/worktree.pynão importa FastAPI, SQLModel nem o SDK — só recebe caminhos e devolve dataclasses. É o que permite que, na v1.1, uma tool custom do SDK (create_worktree_session) chamecreate_worktree()direto e deixe o próprio agente abrir uma sessão irmã. Não implementado no MVP, mas não bloqueado.- Todo shape do backend nasce em
schemas.pye é espelhado emfrontend/src/lib/types.ts. O OpenAPI fica em/docs.
Endpoints
| Método | Rota | O quê |
|---|---|---|
GET/POST |
/api/repos |
listar / registrar repositório |
POST |
/api/repos/browse |
abre o Finder nativo e devolve a pasta escolhida |
GET |
/api/repos/{id}/icon |
logo/favicon do repositório, usada no avatar da sessão |
GET |
/api/providers |
catálogo de provedores de IA, modelos e níveis de esforço |
GET/PATCH |
/api/settings |
preferências de interface (tema, densidade, layout, idioma) |
GET |
/api/system |
versão do Tentacruel + dependências detectadas na máquina |
PATCH |
/api/repos/{id} |
padrões do repositório herdados por sessões novas |
WS |
/ws/install/{provider} |
instala o CLI de um provedor, com saída ao vivo |
DELETE |
/api/repos/{id} |
remover do Tentacruel (nada é apagado do disco) |
GET/POST |
/api/sessions |
listar / criar sessão |
GET/PATCH/DELETE |
/api/sessions/{id} |
detalhe + chats / renomear + provedor / excluir |
POST |
/api/sessions/adopt |
abrir um worktree que já existe |
GET |
/api/repos/{id}/worktrees |
worktrees no disco que nenhuma sessão tem |
GET/POST |
/api/sessions/{id}/sync |
quantos commits atrás da base / trazer esses commits (merge) |
GET |
/api/sessions/{id}/branches |
branches locais, marcando a atual e as presas em outro worktree |
POST |
/api/sessions/{id}/branch |
trocar a branch do worktree — 409 se o git recusar |
GET/POST |
/api/sessions/{id}/chats |
listar / abrir outra conversa no mesmo worktree |
GET/PATCH/DELETE |
/api/chats/{id} |
histórico / renomear, modelo, esforço, permissão / fechar |
POST |
/api/chats/{id}/interrupt |
interromper o turno em execução |
POST |
/api/sessions/{id}/archive |
arquivar (remove_worktree opcional) |
POST |
/api/sessions/{id}/unarchive |
reabrir — 409 se o worktree não estiver mais no disco |
GET |
/api/sessions/{id}/diff |
diff commitado + não commitado + status |
GET |
/api/chats/{id}/usage |
uso da janela de contexto + cotas da assinatura |
GET |
/api/sessions/{id}/files |
filhos de um diretório do worktree (listagem preguiçosa) |
GET/PUT |
/api/sessions/{id}/file |
ler / salvar um arquivo do worktree |
POST |
/api/sessions/{id}/attachments |
guardar o que o usuário colou, arrastou ou escolheu |
POST |
/api/transcribe |
áudio → texto, pelo transcritor da máquina (nada é guardado) |
GET |
/api/sessions/{id}/file/raw |
os bytes de um arquivo, para o navegador desenhar |
POST |
/api/sessions/{id}/commit |
git add -A && git commit |
POST |
/api/sessions/{id}/push |
git push -u origin wt/<slug> |
WS |
/ws/sessions/{id} |
log de provisionamento e lista de chats de um worktree |
WS |
/ws/chats/{id} |
uma conversa, com streaming |
WS |
/ws/events |
ciclo de vida das sessões (mantém o rail vivo) |
Estado
- Banco:
~/.tentacruel/tentacruel.db(SQLite/WAL).make resetapaga. Um~/.wtmux/de antes do rename é adotado no primeiroinit_db(), com o WAL consolidado antes do rename para não perder o histórico recente. - Migração:
init_db()roda a cada boot e é idempotente.create_allsó cria tabelas que faltam, então quatro passos cobrem o resto, nessa ordem: colunas novas (com backfill dos defaults), chats para sessões antigas (adotando as mensagens que ainda apontavam para a sessão), remoção de colunas que o modelo não declara mais e índices que faltam. A remoção só acontece quando a coluna órfã éNOT NULLsem default — a forma que trava todoINSERT, comomessage.session_idtravou depois que a conversa saiu da sessão para o chat. Como o SQLite não fazDROP COLUMNem coluna indexada ou usada por uma foreign key, a tabela é reconstruída. - Worktrees:
<repo>/.tentacruel/worktrees/<slug>. - Histórico do agente: gerenciado pelo próprio Claude Code; o Tentacruel guarda só o
agent_session_idpara oresume.
Notas do Agent SDK
Verificado contra claude-agent-sdk 0.2.142 e a documentação oficial em
https://code.claude.com/docs/en/agent-sdk/python (o domínio platform.claude.com
redireciona para lá). Diferenças em relação ao briefing original, com a doc/API
real prevalecendo:
| Ponto | Realidade na 0.2.142 |
|---|---|
| Classe de opções | ClaudeAgentOptions (o nome antigo ClaudeCodeOptions não existe mais) |
| Sessão persistente | ClaudeSDKClient.connect() sem prompt mantém o processo vivo; query() envia turnos e receive_messages() consome tudo |
session_id |
vem em SystemMessage(subtype="init").data["session_id"] e também em ResultMessage.session_id — SystemMessage tem subtype/data, não content: str |
resume |
ClaudeAgentOptions(resume=<uuid>) mantém o mesmo session_id (com fork_session=False), então o valor guardado continua válido entre reinícios |
| Streaming incremental | exige include_partial_messages=True; os deltas chegam como StreamEvent com event["type"] == "content_block_delta" e delta.type text_delta/thinking_delta |
permission_mode |
o SDK aceita 6 modos; o Tentacruel expõe os 3 do briefing (default, acceptEdits, plan) e troca em tempo real via client.set_permission_mode() |
| Interrupção | client.interrupt() |
Outras decisões que divergem da letra do briefing:
- Tailwind v4 não usa
tailwind.config.js. O mapeamento token → utilitário vive em@theme inlinedentro defrontend/src/styles/globals.css, que é o equivalente v4 de "mapeados no config do Tailwind".tokens.csscontinua sendo a fonte única. - O diff mostra também o não commitado. O briefing pedia
git diff base...HEAD; como emacceptEditso agente edita sem commitar, o painel renderia vazio. A aba mostra as duas seções (não commitado — incluindo arquivos novos — e commitado), além dogit status --short. - O rail mostra avatar + nome; branch e status ficam no tooltip. Um rail de ícones não comporta um nome de branch.
- ESLint foi adicionado manualmente. O template atual do Vite vem com
oxlint, que não suporta regras custom; a regra "template first" exigia ESLint.
Desenvolvimento
Além dos pré-requisitos de instalação, trabalhar no código pede Python 3.12+ e Node 20+ — este último só para compilar a interface, que num pacote instalado já vem pronta.
Rodar a partir do código
git clone <este-repo> Tentacruel && cd Tentacruel
make setup # uv sync + npm install
make dev # backend :8787 + Vite :5173 → abra http://localhost:5173
Modo "produção local" (tudo servido pelo FastAPI, uma porta só):
make start # build do frontend + servidor em http://localhost:8787
Outros alvos: make test, make lint, make build, make clean, make reset.
make help lista todos.
Testes
make test # ruff + pytest + tsc + eslint
uv run pytest -k worktree # só o worktree service
Cobertura atual (427 testes): criação de worktree, .tentacruelinclude (incluindo
padrões que tentam escapar do repo), .tentacruel/setup.sh, colisão de slug
(nome repetido, branch preexistente, diretório órfão), remoção idempotente,
isolamento entre worktrees, diff commitado vs não commitado (incluindo arquivos
novos, apagados e grandes demais para inline), commit, push sem remote, resumos
de ferramenta, e o ciclo completo da API + WebSocket com um agente de mentira
(para não gastar tokens). O caminho com agente real é o teste manual acima.
Publicar uma versão
make dist # compila o frontend e monta o wheel COM a interface dentro
O alvo falha de propósito se a interface não estiver no pacote. Ela é artefato
de build e portanto está no .gitignore, e o hatchling monta a lista de
arquivos a partir do controle de versão — sem o artifacts no pyproject.toml,
o wheel instala sem UI e serve a página de "frontend não compilado". O erro só
aparece depois de instalar, que é o pior lugar para descobri-lo.
A versão vive só no pyproject.toml; o app a lê da metadata do pacote. Já
esteve escrita em três lugares, o que funciona até alguém editar um e esquecer
os outros dois.
Teste manual: os agentes estão mesmo isolados?
Critério de aceite nº 2. Leva ~3 minutos.
make deve registre um repo qualquer com pelo menos um commit.- Crie três sessões:
alfa,beta,gama. - Mande instruções diferentes, uma em cada painel:
- alfa: "Crie um arquivo ALFA.txt com a palavra alfa. Não commite."
- beta: "Crie um arquivo BETA.txt com a palavra beta. Não commite."
- gama: "Liste os arquivos na raiz do projeto e me diga se existe ALFA.txt ou BETA.txt."
- No terminal, confira:
cd <seu-repo>
git status --short # vazio: o checkout principal está intacto
ls .tentacruel/worktrees/alfa # ALFA.txt presente
ls .tentacruel/worktrees/beta # BETA.txt presente
ls .tentacruel/worktrees/alfa/BETA.txt # No such file or directory ✅
ls .tentacruel/worktrees/beta/ALFA.txt # No such file or directory ✅
git worktree list # main + 3 worktrees
- O agente
gamadeve responder que não existe nenhum dos dois arquivos. - A aba Diff de cada sessão mostra apenas o arquivo daquela sessão.
Persistência e resume (critério 3): encerre o make dev (Ctrl+C), suba de
novo e reabra o browser. As três sessões continuam no rail com o histórico
completo. Pergunte a alfa: "que arquivo você criou no turno anterior?" — ela
responde ALFA.txt, porque o Tentacruel guardou o agent_session_id e reconecta com
resume.
Remoção (critério 5): nos controles de gama, pasta-com-X → Apagar o worktree do disco.
ls .tentacruel/worktrees/ não lista mais gama, git worktree list também não, e
git branch --list "wt/*" continua mostrando wt/gama.
Teste manual: o design system é mesmo fonte única?
Critério de aceite nº 7. Leva ~1 minuto. Detalhes em frontend/README.md.
- Abra
frontend/src/styles/tokens.csse troque uma linha:
--wt-primary: #5b6bf5; /* → #16a34a */
-
Com o
make devrodando, o app inteiro fica verde no reload: botão+, anel do avatar ativo, botão Enviar, ícones do assistente, badges, cabeçalho do hunk no diff, anel de foco. Nenhum outro arquivo foi tocado. -
Mesma ideia para comportamento: em
frontend/src/components/ui/Button.tsx, troquedefaultVariants: { variant: "secondary" }para"primary"— todo<Button>semvariantexplícito muda junto. -
Reverta e rode
cd frontend && npm run lint: as regrastentacruel/no-raw-elementsetentacruel/no-visual-classnamesfalham se alguma tela fora desrc/components/ui/usar um<button>cru ou uma classe visual comobg-[#1a1a1a]/px-[13px]. -
/dev/ui(http://localhost:5173/dev/ui) mostra o catálogo vivo de tokens e primitivos.
Vindo do wtmux
Sobre o nome. O produto é Tentacruel, e esse é também o identificador
técnico: o pacote Python (src/tentacruel/), o comando (tentacruel), o
diretório de dados (~/.tentacruel/) e a pasta de worktrees dentro de cada
repo (.tentacruel/worktrees/).
Antes, os quatro se chamavam wtmux — worktree multiplexer, no molde do
tmux, que multiplexa terminais. Instalações daquela época continuam
funcionando, e as duas metades são tratadas de formas diferentes porque o
risco não é o mesmo:
~/.wtmux/é movida no primeiro boot. É diretório nosso, e o WAL do SQLite é consolidado antes do rename — sem isso, o histórico mais recente seria descartado em silêncio, porque o SQLite acha o-walpelo nome do.db.<repo>/.wtmux/fica onde está. Aqueles worktrees estão registrados em.git/worktrees/e podem ter trabalho não commitado de agente dentro. O layout é detectado: repo que já tem.wtmux/mantém, repo novo nasce.tentacruel/..wtmuxincludetambém continua valendo — é arquivo seu, no seu repo.
Duas abreviações de worktree sobreviveram de propósito, e não têm relação
com o nome antigo: o prefixo de branch wt/ e os tokens CSS --wt-*.
Fora do escopo do MVP
Preview de dev server, multi-usuário/auth, empacotamento desktop, merge/PR pela UI (faça no terminal).
Provedores de IA além do Claude Code: a arquitetura está pronta e Codex,
OpenCode e Gemini já aparecem em Configurações → Provedores (detectáveis e
instaláveis). Falta escrever os adapters em services/agents/<id>.py — o que
exige os CLIs instalados para testar de verdade.
Próximo passo previsto (v1.1): tool custom create_worktree_session para o
próprio agente abrir uma sessão irmã — a arquitetura já está preparada.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tentacruel-0.1.1-py3-none-any.whl.
File metadata
- Download URL: tentacruel-0.1.1-py3-none-any.whl
- Upload date:
- Size: 1.6 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
02ecd127bde6e7356147edbdc9be6d14058bc77bbe5f9c333d63fbb180eb186e
|
|
| MD5 |
37dcfa927947cfa7b543026bff85c2bd
|
|
| BLAKE2b-256 |
712373061811b24abc14a6eaf8e070901b5c2df8bfb63064a2d974769971a5de
|