🎉 Initial commit: PiAgentUI — AI Agent Desktop & Web UI
Build & Push Frontend Docker Image / build-and-push (push) Canceled after 0s
Build Validation / validate (push) Canceled after 6s
Build & Push Backend Docker Image / build-and-push (push) Failing after 10m10s
Build & Push Gateway Docker Image / build-and-push (push) Failing after 10m9s

A professional workspace for AI coding agents with:
- Real-time streaming chat with SSE
- Session management (fork, clone, tree navigation)
- MCP tools browser & metadata panel
- Skills browser (global, project, package sources)
- Voice input & STT provider configuration
- Integrated terminal & file explorer
- Model selector with capability metadata
- Keyboard-first UI with command palette
- 8 languages (en, pt-BR, es, ja, zh-CN, ar, bn, hi)
- Tauri 2 desktop app
- React 19 + TypeScript + Vite 8 + Tailwind CSS 4
- Pi Agent extension runtime

Originally forked from OpenCodeUI, now an independent project.
License: GPL-3.0-only
This commit is contained in:
Evandro committed 2026-06-01 00:50:06 -03:00
commit b3489e1ede
983 files changed
+158475

No files matched your search

@@ -0,0 +1,66 @@
# Especificação Técnica: Migração do PiAgentUi para Extensão do Pi Agent
Esta especificação define o desenho arquitetônico, a segurança, as estruturas de dados e a integração desktop necessários para migrar o frontend `PiAgentUi` (originalmente construído para o OpenCode) para o ecossistema do **Pi Agent**.
---
## 1. Arquitetura e Fluxo de Dados
A arquitetura do `PiAgentUi` será descentralizada do OpenCode e integrada diretamente ao processo do Pi Agent através do modelo de extensões.
1. **Loopback Server (Porta Dinâmica):** A extensão `piagentui-server.ts` se conectará ao ciclo de vida do Pi e abrirá um servidor local seguro escutando em `127.0.0.1` em uma porta livre selecionada pelo sistema operacional.
2. **Same-Origin Boot Data:** A extensão servirá o HTML principal e injetará no próprio corpo do `index.html` um bloco JSON contendo as informações de boot (URL base, porta, e o token bearer gerado na inicialização).
3. **Ponte de Comunicação:** Toda chamada HTTP e WebSocket será autenticada usando o token em memória.
```
┌─────────────────┐ ┌────────────────────────┐ ┌────────────────┐
│ Tauri (Rust) │ ───> │ Pi Agent (Processo) │ <───> │ Webview (UI) │
│ Port Check & │ │ - piagentui-server │ │ - React 19 │
│ Launch State │ │ - HTTP + WS Server │ │ - Local Token │
└─────────────────┘ └────────────────────────┘ └────────────────┘
```
---
## 2. Matriz de Endpoints e Recursos
Para o MVP, a API do `PiAgentUi` responderá nos mesmos paths esperados pelo frontend, mapeando-os para chamadas da extensão e mockando recursos incompatíveis.
| Recurso / Funcionalidade | Endpoint Frontend | Ação no Backend (Pi Agent Extension) | Status no MVP |
| :--- | :--- | :--- | :--- |
| **Health Check** | `/global/health` | Retorna status da extensão do Pi | **Suportado** |
| **Event Stream** | `/global/event` | SSE/WebSocket que espelha os eventos do Pi | **Suportado** |
| **Modelos Ativos** | `/api/models` | Consulta o registro de modelos configurado no Pi | **Suportado** |
| **Multi-Sessão (CRUD)** | `/api/sessions` | Gerencia sessões locais da pasta de sessões do Pi | **Suportado** |
| **Enviar Mensagem** | `/api/messages/send` | Dispara `pi.sendUserMessage(...)` assincronamente | **Suportado** |
| **Parar Mensagem** | `/api/messages/abort` | Aborta o turno ativo do Pi Agent se disponível | **Suportado** |
| **PTY Terminal** | `/api/pty` | Retorna erro 501 / Desativado visualmente | **Stubbed** |
| **MCP / LSP** | `/api/mcp` / `/api/lsp`| Retorna lista vazia ou erro 501 | **Stubbed** |
| **Worktrees / VCS** | `/api/worktrees` | Retorna erro 501 / Desativado visualmente | **Stubbed** |
---
## 3. Segurança e Tokens
1. **Porta Dinâmica:** Impedir conflitos de porta iniciando sempre em portas dinâmicas alocadas pelo SO (`port: 0`).
2. **Bearer Token:** Gerar um UUID/token seguro de uso único na inicialização do servidor.
3. **Sem Persistência de Senhas:** Nenhuma credencial do usuário será armazenada fisicamente ou em `localStorage`. O token de autorização residirá unicamente na memória da sessão React.
4. **Validação de Host/Origin:** Rejeitar qualquer requisição com cabeçalho `Host` ou `Origin` que não seja estritamente local (`127.0.0.1` ou `localhost`).
5. **Prevenção de Traversal:** Servir arquivos estáticos validando canonicalização de caminhos para evitar vulnerabilidade de directory traversal no Windows.
---
## 4. Integração Tauri Desktop (Track B)
Com a decisão de **incluir o Tauri já no primeiro marco**, o app desktop sofrerá as seguintes modificações:
* **Renomeação de Comandos:** Todos os comandos Tauri registrados no Rust e no frontend serão renomeados de `*opencode*` para correspondentes de `*pi*` ou `*pi_agent*`.
* **Subprocesso do Pi:** Em vez de iniciar `opencode serve`, o Tauri iniciará o processo do Pi (ex: `pi --mode rpc`) adicionando dinamicamente o parâmetro `--extension` apontando para o nosso servidor local se necessário, ou confiando no auto-load das extensões instaladas.
* **Filtro de URL Restrito:** O Rust bridge do Tauri terá uma validação explícita em tempo de execução para rejeitar qualquer tráfego que tente se conectar a servidores externos, limitando a ponte estritamente a adaptadores de loopback permitidos.
---
## 5. Referências do Projeto
Esta migração baseia-se diretamente no comportamento implementado em:
`D:/Documents/Github/PiAgentUI/better-tau/extensions/mirror-server.ts`
utilizando o modelo de ciclo de vida e roteamento nativo lá desenvolvidos como garantia de estabilidade sob o runtime do Pi.
@@ -0,0 +1,105 @@
# Inventário de Campos e Dependências do SDK do OpenCode no PiAgentUi
Este documento registra todas as dependências da biblioteca `@opencode-ai/sdk` encontradas no `PiAgentUi`, detalhando as estruturas (DTOs) que o frontend consome e como elas serão mapeadas para a nova estrutura de compatibilidade com o Pi Agent.
---
## 1. Arquivos com Acoplamento ao SDK do OpenCode
Os seguintes arquivos realizam importações diretas do cliente SDK (`@opencode-ai/sdk`) ou utilizam tipos derivados dele:
### Arquivos em `src/api/`
* `agent.ts`
* `client.ts`
* `command.ts`
* `config.ts`
* `file.ts`
* `global.ts`
* `lsp.ts`
* `mcp.ts`
* `message.ts`
* `permission.ts`
* `pty.ts`
* `session.ts`
* `skill.ts`
* `todo.ts`
* `tool.ts`
* `vcs.ts`
* `worktree.ts`
* `sdk.ts`
### Arquivos em `src/types/api/`
* `agent.ts`
* `common.ts`
* `config.ts`
* `event.ts`
* `file.ts`
* `mcp.ts`
* `message.ts`
* `model.ts`
* `permission.ts`
* `project.ts`
* `pty.ts`
* `session.ts`
* `skill.ts`
* `tool.ts`
* `vcs.ts`
* `worktree.ts`
---
## 2. Inventário de Estruturas Consumidas pela UI
Para que a migração não quebre o frontend, a API da extensão Pi deve produzir e aceitar dados que correspondam a estas chaves essenciais:
### A. Estrutura de Mensagem (`MessageWithParts` / `Message`)
O frontend lê de `message.info` e de `message.parts`:
* **`info`** (`UserMessage` ou `AssistantMessage`):
* `id`: string (UUID da mensagem).
* `sessionID`: string (UUID da sessão).
* `role`: `'user' | 'assistant'`.
* `time`: `{ created?: number; completed?: number }`.
* `summary`: string (breve descrição se disponível).
* `modelID`: string (somente assistente).
* `providerID`: string (somente assistente).
* **`parts`** (Array de blocos que compõem a mensagem):
* **`TextPart`**: `{ type: 'text', text: string }`.
* **`ReasoningPart`**: `{ type: 'reasoning', text: string }`.
* **`ToolPart`**: `{ type: 'tool', tool: string, state: { status: 'pending' | 'running' | 'completed' | 'error', input?: any, output?: any, error?: string } }`.
* **`FilePart`**: `{ type: 'file', filename?: string, mime?: string, url?: string }`.
* **`StepStartPart`**: `{ type: 'step-start' }`.
* **`StepFinishPart`**: `{ type: 'step-finish' }`.
### B. Estrutura de Sessão (`Session`)
Utilizada no menu lateral de histórico e criação:
* `id`: string (UUID).
* `title`: string (nome da sessão).
* `directory`: string (diretório de trabalho correspondente).
* `time`: `{ created?: number; updated?: number }`.
* `summary`: `{ title?: string; category?: string; difficulty?: string; tokens?: number; cost?: number }`.
* `status`: `'idle' | 'running' | 'error'`.
### C. Envelopes de Eventos Globais (`GlobalEvent`)
Processados pelo pipeline de SSE em `events.ts`:
* `{ directory?: string, payload: { type: string, properties: any } }`
* Eventos chave:
* `server.connected`
* `session.updated`
* `session.status`
* `session.idle`
* `session.error`
* `message.updated`
* `message.part.updated`
* `message.part.delta`
---
## 3. Estratégia de Mapeamento no MVP
| Módulo Original | Ação / Novo Mapeamento |
| :--- | :--- |
| **`session`** | Consultas diretas ao Pi Session Manager via rotas REST locais da extensão. Mapeado para sessões nativas do Pi. |
| **`message`** | Mapeado para o envio assíncrono de prompts ao Pi (`pi.sendUserMessage`). Eventos nativos do Pi traduzidos em deltas de partes. |
| **`tool`** | Retorna ferramentas padrões disponíveis do Pi. |
| **`pty` / `lsp` / `mcp`** | Desativados visualmente / stubs de array vazio / erro 501. |
| **`worktree` / `vcs`** | Desativados visualmente. |
@@ -0,0 +1,140 @@
# PiAgentUI Active Session Switch Design
## Goal
When the user opens an existing session in PiAgentUI and sends a message, the message must be delivered to that selected Pi session, not to whichever session was active when the Pi TUI launched.
## Current finding
- The frontend route/store correctly loads selected sessions and their messages.
- `src/hooks/useChatSession.ts` sends the selected `sessionId` to `sendMessageAsync()`.
- `extensions/piagentui-server.ts` receives `sessionId`, but still calls `pi.sendUserMessage(parsed.text)`.
- `pi.sendUserMessage()` always targets the current active Pi runtime session.
- Pi exposes `switchSession()` only on `ExtensionCommandContext`, not on normal event `ExtensionContext` used by `session_start` and the HTTP server.
- `AgentSessionRuntime.switchSession()` intentionally emits `session_shutdown`, disposes the old session, creates the replacement session, rebinds extensions, then runs `withSession(replacedCtx)`.
- The existing PiAgentUI extension currently stops the web server on every `session_shutdown`, so a normal Pi session switch can close the HTTP/SSE server and force reconnects.
## Selected approach: activate and send on the backend
The backend should become authoritative for message routing:
1. Resolve requested `sessionId` to the JSONL session file in the current session directory.
2. If the requested session differs from the active Pi session, switch the real Pi runtime to that session.
3. Send the user message inside the replacement session callback so the message cannot land in the old session.
4. Keep the existing frontend model: route/session selection still works as today; `sendMessageAsync()` keeps passing `sessionId`.
This preserves the current UI while making the server guarantee correctness.
## Required changes
### Pi core
Expose a safe session-switch capability to extension event contexts, or a narrowly-scoped equivalent that can be used by long-lived extension services.
Preferred API shape:
```ts
interface ExtensionContext {
switchSession(
sessionPath: string,
options?: { withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
): Promise<{ cancelled: boolean }>
}
```
Implementation should reuse the existing `switchSessionHandler` already used by `createCommandContext()`. The important behavior is not a new session implementation; it is making the already-existing runtime switch available to the PiAgentUI HTTP server context.
### PiAgentUI extension server
Add helpers in `extensions/piagentui-server.ts`:
- `getSessionFileForId(sessionId)` scans `latestCtx.sessionManager.getSessionDir()` for `*_${sessionId}.jsonl`.
- `sendUserMessageToSession(parsed)` validates `sessionId`, resolves the session file, switches if needed, and sends in the correct context.
- `session_shutdown` handling must not tear down the HTTP server on `reason === 'resume'` if the server is intentionally switching sessions for the web UI. The server should keep the port stable and update `latestCtx` on the following `session_start`.
Target send flow:
```ts
if (requestedSessionId && requestedSessionId !== activeSessionId) {
const sessionFile = getSessionFileForId(requestedSessionId)
const result = await latestCtx.switchSession(sessionFile, {
withSession: async replacedCtx => {
await maybeSetModel(replacedCtx, parsed.model)
await replacedCtx.sendUserMessage(parsed.text)
},
})
if (result.cancelled) return HTTP 409
return HTTP 202
}
await maybeSetModel(latestCtx, parsed.model)
pi.sendUserMessage(parsed.text)
```
If `sessionId` is missing or already active, keep the existing path.
### Frontend
Keep frontend mostly unchanged.
Small additions only if needed:
- `sendMessageAsync()` continues sending `sessionId`.
- Optional: handle HTTP `409`/`404` with a clear toast if target session cannot be activated.
- Optional: add `activeSessionId` from `server.connected`/`session.status` later, but not required for the core fix.
## Alternatives considered
### Activate on session selection
Frontend would call `/api/sessions/:id/activate` when a user clicks a session. This matches the user's mental model, but it introduces a race: if the user sends before activation completes, the message can still hit the old session unless send also validates activation. It is useful as a UX enhancement after backend routing is correct, not as the primary correctness mechanism.
### Frontend guard only
Frontend could block sending if selected session differs from active Pi session. This avoids Pi core changes but makes UX worse and still requires a trustworthy active-session signal from the backend. It detects the problem instead of solving it.
## Tests
### Extension/unit tests
Add or update extension server tests to cover:
1. Sending with active `sessionId` calls `pi.sendUserMessage()` without switching.
2. Sending with different `sessionId` resolves the matching JSONL file and calls `ctx.switchSession(sessionFile, { withSession })`.
3. The `withSession` callback calls `replacedCtx.sendUserMessage(text)`.
4. Unknown `sessionId` returns 404 and does not send to the active session.
5. Cancelled switch returns 409 and does not send.
6. `session_shutdown` with `reason: 'resume'` does not close the web server during web-initiated session switching.
### Frontend tests
Add or update API/hook tests to cover:
1. `sendMessageAsync()` includes `sessionId` in the payload.
2. Send error responses surface through the existing error handling path.
3. Existing route/session selection behavior remains unchanged.
### Manual verification
1. Start Pi in session A.
2. Open PiAgentUI.
3. Select existing session B in the sidebar.
4. Send a message.
5. Verify new user and assistant messages appear in session B.
6. Verify session A JSONL is unchanged.
7. Verify `/global/event` remains connected and streaming works without F5.
## Risks and mitigations
- Switching sessions invalidates old extension contexts. Mitigation: send inside `withSession(replacedCtx)` and avoid captured stale `pi`/ctx after switch.
- Current extension closes the HTTP server on `session_shutdown`. Mitigation: preserve server across `resume` switches and update `latestCtx` on `session_start`.
- Pi core API change can be too broad. Mitigation: expose only the existing switch handler; do not create parallel session logic.
- Session file lookup by suffix must be strict. Mitigation: only match `_${sessionId}.jsonl` inside the current session dir.
## Acceptance criteria
- Sending from selected session B never writes to active-at-launch session A.
- Frontend routing and message loading stay visually the same.
- The web server port remains stable during a session switch initiated by PiAgentUI.
- Streaming still updates live after switch.
- Typecheck, extension typecheck, relevant tests, and build pass.
@@ -0,0 +1,326 @@
# Design — README atual do PiAgentUI e sistema de áudio/STT
**Data:** 2026-05-30
**Status:** aprovado conceitualmente pelo usuário; aguardando revisão do arquivo antes da implementação
**Escopo:** reescrever o `README.md` do projeto atual e preparar a arquitetura completa para envio de áudio e transcrição em tempo real no app.
## Contexto
O projeto começou como uma adaptação do OpenCodeUI, mas atualmente está evoluindo para o **PiAgentUI**: uma interface web/desktop para o Pi Agent, com ambição explícita de se tornar um app inteiro no estilo Codex ou Claude Desktop.
O estado atual já inclui:
- frontend React 19 + TypeScript + Vite;
- app desktop via Tauri 2;
- extensão Pi local em `extensions/piagentui-server.ts` como backend/runtime bridge;
- chat com streaming/SSE;
- sessões reais do Pi, histórico e roteamento de sessão;
- tool calls e tool results renderizados em cards;
- contexto real do runtime via endpoint `/api/sessions/:sessionId/context`;
- enriquecimento de metadados de modelo via catálogo OpenRouter local em `src/assets/models.json`;
- lista real de Skills agrupadas por origem;
- metadados reais de MCP configurado;
- slash commands reais do Pi expostos por `/api/commands/list`;
- anexos de arquivos, incluindo áudio como arquivo quando o modelo selecionado declara suporte a áudio.
Ainda não existe:
- gravação de microfone no composer;
- transcrição parcial em tempo real;
- endpoint backend para STT;
- configurações STT;
- provedor STT customizável OpenAI-compatible;
- pipeline de áudio integrado ao envio de mensagens.
## Objetivos
1. Reescrever `README.md` em português para refletir o produto atual, não o projeto original OpenCodeUI.
2. Apresentar PiAgentUI como interface moderna para o Pi Agent e base futura de app completo tipo Codex/Claude.
3. Documentar capacidades existentes de forma honesta, separando **implementado**, **em andamento** e **roadmap**.
4. Projetar um sistema de áudio/STT completo, com opção padrão forte e configuração externa flexível.
5. Implementar o sistema em fases testáveis, com TDD para lógica, API, stores e componentes.
## Não objetivos
- Não renomear pacote npm, bundle, app Tauri ou metadados de release nesta primeira etapa, salvo menção no README como débito de branding.
- Não implementar agente de voz falante/TTS nesta fase; o foco é entrada por voz e transcrição para texto.
- Não substituir o fluxo normal de texto; áudio será uma camada opcional sobre o composer.
- Não acoplar PiAgentUI ao OpenRouter para STT. OpenRouter continua sendo catálogo/metadados quando útil; STT usa provedores próprios.
## README.md proposto
O novo README deve ser escrito em português e deve substituir integralmente o conteúdo herdado. Estrutura recomendada:
1. **Título e tagline**
- `# PiAgentUI`
- “Interface web/desktop para Pi Agent — caminhando para um app completo estilo Codex/Claude.”
2. **Visão geral**
- Explicar que o PiAgentUI é uma UI local para operar o Pi Agent com sessões, ferramentas, MCP, Skills, comandos e contexto real.
- Deixar claro que o runtime Pi é a fonte de verdade.
3. **Estado atual**
- Listar capacidades implementadas:
- chat streaming;
- sessões reais;
- tool cards;
- context usage;
- models metadata;
- skills;
- MCP metadata;
- slash commands;
- anexos multimodais;
- terminal/file explorer/Tauri quando aplicável.
4. **Roadmap do produto**
- Ações visuais para comandos Pi;
- áudio/STT em tempo real;
- experiência voice-first;
- MCP actions/connect/auth;
- app completo tipo Codex/Claude.
5. **Arquitetura**
- Frontend React/Vite/Tauri;
- extensão Pi como bridge local;
- Pi runtime como estado autoritativo;
- APIs locais `/api/*`;
- SSE para eventos.
6. **Desenvolvimento local**
- `npm install`;
- `npm run dev`;
- `npm run build`;
- `npm run test:run`;
- `npm run typecheck`;
- `npm run typecheck:extensions`.
7. **Notas de runtime**
- Algumas mudanças exigem reiniciar/recarregar o Pi/extension server.
- O arquivo de descoberta local fica em `~/.pi/agent/piagentui-port.json`.
8. **Áudio e transcrição — planejado**
- Resumir o plano STT sem prometer que já está implementado.
9. **Licença e origem**
- Manter licença atual se aplicável.
- Mencionar que o projeto deriva de uma base OpenCodeUI, mas agora segue direção própria para PiAgentUI.
## Sistema de áudio/STT — abordagem recomendada
### Opção padrão
Usar **OpenAI Realtime Transcription** com `gpt-realtime-whisper` como padrão para transcrição ao vivo.
Razões:
- foi projetado para live speech-to-text;
- entrega deltas parciais;
- tem latência baixa;
- usa sessão dedicada de transcrição, sem resposta falada do modelo;
- combina com um composer por voz onde o texto aparece enquanto o usuário fala.
### Fallback padrão
Usar **OpenAI Audio Transcriptions** para áudio fechado/arquivo:
- `gpt-4o-transcribe` como opção de maior qualidade;
- `gpt-4o-mini-transcribe` como opção de menor custo;
- `whisper-1` para compatibilidade com integrações antigas.
Esse fallback deve ser usado quando:
- o navegador/ambiente não suportar streaming confiável;
- o usuário enviar um arquivo de áudio;
- o provedor configurado só oferecer endpoint `/audio/transcriptions`;
- a conexão realtime falhar e houver áudio gravado em buffer.
### Provedor OpenAI-compatible externo
O usuário deve poder configurar qualquer API compatível com o endpoint de transcrição da OpenAI.
Configuração proposta:
```ts
interface SttProviderConfig {
id: string
label: string
kind: 'openai' | 'openai-compatible'
mode: 'realtime' | 'file' | 'both'
baseUrl: string
apiKey: string
realtimeModel?: string
transcriptionModel: string
transcriptionEndpoint?: string
language?: string
prompt?: string
responseFormat?: 'json' | 'text' | 'verbose_json'
}
```
Exemplo para Together AI:
```json
{
"kind": "openai-compatible",
"mode": "file",
"baseUrl": "https://api.together.xyz/v1",
"transcriptionEndpoint": "/audio/transcriptions",
"transcriptionModel": "openai/whisper-large-v3"
}
```
Observação: APIs “OpenAI-compatible” costumam ser compatíveis no endpoint HTTP de arquivo, mas realtime/WebSocket varia por provedor. Por isso, realtime deve ser capability separada, não assumida automaticamente.
## Arquitetura proposta do STT
### Frontend
Criar uma feature dedicada:
- `src/features/voice/VoiceRecorderButton.tsx`
- `src/features/voice/VoiceTranscriptOverlay.tsx`
- `src/features/voice/useVoiceRecorder.ts`
- `src/features/voice/useRealtimeTranscript.ts`
- `src/features/voice/audioEncoding.ts`
Responsabilidades:
- pedir permissão de microfone;
- mostrar estados: idle, requesting_permission, recording, connecting, transcribing, error;
- capturar áudio;
- enviar chunks para o backend;
- renderizar transcrição parcial e final;
- inserir texto final no composer;
- opcionalmente enviar automaticamente após confirmação do usuário.
### Store/configuração
Criar configuração local de STT:
- `src/store/sttSettingsStore.ts`
- persistência em localStorage inicialmente;
- campos para provider padrão, base URL, modelo, idioma, prompt, modo e preferências de UX.
Preferências de UX:
```ts
interface VoiceInputPreferences {
insertMode: 'append' | 'replace-selection'
autoSend: boolean
showPartialTranscript: boolean
pushToComposerWhileSpeaking: boolean
silenceAutoStopMs: number
}
```
### API frontend
Criar cliente dedicado:
- `src/api/stt.ts`
Endpoints planejados:
- `GET /api/stt/config/schema`
- `POST /api/stt/transcriptions`
- `POST /api/stt/realtime/session`
- `POST /api/stt/realtime/:sessionId/chunk`
- `POST /api/stt/realtime/:sessionId/commit`
- `DELETE /api/stt/realtime/:sessionId`
### Backend/extensão Pi
Adicionar ao `extensions/piagentui-server.ts` ou extrair para módulo dedicado se o arquivo ficar grande:
- parser seguro de multipart/audio para transcrição por arquivo;
- proxy para `/v1/audio/transcriptions`;
- gerenciador de sessão realtime;
- normalização de eventos para o frontend.
Formato de eventos para o frontend:
```ts
type SttEvent =
| { type: 'stt.session.started'; sessionId: string }
| { type: 'stt.transcript.delta'; sessionId: string; text: string; itemId?: string }
| { type: 'stt.transcript.completed'; sessionId: string; text: string; itemId?: string }
| { type: 'stt.error'; sessionId?: string; message: string; recoverable: boolean }
| { type: 'stt.session.closed'; sessionId: string }
```
### Segurança
- API keys não devem aparecer em logs.
- Configurações secretas devem ser mascaradas na UI.
- O backend deve limitar tamanho/duração de áudio.
- O backend deve validar `baseUrl` e impedir esquemas não HTTP(S).
- Erros do provedor devem ser normalizados, sem vazar headers.
- O usuário deve confirmar antes de habilitar auto-send de transcrição.
### Testes
Seguir TDD:
1. `src/api/stt.test.ts`
- monta payloads corretos;
- trata deltas/finais;
- mascara erros.
2. `src/store/sttSettingsStore.test.ts`
- defaults;
- persistência;
- atualização parcial;
- remoção de secret.
3. `src/features/voice/*.test.tsx`
- estados do botão;
- permissão negada;
- parcial/final no composer;
- auto-send desligado por padrão.
4. `extensions/piagentui-server.test.ts`
- endpoints STT;
- validação de config;
- proxy OpenAI-compatible;
- realtime session lifecycle com mocks.
## Plano de implementação recomendado
1. Atualizar `README.md` com foco PiAgentUI atual.
2. Adicionar uma seção curta no README para STT planejado.
3. Criar plano TDD específico para STT antes de escrever produção.
4. Implementar configuração STT.
5. Implementar transcrição por arquivo OpenAI-compatible.
6. Implementar botão de gravação e inserção no composer.
7. Implementar realtime OpenAI como provider padrão.
8. Adicionar fallback de arquivo para provedores sem realtime.
9. Fazer verificação manual no browser/Tauri.
## Critérios de aceite
### README
- Não deve mais vender o projeto como OpenCodeUI.
- Deve explicar claramente que o projeto atual é PiAgentUI.
- Deve listar funcionalidades reais já implementadas.
- Deve separar roadmap de funcionalidades existentes.
- Deve incluir comandos de desenvolvimento corretos.
### STT
- Usuário consegue configurar provider padrão OpenAI.
- Usuário consegue configurar provider externo OpenAI-compatible para `/audio/transcriptions`.
- Usuário consegue gravar áudio pelo microfone.
- Usuário vê transcrição parcial quando o provider realtime suporta.
- Usuário recebe texto final no composer.
- Auto-send fica desligado por padrão.
- Falhas de permissão, rede e provider são visíveis e recuperáveis.
- Testes cobrem API, store e UI principal.
## Fontes consultadas
- OpenAI Realtime transcription: https://developers.openai.com/api/docs/guides/realtime-transcription
- OpenAI GPT-4o Transcribe: https://developers.openai.com/api/docs/models/gpt-4o-transcribe
- OpenAI speech transcription methods cookbook: https://developers.openai.com/cookbook/examples/speech_transcription_methods
- Together AI transcription docs: https://docs.together.ai/docs/inference/transcription/overview
- Together AI Whisper Large v3: https://www.together.ai/models/openai-whisper-large-v3
@@ -0,0 +1,565 @@
# Spec — STT, acesso LAN/QR, sessões reais e histórico por projeto
**Data:** 2026-05-30
**Status:** proposta técnica pronta para revisão do usuário antes de implementação
**Produto:** PiAgentUI
**Escopo:** implementar, com mudanças mínimas e TDD, quatro melhorias conectadas ao uso real do PiAgentUI:
1. sistema de áudio/STT no composer;
2. exposição controlada do PiAgentUI na rede local com IP e QR Code;
3. criação de novas sessões que realmente criam novos chats no Pi Agent;
4. histórico lateral agrupado por pastas/projetos, expansível e recolhível.
## Contexto observado no projeto
Arquitetura atual relevante:
- Backend local da UI fica em `extensions/piagentui-server.ts`.
- Núcleo de helpers seguros fica em `extensions/piagentui-server-core.ts`.
- O servidor HTTP atualmente faz bind em `127.0.0.1` e valida `Host`/`Origin` como loopback-only.
- `serveIndexHtml()` injeta `window.PI_BOOT_DATA` com `baseUrl: http://127.0.0.1:<port>` e bearer token local.
- O discovery local é salvo em `~/.pi/agent/piagentui-port.json` com `{ port, token }`.
- `POST /api/sessions` atualmente **não cria sessão real**: ele apenas retorna `getSessionInfo(getCurrentSessionId())`.
- `GET /api/sessions` lê arquivos `.jsonl` do diretório de sessões e deriva metadados via `readSessionMetadata()`.
- `readSessionMetadata()` já extrai `header.cwd`, título por `session_info` ou primeira mensagem de usuário, timestamps e contagem básica.
- `sendUserMessageToRequestedSession()` já consegue trocar rapidamente para uma sessão solicitada via `latestCtx.switchSession(sessionFile, { withSession })` e enviar mensagem nela.
- A UI de sessões está em `src/features/sessions/SessionList.tsx`.
- O estado de sessões vem de `src/contexts/SessionContext.tsx` e `src/api/session.ts`.
- O input/composer está em `src/features/chat/InputBox.tsx` e `src/features/chat/input/InputToolbar.tsx`.
- Já existem anexos multimodais, incluindo áudio como arquivo quando o modelo suporta.
- A store de servidores fica em `src/store/serverStore.ts` e já suporta múltiplos backends, bearer token, Basic Auth e health checks.
## Objetivos
### Objetivo 1 — STT no composer
Adicionar entrada por voz sem substituir o fluxo de texto:
- botão de microfone no composer;
- gravação de áudio pelo navegador/Tauri webview;
- transcrição por provider configurável;
- inserção do texto transcrito no input;
- fallback por arquivo via OpenAI-compatible `/audio/transcriptions`;
- arquitetura preparada para realtime OpenAI depois, sem bloquear o MVP.
### Objetivo 2 — acesso LAN com QR Code
Permitir que o usuário acesse o PiAgentUI por outro dispositivo na mesma rede local:
- manter acesso LAN **desligado por padrão**;
- quando ligado, mostrar IP local do computador, URL de acesso e QR Code;
- não remover autenticação existente;
- não expor sem aviso que o controle do agente fica disponível na rede local;
- manter loopback funcionando normalmente.
### Objetivo 3 — novas sessões reais no Pi Agent
Corrigir o comportamento de “novo chat”:
- `New chat` deve criar um novo arquivo de sessão Pi `.jsonl`;
- deve alternar rapidamente para essa sessão real;
- enviar a primeira mensagem nessa sessão deve persistir no novo arquivo, não apenas simular um chat novo na UI;
- o retorno de `POST /api/sessions` deve conter o id real, diretório real e metadata real.
### Objetivo 4 — histórico por pastas/projetos
Agrupar sessões no histórico lateral por projeto:
- cada pasta representa um `cwd`/diretório de projeto vindo do header da sessão;
- nome visual da pasta = basename do diretório, com fallback legível;
- pastas podem expandir/recolher;
- sessões ficam dentro da pasta do projeto;
- busca continua funcionando;
- seleção e drag/drop de sessão continuam funcionando;
- sem reorganizar o backend inteiro de sessões.
## Não objetivos
- Não implementar TTS/voz falada nesta fase.
- Não criar sistema multiusuário, login ou permissões finas.
- Não abrir o servidor LAN por padrão.
- Não publicar automaticamente a extensão.
- Não renomear todo o legado `opencode`/OpenCodeUI agora.
- Não refatorar `SessionList.tsx` inteiro além do necessário para agrupamento por projeto.
- Não alterar arquitetura de pane layout, terminal, file explorer ou MCP além dos pontos necessários.
## Decisões recomendadas
### A. STT em duas fases, mas com base única
**Recomendado:** implementar primeiro `file transcription`, com tipos e UI já preparados para `realtime`.
Motivo:
- é mais rápido e testável;
- funciona com OpenAI e provedores OpenAI-compatible;
- evita acoplar a primeira entrega a WebSocket realtime;
- mantém o caminho aberto para `gpt-realtime-whisper`.
Configuração inicial:
```ts
export type SttMode = 'file' | 'realtime'
export type SttProviderKind = 'openai' | 'openai-compatible'
export interface SttSettings {
enabled: boolean
providerKind: SttProviderKind
mode: SttMode
baseUrl: string
apiKeyConfigured: boolean
transcriptionEndpoint: string
transcriptionModel: string
language?: string
insertMode: 'append' | 'replace'
}
```
O segredo real da API key não deve ser retornado ao frontend depois de salvo. A UI recebe apenas `apiKeyConfigured: true/false`.
### B. Settings STT server-side local
**Recomendado:** salvar configuração STT em arquivo local do usuário:
```text
~/.pi/agent/piagentui-settings.json
```
Estrutura mínima:
```json
{
"stt": {
"enabled": true,
"providerKind": "openai-compatible",
"mode": "file",
"baseUrl": "https://api.openai.com/v1",
"apiKey": "...",
"transcriptionEndpoint": "/audio/transcriptions",
"transcriptionModel": "gpt-4o-mini-transcribe",
"language": "pt",
"insertMode": "append"
},
"network": {
"lanAccessEnabled": false
}
}
```
Motivo:
- evita persistir API key no localStorage do browser;
- permite LAN sem expor secrets no `window.PI_BOOT_DATA`;
- mantém tudo local e simples;
- evita banco de dados ou integração de credenciais complexa.
### C. LAN por bind amplo + validação dinâmica
**Recomendado:** o servidor pode escutar em `0.0.0.0`, mas responder a clientes não-loopback apenas quando `network.lanAccessEnabled === true`.
Mudanças necessárias:
- `listenOnAvailablePort(server, DEFAULT_PORT, '0.0.0.0')`;
- substituir validação loopback-only por função configurável:
- loopback sempre permitido;
- LAN permitido apenas se `lanAccessEnabled`;
- `Origin` deve bater com host loopback ou LAN permitido;
- `serveIndexHtml(req, res)` deve montar `baseUrl` a partir de `req.headers.host`, não sempre `127.0.0.1`;
- endpoint `GET /api/network/access` retorna estado, IPs e URLs;
- endpoint `POST /api/network/access` liga/desliga LAN.
Acesso LAN expõe controle do agente para quem estiver na rede e conseguir acessar a URL. A UI deve mostrar warning explícito.
### D. QR Code no frontend
**Recomendado:** gerar o QR Code no frontend a partir da URL LAN ativa.
Opções:
1. usar dependência pequena `qrcode`;
2. implementar QR manualmente;
3. usar serviço externo.
Decisão: usar `qrcode` se não houver lib existente. É a solução mais simples, testável e offline. Evitar serviço externo por privacidade.
### E. Criar sessão real sem mexer no core do Pi
O contexto regular da extensão tem `switchSession`, mas `newSession()` fica restrito ao command context do Pi. Para não mexer no core do Pi agora, o backend do PiAgentUI pode criar o arquivo `.jsonl` compatível com `SessionManager.newSession()` e depois chamar `latestCtx.switchSession(newSessionFile, { withSession })`.
Header da sessão compatível:
```json
{
"type": "session",
"version": 3,
"id": "<novo-id>",
"timestamp": "<iso-date>",
"cwd": "<diretorio-do-projeto>",
"parentSession": "<opcional>"
}
```
Fluxo:
1. receber `POST /api/sessions` com `{ directory?, title?, parentID? }`;
2. resolver `cwd = directory || latestCtx.cwd || process.cwd()`;
3. gerar id válido e filename com timestamp;
4. escrever arquivo `.jsonl` com header;
5. se `title` existir, opcionalmente adicionar entry `session_info` compatível com o parser atual;
6. trocar rapidamente para o novo arquivo com `latestCtx.switchSession()`;
7. atualizar `latestCtx` no `withSession`;
8. retornar metadata real lida de `readSessionMetadata()`.
Se `switchSession` não estiver disponível, retornar `501` em vez de fingir sucesso.
### F. Histórico por projeto no frontend
**Recomendado:** agrupar no frontend usando `session.directory` já retornado pelo backend.
Criar helper puro:
```text
src/features/sessions/sessionProjectGroups.ts
```
Responsabilidades:
- normalizar paths Windows/Linux;
- extrair nome do projeto pelo basename;
- criar chave estável por diretório normalizado;
- ordenar grupos por `max(session.time.updated)` desc;
- ordenar sessões dentro do grupo por `time.updated` desc;
- manter busca como modo flat ou grupos filtrados.
A UI pode adicionar modo `groupedByProject?: boolean` em `SessionList`, preservando o agrupamento temporal antigo se necessário.
Estado de expansão:
- store local leve em `localStorage`, por exemplo `piagentui-session-project-folders`;
- padrão: pasta do projeto atual expandida; demais recolhidas;
- se busca ativa, expandir todos os grupos com match.
## Endpoints propostos
### STT
#### `GET /api/settings/stt`
Retorna settings redigidos:
```json
{
"enabled": true,
"providerKind": "openai-compatible",
"mode": "file",
"baseUrl": "https://api.openai.com/v1",
"apiKeyConfigured": true,
"transcriptionEndpoint": "/audio/transcriptions",
"transcriptionModel": "gpt-4o-mini-transcribe",
"language": "pt",
"insertMode": "append"
}
```
#### `POST /api/settings/stt`
Atualiza settings. Se `apiKey` vier vazia/ausente, mantém chave anterior; se vier `null`, remove.
#### `POST /api/stt/transcriptions`
Recebe áudio como `multipart/form-data`, chama provider configurado e retorna:
```json
{
"text": "texto transcrito",
"durationMs": 1234,
"provider": "openai-compatible",
"model": "gpt-4o-mini-transcribe"
}
```
### LAN/QR
#### `GET /api/network/access`
```json
{
"lanAccessEnabled": false,
"port": 58785,
"localUrl": "http://127.0.0.1:58785",
"lanUrls": ["http://192.168.0.10:58785"],
"primaryLanUrl": "http://192.168.0.10:58785"
}
```
#### `POST /api/network/access`
```json
{ "lanAccessEnabled": true }
```
Retorna o mesmo shape do `GET`.
### Sessões
#### `POST /api/sessions`
Novo comportamento: cria sessão real.
Body:
```json
{
"directory": "D:/Documents/Github/PiAgentUI/PiAgentUi",
"title": "Nova conversa",
"parentID": "opcional"
}
```
Response:
```json
{
"id": "019...",
"title": "Nova conversa",
"directory": "D:/Documents/Github/PiAgentUI/PiAgentUi",
"time": { "created": 123, "updated": 123 },
"status": { "type": "idle" },
"summary": { "deletions": 0, "files": 0, "additions": 0 }
}
```
## Componentes/frontend propostos
### STT
Criar:
- `src/api/stt.ts` — chamadas de settings e transcrição;
- `src/types/api/stt.ts` — tipos de settings/resposta;
- `src/hooks/useVoiceTranscription.ts` — MediaRecorder, estados e envio para backend;
- `src/features/chat/input/VoiceInputButton.tsx` — botão mic/stop/loading/error;
- alterações pontuais em `InputBox.tsx` e `InputToolbar.tsx` para inserir texto transcrito.
Estados mínimos:
```ts
type VoiceState =
| 'idle'
| 'requesting-permission'
| 'recording'
| 'transcribing'
| 'error'
```
### LAN/QR
Criar:
- `src/api/network.ts`;
- `src/types/api/network.ts`;
- `src/features/settings/components/NetworkAccessSettings.tsx` ou adicionar seção pequena em `ServersSettings.tsx`;
- QR gerado a partir de `primaryLanUrl`.
Como o app já tem `ServersSettings.tsx`, a mudança mínima é adicionar uma seção “Local network access” dentro dele, sem criar novo painel de settings se não for necessário.
### Sessões por projeto
Criar:
- `src/features/sessions/sessionProjectGroups.ts`;
- testes unitários para grouping;
- alterar `SessionList.tsx` para renderizar project folders quando `groupedByProject` estiver ativo;
- adicionar traduções apenas nos namespaces já usados (`chat`/`commands`/`common`) conforme necessário.
## Segurança
- LAN fica desligado por padrão.
- Loopback continua sempre permitido.
- API continua exigindo bearer token.
- O HTML servido injeta o token apenas para o cliente que abriu a UI; isso significa que qualquer pessoa com acesso à URL LAN durante LAN enabled pode controlar o agente. A UI deve deixar isso claro.
- STT API key não deve aparecer em resposta de settings.
- Não logar API key.
- Não aceitar `baseUrl` sem `http:` ou `https:`.
- Limitar upload STT por tamanho. Sugestão inicial: 25 MB.
- Reusar `isRequestBodySizeAllowed` ou criar limite específico para multipart.
- Não permitir path traversal ou file reads novos.
## Plano de testes obrigatório
### Backend extension
Arquivo: `extensions/piagentui-server.test.ts`
Casos:
1. `POST /api/sessions` cria arquivo `.jsonl` com header `type=session`, `version=3`, `cwd` correto e id novo.
2. `POST /api/sessions` chama `switchSession()` com `withSession` e retorna metadata da sessão nova.
3. `POST /api/sessions` retorna `501` quando `switchSession` indisponível, sem fingir sucesso.
4. `GET /api/network/access` retorna loopback URL e lista de IPs LAN mockável.
5. LAN disabled rejeita host não-loopback.
6. LAN enabled aceita host LAN com bearer token.
7. `serveIndexHtml()` injeta `baseUrl` baseado no host da request.
8. `GET /api/settings/stt` redige `apiKey`.
9. `POST /api/settings/stt` salva settings e preserva/remover chave conforme contrato.
10. `POST /api/stt/transcriptions` valida provider config e trata erro do provider sem vazar segredo.
Arquivo: `extensions/piagentui-server-core.test.ts`
Casos:
1. `isLoopbackHost()` preserva comportamento atual.
2. nova validação aceita LAN apenas quando enabled.
3. `getLocalNetworkUrls()` ignora loopback, IPv6 local problemático e interfaces internas inválidas.
4. validação de URL STT recusa protocolos não HTTP(S).
### Frontend API/hooks
Arquivos novos:
- `src/api/stt.test.ts`;
- `src/api/network.test.ts`;
- `src/hooks/useVoiceTranscription.test.tsx`;
- `src/features/sessions/sessionProjectGroups.test.ts`.
Casos:
1. API STT envia/recebe shape correto.
2. API network chama endpoints corretos.
3. hook de voz passa por `idle -> recording -> transcribing -> idle` e chama callback com texto final.
4. hook trata permissão negada.
5. grouping por projeto cria pastas corretas para paths Windows e POSIX.
6. grouping ordena pastas por sessão mais recente.
7. grouping mantém sessões ordenadas por update.
### Componentes
Arquivos prováveis:
- `src/features/chat/input/VoiceInputButton.test.tsx`;
- `src/features/settings/components/ServersSettings.test.tsx` ou novo test de `NetworkAccessSettings`;
- `src/features/sessions/SessionList.test.tsx`.
Casos:
1. botão de microfone mostra estados e dispara start/stop.
2. seção LAN mostra URL e QR quando enabled.
3. SessionList renderiza pastas por projeto, expande/recolhe e preserva seleção.
## Ordem de implementação recomendada
1. **Sessões reais primeiro** — corrige fundação crítica de chat.
2. **Agrupamento por projeto** — depende de sessões reais/metadata confiável.
3. **LAN/QR** — isolado, mas tem implicações de segurança.
4. **STT file transcription** — maior superfície; deve entrar depois da base de rede/sessão estar sólida.
5. **Realtime STT** — fase seguinte, usando o contrato já criado.
## Critérios de aceite
### Sessões reais
- Clicar em New chat cria um novo arquivo `.jsonl` de sessão Pi.
- O id retornado pelo backend é diferente da sessão anterior.
- Enviar mensagem após criar novo chat persiste nessa nova sessão.
- Troca de sessão continua rápida e não perde SSE.
### Histórico por projeto
- Sidebar mostra pastas por projeto.
- Cada pasta contém sessões daquele `cwd`.
- Expand/recolher funciona.
- Busca ainda localiza sessões.
- Selecionar sessão dentro da pasta navega para o chat correto.
### LAN/QR
- Com LAN off, acesso por IP LAN recebe 403.
- Com LAN on, URL LAN abre a UI em outro dispositivo da rede.
- QR Code aponta para a URL LAN ativa.
- API continua exigindo token.
- UI mostra aviso de segurança.
### STT
- Usuário consegue gravar áudio e transcrever para texto.
- Texto final entra no composer sem auto-send.
- Provider OpenAI-compatible é configurável.
- API key não aparece em respostas/logs.
- Erros de permissão/rede/provider são visíveis e recuperáveis.
## Riscos e mitigação
| Risco | Mitigação |
| --- | --- |
| LAN expõe controle total do agente | default off, warning explícito, bearer obrigatório, permitir desligar rápido |
| Criar sessão manualmente diverge do Pi core | usar header exatamente compatível com `SessionManager.newSession()`; testes contra parser atual |
| `switchSession` indisponível em runtime antigo | retornar `501`, não simular sucesso |
| QR exige dependência nova | usar `qrcode` pequena e isolada; se rejeitado, renderizar URL primeiro e QR depois |
| STT com CORS se chamado direto do frontend | chamadas a providers sempre pelo backend local |
| API key em LAN | armazenar server-side local, responder apenas `apiKeyConfigured` |
| `SessionList.tsx` já é grande | extrair grouping puro para arquivo separado; modificar renderização o mínimo possível |
## Arquivos previstos
### Criar
- `src/api/stt.ts`
- `src/api/stt.test.ts`
- `src/types/api/stt.ts`
- `src/api/network.ts`
- `src/api/network.test.ts`
- `src/types/api/network.ts`
- `src/hooks/useVoiceTranscription.ts`
- `src/hooks/useVoiceTranscription.test.tsx`
- `src/features/chat/input/VoiceInputButton.tsx`
- `src/features/chat/input/VoiceInputButton.test.tsx`
- `src/features/sessions/sessionProjectGroups.ts`
- `src/features/sessions/sessionProjectGroups.test.ts`
### Modificar
- `extensions/piagentui-server.ts`
- `extensions/piagentui-server.test.ts`
- `extensions/piagentui-server-core.ts`
- `extensions/piagentui-server-core.test.ts`
- `src/features/chat/InputBox.tsx`
- `src/features/chat/input/InputToolbar.tsx`
- `src/features/sessions/SessionList.tsx`
- `src/features/sessions/SessionList.test.tsx`
- `src/features/settings/components/ServersSettings.tsx`
- `src/store/serverStore.ts` apenas se necessário para refletir LAN URL ativa
- `src/locales/en/*.json` e `src/locales/zh-CN/*.json` para labels mínimos
- `package.json` se a dependência `qrcode` for adotada
## Abordagens consideradas
### Abordagem 1 — implementar tudo no frontend
Rejeitada. STT direto do browser expõe API keys, sofre com CORS e não combina com LAN.
### Abordagem 2 — mexer no core do Pi para expor `newSession` ao extension context
Possível, mas maior. Já fizemos patch de `switchSession`; para este pacote o objetivo é mudança mínima no PiAgentUI. Só considerar se criação manual de sessão se mostrar incompatível.
### Abordagem 3 — backend PiAgentUI como orquestrador local
Recomendada. Mantém mudanças dentro do PiAgentUI, usa `switchSession` já disponível, preserva segurança local e permite testes focados.
## Gate de implementação
Esta spec cobre múltiplos subsistemas e altera segurança/rede/sessão. A implementação deve começar somente após aprovação explícita do usuário sobre este design.
Após aprovação, criar plano em:
```text
docs/superpowers/plans/2026-05-30-piagentui-stt-lan-real-sessions-project-folders.md
```
O plano deve usar TDD e commits pequenos por feature:
1. sessões reais;
2. agrupamento por projeto;
3. LAN/QR;
4. STT settings + file transcription;
5. voice UI;
6. validação final e revisão de código.
@@ -0,0 +1,87 @@
# Design — Controle Git no composer do PiAgentUI
## Objetivo
Adicionar à bolha de envio de mensagens um controle Git compacto, localizado no canto inferior esquerdo da toolbar do composer, mostrando a branch atual e abrindo um popover com branches e operações Git principais.
## Direção visual aprovada
- O controle aparece como um chip integrado à `InputToolbar`, antes dos seletores de agente e variante.
- O chip usa iconografia GitHub/Octicons, texto monoespaçado, estado dirty e chevron.
- Ao clicar, abre um popover acima da bolha de input, alinhado ao chip, seguindo o padrão visual de `DropdownMenu`: glass, borda sutil, radius alto e sombra do app.
- O popover tem duas áreas:
- lista de branches local/remota e ação de nova branch;
- status, ações Git e área dedicada de commit.
- As ações principais usam ícones: branch, commit, diff/status, pull, push, refresh, stage/unstage, arquivos e alertas.
## Escopo MVP seguro
Inclui:
- carregar branch atual, branches locais/remotas e status resumido;
- trocar branch local/remota;
- criar branch a partir da atual;
- listar arquivos alterados;
- stage/unstage arquivo;
- stage all/unstage all;
- commit com textarea dedicada;
- pull e push explícitos;
- refresh de status;
- mensagens de erro e loading.
Não inclui no MVP:
- merge manual;
- rebase;
- reset hard;
- revert;
- stash;
- force push;
- edição interativa de hunks.
## Segurança e comportamento
- Backend executa Git via `spawn`/`execFile` com argv fixo, nunca shell arbitrário.
- Diretório vem do projeto atual e precisa ser repositório Git válido.
- Commit exige mensagem não vazia e pelo menos um arquivo staged.
- Pull/push são ações explícitas no popover.
- Erros do Git são retornados como mensagens sanitizadas.
- Operações atualizam status e branch após sucesso.
## Componentes
- `GitComposerControl`: chip + popover Git do composer.
- `src/api/vcs.ts`: funções de API para status, branches e ações.
- `src/types/api/vcs.ts`: contratos frontend/backend.
- `extensions/piagentui-git-manager.ts`: camada backend focada em Git.
- `extensions/piagentui-server.ts`: endpoints `/api/vcs/*`.
## Endpoints planejados
- `GET /api/vcs/info`
- `GET /api/vcs/branches`
- `GET /api/vcs/status`
- `POST /api/vcs/checkout`
- `POST /api/vcs/branch`
- `POST /api/vcs/stage`
- `POST /api/vcs/unstage`
- `POST /api/vcs/commit`
- `POST /api/vcs/pull`
- `POST /api/vcs/push`
## Testes
- Testes unitários do git manager com runner fake.
- Testes de API frontend (`src/api/vcs.test.ts`).
- Teste de componente para renderizar branch, abrir popover, preencher commit e disparar ações.
- Testes de servidor para endpoints principais com mocks.
## Referência de ícones
Usar `@primer/octicons-react`, pacote oficial do Primer/GitHub Octicons. A documentação oficial mostra importação direta, por exemplo:
```ts
import { GitCommitIcon } from '@primer/octicons-react'
```
Fonte consultada via Context7: `https://primer.style/octicons/icon/git-commit-24` e `https://primer.style/octicons/code`.