Files
PiAgentUI/docs/superpowers/specs/2026-05-30-piagentui-stt-lan-real-sessions-project-folders-design.md
T
Evandro b3489e1ede
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
🎉 Initial commit: PiAgentUI — AI Agent Desktop & Web UI
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
2026-06-01 00:50:06 -03:00

566 lines
19 KiB
Markdown

# 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.