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

19 KiB

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:

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:

~/.pi/agent/piagentui-settings.json

Estrutura mínima:

{
  "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:

{
  "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:

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:

{
  "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:

{
  "text": "texto transcrito",
  "durationMs": 1234,
  "provider": "openai-compatible",
  "model": "gpt-4o-mini-transcribe"
}

LAN/QR

GET /api/network/access

{
  "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

{ "lanAccessEnabled": true }

Retorna o mesmo shape do GET.

Sessões

POST /api/sessions

Novo comportamento: cria sessão real.

Body:

{
  "directory": "D:/Documents/Github/PiAgentUI/PiAgentUi",
  "title": "Nova conversa",
  "parentID": "opcional"
}

Response:

{
  "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:

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:

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.