🎉 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,845 @@
# Pi Agent Extension Migration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Migrate PiAgentUi from OpenCode to Pi Agent while preserving the current UI and interaction model.
**Architecture:** Build a Pi package/extension as the primary integration. Pi loads `extensions/piagentui-server.ts`, which starts a loopback local server, serves the existing React build, and exposes an owned compatibility API shaped to the current frontend. Tauri/Rust desktop service cleanup is a second track after the Pi package MVP works.
**Tech Stack:** React 19, Vite 8, TypeScript 5.9, Vitest, Tauri 2/Rust, Pi extension API (`@earendil-works/pi-coding-agent`), Node HTTP, SSE/WebSocket event streaming.
---
## Decisions Required Before Implementation
Default recommendations are listed; please confirm or override them before code changes.
1. **MVP session model:** recommended = active Pi session mirror first. The UI controls the currently running Pi session; full session CRUD/fork/switch is deferred.
2. **Desktop/Tauri scope:** recommended = defer process spawning. Tauri can connect to an already-running Pi extension server later, but Track A must not depend on Tauri.
3. **Unsupported features:** recommended = disable/stub PTY, MCP, LSP, provider OAuth, worktrees, and multi-session operations until Pi equivalents are verified.
4. **Token/bootstrap:** recommended = same-origin injected boot data in HTML served by the extension; bearer token stays in memory, never query strings/localStorage.
5. **Endpoint convention:** recommended = keep OpenCode-shaped endpoint paths/envelopes where the current frontend already expects them, then gradually move wrappers to owned names internally.
6. **Product name:** needs user decision. Options: `PiAgentUi`, `Pi Agent UI`, or another exact name.
Implementation should not begin until these are confirmed.
---
## Non-Negotiable Scope
- No UI redesign.
- No `/hud` command.
- No `Ctrl+Shift+H` HUD toggle.
- Remove OpenCode runtime usage completely by final migration.
- Remove `@opencode-ai/sdk` by final migration.
- Reuse user Pi settings/auth/model setup; do not collect provider credentials in PiAgentUi.
- Use `better-tau/extensions/mirror-server.ts` as the main reference for a Pi extension serving HTTP/WebSocket from inside Pi.
- Current known `better-tau` unrelated changes must not be touched: `.gitignore`, `extensions/helper-bridge.ts`, `extensions/helper/`.
---
## Core Corrections from Critical Review
- **Do not conflate Pi package and Tauri app.** Track A is Pi package/extension; Track B is desktop cleanup.
- **Do not flatten message DTOs.** Current UI expects `MessageWithParts { info, parts }`, and many callsites read `message.info.*`.
- **Do not emit raw event envelopes unless `events.ts` is rewritten first.** Current frontend accepts `{ directory, payload: { type, properties } }` from `/global/event`.
- **Use actual Pi lifecycle events.** The shutdown event is `session_shutdown`, not `session_end`.
- **Typecheck extension files.** Existing `tsc -b` does not include `extensions/**/*.ts`.
- **Resolve token bootstrap early.** The UI cannot call protected APIs unless it receives base URL/token safely.
- **Treat `pi.sendUserMessage(...)` as asynchronous fire-and-stream.** It should return `202 accepted`; real message IDs arrive through events.
---
## File/Module Responsibility Map
### New files
- `extensions/piagentui-server.ts`
- Pi extension entrypoint.
- Starts loopback HTTP/SSE/WS server.
- Serves built `dist/` assets.
- Injects safe runtime boot data into `index.html`.
- Bridges Pi session lifecycle and messages to current frontend-compatible events.
- `extensions/piagentui-server-core.ts`
- Pure helpers for testable server behavior: loopback validation, auth validation, body-size guard, path traversal prevention, event envelope helpers.
- `extensions/piagentui-server-core.test.ts`
- Unit tests for server security helpers.
- `tsconfig.extension.json`
- TypeScript project for extension files and extension tests/helpers.
- `src/api/piClient.ts`
- Typed fetch client replacing SDK usage.
- Sends local bearer auth in headers only.
- Normalizes typed errors.
- `src/api/piClient.test.ts`
- Request/auth/error behavior tests.
- `src/types/api/compat.ts`
- Owned compatibility DTOs shaped for current UI.
- `src/api/piContract.ts`
- Owned request/response contracts and current event envelope definitions.
- `src/api/piEventTranslator.test.ts`
- Contract tests for frontend event processing.
- `docs/superpowers/specs/2026-05-28-pi-agent-extension-migration-design.md`
- Design spec, endpoint matrix, unsupported features, security, acceptance criteria.
### Modified files
- `package.json`
- Add `pi.extensions`.
- Add package metadata needed for Pi package usage.
- Add extension typecheck script.
- Add/remove dependencies in the correct sequence.
- Remove `@opencode-ai/sdk` when no imports remain.
- `tsconfig.json`
- Include `tsconfig.extension.json` in references.
- `vite.config.ts`
- Remove OpenCode dev proxy when API layer no longer uses OpenCode server.
- Add Pi extension dev proxy only if chosen.
- `src/constants/api.ts`, `src/api/http.ts`, `src/store/serverStore.ts`
- Convert from OpenCode server/basic-auth concepts to PiAgentUi local server boot/connection state.
- No provider credential storage.
- `src/api/sdk.ts`
- Delete after all imports are removed, or temporarily replace with a non-SDK compatibility facade.
- `src/api/*.ts`
- Replace SDK calls with owned client calls or unsupported-feature stubs.
- Files likely in scope: `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`.
- `src/types/api/*.ts`
- Replace SDK-derived type aliases with owned DTOs or explicit unsupported types.
- Files likely in scope: `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`.
- `src/main.tsx`, `src/features/settings/components/ServiceSettings.tsx`, `src/hooks/useCloseServiceDialog.ts`, `src/store/serviceStore.ts`
- Track A: ensure OpenCode auto-start does not run while Pi extension mode is active.
- Track B: rename desktop service integration.
- `src-tauri/src/app/commands/opencode.rs`, `src-tauri/src/app/commands/mod.rs`, `src-tauri/src/app/mod.rs`
- Track B only: rename commands and remove OpenCode process management.
- `src-tauri/src/app/bridge/args.rs`, `src-tauri/src/app/commands/bridge.rs`
- Track B only: enforce loopback URL allowlist in Rust bridge.
- `src-tauri/capabilities/default.json`, `src-tauri/tauri.conf.json`, `src-tauri/Cargo.toml`
- Track B only: remove OpenCode branding and tighten security.
---
## Compatibility Contract
### DTO shape must match current UI
Current UI expects message shape similar to:
```ts
export interface MessageWithParts {
info: Message
parts: Part[]
}
```
Therefore owned DTOs must preserve this shape first, then adapt internally:
```ts
export interface CompatMessageInfo {
id: string
sessionID: string
role: 'user' | 'assistant'
time?: { created?: number; completed?: number }
summary?: string
modelID?: string
providerID?: string
}
export interface CompatMessageWithParts {
info: CompatMessageInfo
parts: CompatPart[]
}
export type CompatPart =
| CompatTextPart
| CompatReasoningPart
| CompatToolPart
| CompatFilePart
| CompatStepStartPart
| CompatStepFinishPart
| CompatUnsupportedPart
export interface CompatTextPart {
id: string
sessionID: string
messageID: string
type: 'text'
text: string
}
export interface CompatReasoningPart {
id: string
sessionID: string
messageID: string
type: 'reasoning'
text: string
}
export interface CompatToolPart {
id: string
sessionID: string
messageID: string
type: 'tool'
tool: string
state: { status: 'pending' | 'running' | 'completed' | 'error'; input?: unknown; output?: unknown; error?: string }
}
export interface CompatFilePart {
id: string
sessionID: string
messageID: string
type: 'file'
mime?: string
filename?: string
url?: string
}
export interface CompatStepStartPart {
id: string
sessionID: string
messageID: string
type: 'step-start'
}
export interface CompatStepFinishPart {
id: string
sessionID: string
messageID: string
type: 'step-finish'
}
export interface CompatUnsupportedPart {
id: string
sessionID: string
messageID: string
type: 'unsupported'
label: string
data?: unknown
}
```
Exact fields must be finalized by a field inventory before implementation.
### Event envelope must match current frontend first
Current `src/api/events.ts` expects:
```ts
export interface CompatGlobalEvent {
directory?: string
payload: {
type: string
properties: unknown
}
}
```
For MVP, the extension should emit this envelope on the current path or the frontend parser must be rewritten before server events are implemented.
Recommended MVP: keep `/global/event` and OpenCode-style event names already handled by the UI:
- `server.connected`
- `session.updated`
- `session.status`
- `session.idle`
- `session.error`
- `message.updated`
- `message.part.updated`
- `message.part.delta`
- `message.part.removed`
- `permission.asked` only if mapped; otherwise unsupported notification
- `question.asked` only if mapped; otherwise unsupported notification
For text deltas, current store expects payload shape compatible with its `PartDeltaPayload`, including `sessionID`, `messageID`, `partID`, `field`, and `delta`.
### Send message contract
Current frontend send params include:
```ts
interface SendMessageParams {
sessionId: string
text: string
attachments: Attachment[]
model: { providerID: string; modelID: string }
agent?: string
variant?: string
directory?: string
}
```
MVP behavior:
- `text`: supported.
- `directory`: supported only as metadata/active project hint if Pi context allows it.
- `model`: support only if Pi allows model switch before send; otherwise ignore with warning event or reject with unsupported error.
- `attachments`: defer unless image/file input mapping is verified.
- `agent`, `variant`: defer unless Pi equivalent is verified.
- response: `202 accepted` plus optional synthetic client request ID; real assistant message ID is learned from events.
---
## Endpoint Coverage Matrix
### MVP supported
| Capability | Existing frontend area | Compatibility path | Backend source |
| ----------------------- | --------------------------------- | ---------------------------------------------------- | ------------------------------------- |
| Health | `serverStore`, `client.ts` | `/global/health` or current health path | extension runtime state |
| Event stream | `events.ts` | `/global/event` | Pi extension lifecycle/events |
| Active models | `client.ts`, model hooks | wrapper-owned endpoint, final path decided in Task 1 | Pi model registry/settings |
| Active session snapshot | `session.ts`, stores | wrapper-owned endpoint, OpenCode-shaped response | active Pi context/session mirror |
| Send message | `message.ts`, `useChatSession.ts` | wrapper-owned endpoint | `pi.sendUserMessage(...)` |
| Abort turn | message/session controls | wrapper-owned endpoint | current Pi context abort if available |
| Static assets | browser/webview | `/`, `/assets/*` | extension static server |
### Explicitly unsupported/deferred for MVP
- PTY terminal sessions.
- MCP management.
- LSP diagnostics endpoints.
- Provider OAuth/login.
- Full multi-session create/fork/switch/delete.
- Worktrees.
- VCS operations unless trivially provided by current UI independent of OpenCode.
- Permission/question flows unless Pi extension UI/RPC gives equivalent semantics.
- Share/export unless Pi exposes safe API from extension context.
- Config, project, agent, skill, todo, tool, file APIs that currently depend on OpenCode SDK.
Every deferred feature must return a typed unsupported error and must not call an OpenCode endpoint.
---
## Security Contract
Required for Track A:
- Bind only to `127.0.0.1` and optionally `::1`.
- Use OS-selected random high port by default.
- Generate per-run bearer token.
- Bootstrap token via same-origin injected boot data in served HTML, unless user chooses another mechanism.
- Keep bearer token in memory/session only; do not store it in `localStorage` or settings backups.
- Reject API and stream requests without `Authorization: Bearer <token>`.
- For browser-native SSE/WS where custom headers are constrained, prefer same-origin cookie/header strategy from extension-served page; do not put token in query strings or userinfo URLs.
- Validate `Host` and `Origin`.
- No wildcard CORS.
- Limit request body and WS frame sizes.
- Reject static path traversal.
- Never send provider API keys/OAuth tokens to browser.
- Never log local bearer token, provider secrets, or prompts by default.
- Close HTTP server and sockets on `session_shutdown`.
Required for Track B if touched:
- Rust bridge must validate URLs server-side, not only via Tauri capabilities.
- Allow only `http://127.0.0.1:*`, `http://localhost:*`, `ws://127.0.0.1:*`, `ws://localhost:*` as appropriate.
- Reject userinfo URLs, LAN IPs, non-loopback hosts, and external HTTPS URLs.
- Narrow `src-tauri/capabilities/default.json` from broad `http://**` / `https://**` to loopback-only.
- Reintroduce CSP instead of `csp: null` if compatible with current app.
---
## Validation Contract
Run during implementation milestones:
```bash
cd PiAgentUi
npm run typecheck
npm run lint
npm run test:run
npm run build
```
After extension tsconfig is added:
```bash
cd PiAgentUi
npm run typecheck:extensions
```
If Track B is touched:
```bash
cd PiAgentUi/src-tauri
cargo test
```
OpenCode cleanup check must use an allowlist because docs may mention migration history:
```bash
cd PiAgentUi
rg -n "@opencode-ai/sdk|getSDKClient|unwrap\(|opencode serve|OpenCode|opencode" src src-tauri extensions package.json vite.config.ts
```
Expected final runtime result:
- no `@opencode-ai/sdk` imports;
- no `opencode serve` launcher;
- no OpenCode user-facing app title;
- only temporary deprecated aliases if Track B explicitly keeps them for one release.
Pi package smoke:
```bash
cd PiAgentUi
npm run build
npm pack --dry-run
```
Then install/load the package in Pi using the local package workflow chosen for this repo and verify:
1. Pi loads the extension.
2. Extension serves the UI.
3. UI receives boot data.
4. UI sends a message to current Pi session.
5. Assistant response streams into existing chat UI.
6. Refresh/reconnect recovers current snapshot.
7. Invalid HTTP/SSE/WS auth is rejected.
---
## Implementation Tasks
### Task 1: Confirm Decisions and Write Design Spec
**Files:**
- Create: `docs/superpowers/specs/2026-05-28-pi-agent-extension-migration-design.md`
- [ ] Confirm all decisions in “Decisions Required Before Implementation”.
- [ ] Write the design spec with: MVP, non-goals, endpoint matrix, DTO/event contract, security contract, validation contract, unsupported-feature behavior, Track A/Track B split.
- [ ] Include exact better-tau reference: `D:/Documents/Github/PiAgentUI/better-tau/extensions/mirror-server.ts`.
- [ ] Run:
```bash
cd PiAgentUi
npm run typecheck
```
Expected: unchanged project still typechecks before migration starts.
### Task 2: Inventory Current Frontend API Fields and SDK Coupling
**Files:**
- Create: `docs/superpowers/specs/2026-05-28-pi-agent-ui-field-inventory.md`
- [ ] Run:
```bash
cd PiAgentUi
rg -n "@opencode-ai/sdk|getSDKClient|unwrap\(|from './sdk'|from '../api/sdk'|Client\[|SDK[A-Za-z]" src
```
- [ ] Inventory all files importing SDK-derived types or `getSDKClient`.
- [ ] Inventory consumed fields for:
- `Session`
- `MessageWithParts`
- `Message.info`
- every `Part` variant rendered by components/stores
- `GlobalEvent`
- `PartDeltaPayload`
- `SendMessageParams`
- [ ] Mark each API module as: supported, stubbed, hidden, or deferred.
- [ ] Do not edit runtime code in this task.
### Task 3: Add Owned Compatibility Types Without Breaking UI Shape
**Files:**
- Create: `src/types/api/compat.ts`
- Create: `src/api/piContract.ts`
- Modify: `src/types/api/message.ts`, `src/types/api/event.ts`, `src/types/api/session.ts` only after inventory confirms exact fields.
- [ ] Add owned compatibility DTOs preserving current UI shape.
- [ ] Replace SDK aliases gradually with owned DTO aliases that match current field names.
- [ ] Do not flatten `MessageWithParts`.
- [ ] Keep `GlobalEvent` envelope compatible with current `events.ts` unless rewriting `events.ts` in the same task.
- [ ] Run:
```bash
cd PiAgentUi
npm run typecheck
npm run test:run -- src/store/messageStore.test.ts src/api/events.test.ts src/hooks/useChatSession.test.tsx
```
Expected: failures only from intentional type differences fixed before proceeding.
### Task 4: Add Extension Typecheck and Package Metadata
**Files:**
- Create: `tsconfig.extension.json`
- Modify: `tsconfig.json`
- Modify: `package.json`
- [ ] Add `tsconfig.extension.json` including `extensions/**/*.ts`.
- [ ] Add script:
```json
"typecheck:extensions": "tsc -p tsconfig.extension.json --noEmit"
```
- [ ] Include `typecheck:extensions` in `validate` after extension files exist.
- [ ] Add Pi package metadata:
```json
"pi": {
"extensions": ["./extensions/piagentui-server.ts"]
}
```
- [ ] Add package contents/prepack strategy so `dist/` and `extensions/` are included when packed/installed.
- [ ] Decide dependency strategy for `@earendil-works/pi-coding-agent`:
- runtime should be supplied by Pi package loader if supported;
- local typecheck still needs a devDependency or path-compatible type resolution.
- [ ] Run:
```bash
cd PiAgentUi
npm run typecheck
npm run typecheck:extensions
npm pack --dry-run
```
### Task 5: Implement Extension Server Core Helpers and Tests
**Files:**
- Create: `extensions/piagentui-server-core.ts`
- Create: `extensions/piagentui-server-core.test.ts`
- [ ] Write tests first for:
- loopback host accepted;
- LAN/external host rejected;
- userinfo URL rejected;
- malformed auth rejected;
- valid bearer auth accepted;
- Origin/Host mismatch rejected;
- static path traversal rejected;
- body-size guard rejects oversized body;
- boot data injection does not put token in URL.
Use test token construction that is obviously fake and not a credential:
```ts
const sampleToken = ['test', 'token', 'value'].join('-')
```
- [ ] Implement pure helpers to pass tests.
- [ ] Run:
```bash
cd PiAgentUi
npm run test:run -- extensions/piagentui-server-core.test.ts
npm run typecheck:extensions
```
### Task 6: Implement Pi Extension Server Skeleton
**Files:**
- Create: `extensions/piagentui-server.ts`
- Modify: extension tests/helpers as needed
- [ ] Implement extension entrypoint importing `ExtensionAPI` from `@earendil-works/pi-coding-agent`.
- [ ] Start server on `session_start` or the earliest correct lifecycle event.
- [ ] Cleanup on `session_shutdown`.
- [ ] Bind loopback on random high port.
- [ ] Serve `dist/index.html`, injecting boot data same-origin.
- [ ] Serve `/assets/*` safely.
- [ ] Implement authenticated health endpoint compatible with frontend/serverStore path decision.
- [ ] Run:
```bash
cd PiAgentUi
npm run typecheck:extensions
npm run build
npm pack --dry-run
```
### Task 7: Replace Basic Auth Server Store with Pi Local Boot State
**Files:**
- Modify: `src/store/serverStore.ts`
- Modify: `src/store/serverStore.test.ts`
- Modify: `src/api/http.ts`
- Modify: `src/constants/api.ts`
- [ ] Remove provider/server password persistence from active Pi mode.
- [ ] Store only base URL and in-memory/session-scoped local token.
- [ ] Add migration tests proving old Basic auth/password values are not exported in backups after migration.
- [ ] Ensure auth headers use bearer token and never query strings.
- [ ] Run:
```bash
cd PiAgentUi
npm run test:run -- src/store/serverStore.test.ts src/api/http.test.ts
npm run typecheck
```
### Task 8: Add `piClient` and Replace SDK Wrapper Entry Points
**Files:**
- Create: `src/api/piClient.ts`
- Create: `src/api/piClient.test.ts`
- Modify: `src/api/client.ts`
- Modify: `src/api/session.ts`
- Modify: `src/api/message.ts`
- [ ] Write `piClient` tests for bearer headers, no token in URL, JSON errors, base URL normalization.
- [ ] Implement `get`, `post`, `delete`, and stream helper if needed.
- [ ] Replace model/session/message wrappers first because they are MVP-supported.
- [ ] Preserve existing exported function names consumed by hooks/components.
- [ ] For send message, return accepted state if Pi returns no message ID; rely on events for message updates.
- [ ] Run:
```bash
cd PiAgentUi
npm run test:run -- src/api/piClient.test.ts src/hooks/useSessions.test.tsx src/hooks/useChatSession.test.tsx src/hooks/useModelSelection.test.tsx
npm run typecheck
```
### Task 9: Implement Pi Event Translation Compatible with Existing `events.ts`
**Files:**
- Modify: `extensions/piagentui-server.ts`
- Create: `src/api/piEventTranslator.test.ts`
- Modify: `src/api/events.ts` only if necessary and covered by tests.
- [ ] Add tests for the current envelope `{ directory, payload: { type, properties } }`.
- [ ] Add tests for `message.part.delta` shape expected by `messageStore`.
- [ ] Map Pi events to existing frontend events:
- turn start -> `session.status` running;
- turn end -> `session.idle` or `session.status` idle;
- message update -> `message.updated`;
- part delta/update -> `message.part.delta` / `message.part.updated`;
- tool events -> tool part updates;
- errors -> `session.error`.
- [ ] Add initial snapshot event on connect.
- [ ] Add reconnect behavior using existing generation/snapshot logic.
- [ ] Run:
```bash
cd PiAgentUi
npm run test:run -- src/api/events.test.ts src/api/sse.test.ts src/api/piEventTranslator.test.ts src/store/messageStore.test.ts
npm run typecheck
```
### Task 10: Gate or Stub All Unsupported OpenCode-Only Features
**Files:**
- Modify: remaining `src/api/*.ts`
- Modify: affected hooks/components only when they crash on unsupported errors.
- [ ] Run:
```bash
cd PiAgentUi
rg -n "getSDKClient|unwrap\(|@opencode-ai/sdk|opencode|OpenCode" src
```
- [ ] For every remaining API module, implement one of:
- supported Pi-backed wrapper;
- typed unsupported error;
- hidden/disabled UI action with existing notification style.
- [ ] Cover at least:
- `agent.ts`
- `command.ts`
- `config.ts`
- `file.ts`
- `global.ts`
- `lsp.ts`
- `mcp.ts`
- `permission.ts`
- `pty.ts`
- `skill.ts`
- `todo.ts`
- `tool.ts`
- `vcs.ts`
- `worktree.ts`
- [ ] Ensure `pty.ts` never embeds credentials/tokens in URLs.
- [ ] Run:
```bash
cd PiAgentUi
npm run test:run
npm run typecheck
```
### Task 11: Prevent OpenCode Auto-Start in Pi Extension Mode
**Files:**
- Modify: `src/main.tsx`
- Modify: `src/store/serviceStore.ts`
- Modify: `src/features/settings/components/ServiceSettings.tsx`
- Modify: `src/hooks/useCloseServiceDialog.ts`
- [ ] Detect Pi extension mode from boot data.
- [ ] In Pi extension mode, do not auto-start `opencode serve`.
- [ ] Hide/disable OpenCode service settings without changing layout structure more than necessary.
- [ ] Do not rename desktop commands yet unless Track B is approved.
- [ ] Run:
```bash
cd PiAgentUi
npm run test:run -- src/store/serviceStore.test.ts src/utils/settingsBackup.test.ts
npm run typecheck
```
### Task 12: Remove `@opencode-ai/sdk` and SDK Facade
**Files:**
- Modify: `package.json`, lockfile if present
- Delete or rewrite: `src/api/sdk.ts`
- Modify: tests that mock SDK
- [ ] Confirm no imports remain:
```bash
cd PiAgentUi
rg -n "@opencode-ai/sdk|getSDKClient|unwrap\(" src
```
- [ ] Remove dependency using the repo's package manager:
```bash
cd PiAgentUi
npm uninstall @opencode-ai/sdk
```
If package manager/lockfile state is unclear, stop and ask.
- [ ] Delete `src/api/sdk.ts` if unused.
- [ ] Update tests to mock `piClient` or API wrappers instead of SDK.
- [ ] Run:
```bash
cd PiAgentUi
npm run validate
npm run typecheck:extensions
```
### Task 13: Track B — Tauri Desktop Cleanup and Security Hardening
Only start after Track A smoke passes and user approves desktop scope.
**Files:**
- Rename: `src-tauri/src/app/commands/opencode.rs` -> `src-tauri/src/app/commands/pi_agent.rs`
- Modify: `src-tauri/src/app/commands/mod.rs`
- Modify: `src-tauri/src/app/mod.rs`
- Modify: `src-tauri/src/app/bridge/args.rs`
- Modify: `src-tauri/src/app/commands/bridge.rs`
- Modify: `src/store/serviceStore.ts`
- Modify: `src-tauri/capabilities/default.json`
- Modify: `src-tauri/tauri.conf.json`
- Modify: `src-tauri/Cargo.toml`
- [ ] Decide desktop behavior: connect to existing Pi extension server, spawn `pi --mode rpc`, or defer.
- [ ] Rename service commands to Pi names if desktop remains.
- [ ] Keep old command aliases for one transition release only if required by existing frontend code.
- [ ] Migrate storage keys:
- `opencode-auto-start-service` -> `pi-agent-auto-start-service`
- `opencode-binary-path` -> `pi-agent-binary-path`
- `opencode-service-env-vars` -> `pi-agent-service-env-vars`
- [ ] Add Rust-side URL allowlist tests for bridge connect/send paths.
- [ ] Narrow Tauri HTTP permissions to loopback-only.
- [ ] Reintroduce CSP if compatible.
- [ ] Run:
```bash
cd PiAgentUi
npm run typecheck
npm run test:run
cd src-tauri
cargo test
```
### Task 14: Final Product Metadata and OpenCode Cleanup
**Files:**
- Modify: `package.json`
- Modify: `src-tauri/Cargo.toml`
- Modify: `src-tauri/tauri.conf.json`
- Modify: README/docs/user-facing strings
- [ ] Apply user-approved final product name.
- [ ] Update package name/description.
- [ ] Update Tauri product name/window title/identifier if Track B is in scope.
- [ ] Remove OpenCode user-facing strings.
- [ ] Run allowlisted grep:
```bash
cd PiAgentUi
rg -n "@opencode-ai/sdk|getSDKClient|unwrap\(|opencode serve|OpenCode|opencode" src src-tauri extensions package.json vite.config.ts README.md
```
Expected: no runtime/user-facing OpenCode references except approved temporary aliases.
- [ ] Run full validation:
```bash
cd PiAgentUi
npm run validate
npm run typecheck:extensions
npm run build
npm pack --dry-run
```
If Track B is in scope:
```bash
cd PiAgentUi/src-tauri
cargo test
```
---
## Review Checklist for Implementation Milestones
After Tasks 6, 9, 12, and 13, run a critical review pass before continuing:
- Correctness/regression review against this plan.
- Test coverage and validation review.
- Security review for local HTTP/SSE/WS, token handling, and Tauri bridge if touched.
- Simplicity review to catch unnecessary OpenCode compatibility that can be deleted.
The reviewer output should be synthesized before any fixes are applied. Apply fixes with one writer only.
---
## Current Recommended Milestone Split
1. **Milestone 1:** decisions + design spec + field inventory.
2. **Milestone 2:** owned DTOs + extension typecheck/package metadata.
3. **Milestone 3:** secure extension server skeleton + boot data.
4. **Milestone 4:** model/session/message wrappers + event streaming.
5. **Milestone 5:** unsupported feature gating + SDK removal.
6. **Milestone 6:** Pi package smoke and final OpenCode cleanup.
7. **Milestone 7:** optional Tauri desktop cleanup/hardening.
@@ -0,0 +1,531 @@
# Plano de Implementação — Troca Correta de Sessões no PiAgentUI
> **Para agentes executores:** SUB-SKILL OBRIGATÓRIA: use `superpowers:subagent-driven-development` (recomendado) ou `superpowers:executing-plans` para implementar este plano tarefa por tarefa. Os passos usam checkbox (`- [ ]`) para acompanhamento.
**Objetivo:** garantir que uma mensagem enviada no front para a sessão B seja enviada para a sessão real B do Pi, nunca para a sessão que estava ativa quando o Pi abriu.
**Arquitetura:** o backend da extensão PiAgentUI será autoritativo para roteamento de mensagens. Antes de chamar `sendUserMessage`, ele compara `parsed.sessionId` com a sessão real ativa; se divergir, resolve o arquivo `.jsonl`, troca a sessão real do Pi com `switchSession`, e envia dentro do `withSession(replacedCtx)`.
**Tech Stack:** TypeScript, extensão Pi (`ExtensionContext` / `ReplacedSessionContext`), HTTP local Node, Vitest, React frontend sem mudança estrutural.
---
## Estrutura de arquivos
- Modificar: `../pi/packages/coding-agent/src/core/extensions/types.ts`
- Adicionar `switchSession()` em `ExtensionContext`.
- Modificar: `../pi/packages/coding-agent/src/core/extensions/runner.ts`
- Expor `switchSession` também em `createContext()` usando o handler já existente.
- Modificar: `../pi/packages/coding-agent/dist/core/extensions/types.d.ts`
- Atualizar declaração usada pelo typecheck da extensão, caso o build do Pi não regenere dist durante a sessão.
- Modificar: `PiAgentUi/extensions/piagentui-server.ts`
- Resolver arquivo da sessão por ID.
- Enviar mensagem para sessão solicitada com `switchSession(..., { withSession })`.
- Preservar servidor HTTP/SSE em `session_shutdown` causado por `resume` web-iniciado.
- Modificar: `PiAgentUi/extensions/piagentui-server.test.ts`
- Cobrir envio para sessão ativa, envio para sessão diferente, sessão inexistente e switch cancelado.
- Modificar: `PiAgentUi/src/api/message.ts`
- Garantir que erro HTTP de envio não seja engolido; `sendMessageAsync()` já envia `sessionId`.
- Modificar opcionalmente: `PiAgentUi/src/hooks/useChatSession.ts`
- Se necessário, manter erro visível via `handleError('send message', error)` sem mudar UX.
---
## Tarefa 1: Expor switchSession no contexto normal da extensão Pi
**Arquivos:**
- Modificar: `../pi/packages/coding-agent/src/core/extensions/types.ts`
- Modificar: `../pi/packages/coding-agent/src/core/extensions/runner.ts`
- Modificar: `../pi/packages/coding-agent/dist/core/extensions/types.d.ts`
- [ ] **Passo 1: Atualizar o contrato TypeScript do `ExtensionContext`**
Em `../pi/packages/coding-agent/src/core/extensions/types.ts`, dentro de `export interface ExtensionContext`, adicionar logo após `compact(options?: CompactOptions): void;`:
```ts
/**
* Switch to a different session file from long-lived extension services.
* Use `withSession` for work that must run after the replacement context is active.
*/
switchSession(
sessionPath: string,
options?: { withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
): Promise<{ cancelled: boolean }>;
```
- [ ] **Passo 2: Expor o método no contexto criado pelo runner**
Em `../pi/packages/coding-agent/src/core/extensions/runner.ts`, dentro de `createContext(): ExtensionContext`, adicionar antes de `getSystemPrompt: () => {`:
```ts
switchSession: (sessionPath, options) => {
runner.assertActive();
return runner.switchSessionHandler(sessionPath, options);
},
```
O trecho final deve ficar assim:
```ts
compact: (options) => {
runner.assertActive();
runner.compactFn(options);
},
switchSession: (sessionPath, options) => {
runner.assertActive();
return runner.switchSessionHandler(sessionPath, options);
},
getSystemPrompt: () => {
runner.assertActive();
return runner.getSystemPromptFn();
},
```
- [ ] **Passo 3: Atualizar a declaração dist se o build do Pi não for executado**
Em `../pi/packages/coding-agent/dist/core/extensions/types.d.ts`, adicionar assinatura equivalente dentro de `ExtensionContext`:
```ts
/**
* Switch to a different session file from long-lived extension services.
* Use `withSession` for work that must run after the replacement context is active.
*/
switchSession(sessionPath: string, options?: {
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
}): Promise<{
cancelled: boolean;
}>;
```
- [ ] **Passo 4: Verificar tipos do Pi core**
Executar:
```bash
cd D:/Documents/Github/PiAgentUI/pi/packages/coding-agent
pnpm test -- --runInBand test/suite/regressions/2860-replaced-session-context.test.ts
```
Se esse pacote não usa `pnpm test` com esse formato, executar o comando de teste já documentado no pacote. Resultado esperado: testes existentes passam sem erro de tipo. Se o comando correto não existir, registrar a saída e seguir para o typecheck do PiAgentUI na Tarefa 4.
---
## Tarefa 2: Roteamento de mensagem por sessão no servidor PiAgentUI
**Arquivos:**
- Modificar: `PiAgentUi/extensions/piagentui-server.ts`
- [ ] **Passo 1: Adicionar tipo local para contexto com switch**
Abaixo de `const MIME_TYPES`, adicionar:
```ts
type SwitchableExtensionContext = ExtensionContext & {
switchSession?: (
sessionPath: string,
options?: { withSession?: (ctx: any) => Promise<void> },
) => Promise<{ cancelled: boolean }>
}
```
- [ ] **Passo 2: Trocar o tipo de `latestCtx`**
Substituir:
```ts
let latestCtx: ExtensionContext | null = null
```
por:
```ts
let latestCtx: SwitchableExtensionContext | null = null
```
- [ ] **Passo 3: Criar helper de arquivo de sessão**
Adicionar depois de `getCurrentSessionId()`:
```ts
function getSessionFileForId(sessionId: string): string | null {
const sessionDir = latestCtx?.sessionManager?.getSessionDir()
if (!sessionDir || !fs.existsSync(sessionDir)) return null
const files = fs.readdirSync(sessionDir).filter(f => f.endsWith('.jsonl'))
const matchedFile = files.find(f => f.endsWith(`_${sessionId}.jsonl`))
return matchedFile ? path.join(sessionDir, matchedFile) : null
}
```
- [ ] **Passo 4: Extrair seleção de modelo para helper reutilizável**
Adicionar depois de `getSessionFileForId()`:
```ts
async function setRequestedModel(ctx: SwitchableExtensionContext, model: any) {
if (!model?.providerID || !model?.modelID) return
try {
const targetModel = ctx.modelRegistry?.find(model.providerID, model.modelID)
if (!targetModel) return
const currentModel = ctx.model
if (!currentModel || currentModel.id !== targetModel.id || currentModel.provider !== targetModel.provider) {
console.log(`[PiAgentUi] Switching session model to ${targetModel.provider}:${targetModel.id}`)
await pi.setModel(targetModel)
}
} catch (err) {
console.error('[PiAgentUi] Failed to set model:', err)
}
}
```
- [ ] **Passo 5: Adicionar flag para preservar servidor durante switch web-iniciado**
Perto dos estados do servidor, adicionar:
```ts
let preserveServerForSessionSwitch = false
```
- [ ] **Passo 6: Criar helper de envio correto por sessão**
Adicionar depois de `setRequestedModel()`:
```ts
async function sendUserMessageToRequestedSession(parsed: any): Promise<{ status: number; body: any }> {
const requestedSessionId = parsed.sessionId
const activeSessionId = getCurrentSessionId()
if (!parsed.text) {
return { status: 400, body: { error: 'Missing message text' } }
}
if (!requestedSessionId || requestedSessionId === activeSessionId) {
if (latestCtx) {
await setRequestedModel(latestCtx, parsed.model)
}
const p = pi.sendUserMessage(parsed.text) as any
if (p && typeof p.catch === 'function') {
p.catch((err: any) => {
console.error('[PiAgentUi] Failed to send user message:', err)
})
}
return { status: 202, body: { sessionID: activeSessionId } }
}
const sessionFile = getSessionFileForId(requestedSessionId)
if (!sessionFile) {
return { status: 404, body: { error: `Session not found: ${requestedSessionId}` } }
}
if (!latestCtx?.switchSession) {
return { status: 501, body: { error: 'Pi session switching is not available in this runtime' } }
}
preserveServerForSessionSwitch = true
try {
const result = await latestCtx.switchSession(sessionFile, {
withSession: async replacedCtx => {
latestCtx = replacedCtx as SwitchableExtensionContext
await setRequestedModel(latestCtx, parsed.model)
await replacedCtx.sendUserMessage(parsed.text)
},
})
if (result.cancelled) {
return { status: 409, body: { error: 'Session switch cancelled' } }
}
return { status: 202, body: { sessionID: requestedSessionId } }
} finally {
preserveServerForSessionSwitch = false
}
}
```
- [ ] **Passo 7: Substituir lógica atual de `/api/messages/send`**
Dentro do handler `if (pathParts[1] === 'messages' && pathParts[2] === 'send' && method === 'POST')`, substituir o bloco que faz `setModel`, calcula `activeSessionId`, alerta divergência e chama `pi.sendUserMessage(parsed.text)` por:
```ts
const result = await sendUserMessageToRequestedSession(parsed)
res.writeHead(result.status, { 'Content-Type': 'application/json' })
res.end(
JSON.stringify({
messageID: 'msg-' + Math.random().toString(36).substring(2) + '-' + Date.now(),
...result.body,
}),
)
```
O `catch` do `JSON.parse` permanece igual.
- [ ] **Passo 8: Preservar servidor em `session_shutdown` de switch interno**
Substituir:
```ts
pi.on('session_shutdown', async () => {
stopServer()
})
```
por:
```ts
pi.on('session_shutdown', async event => {
if (preserveServerForSessionSwitch && event?.reason === 'resume') {
return
}
stopServer()
})
```
- [ ] **Passo 9: Verificar typecheck da extensão**
Executar:
```bash
cd D:/Documents/Github/PiAgentUI/PiAgentUi
npx tsc --noEmit -p tsconfig.extension.json
```
Resultado esperado: nenhum erro.
---
## Tarefa 3: Testes do servidor de extensão
**Arquivos:**
- Modificar: `PiAgentUi/extensions/piagentui-server.test.ts`
- [ ] **Passo 1: Adicionar sessão alternativa no teste existente**
Depois da criação de `sessionFile`, adicionar:
```ts
const otherSessionId = 'test-session-uuid-other'
const otherSessionFile = path.join(tempSessionDir, `session_${otherSessionId}.jsonl`)
fs.writeFileSync(otherSessionFile, JSON.stringify({
type: 'session',
id: otherSessionId,
timestamp: new Date().toISOString(),
cwd: '/mock/cwd'
}) + '\n')
```
- [ ] **Passo 2: Adicionar mocks de switch no contexto**
Antes de `const mockContext = {`, adicionar:
```ts
const replacedSendUserMessage = vi.fn()
const switchSession = vi.fn(async (_sessionPath: string, options?: { withSession?: (ctx: any) => Promise<void> }) => {
await options?.withSession?.({
...mockContext,
sessionManager: {
getSessionDir: () => tempSessionDir,
getSessionId: () => otherSessionId,
},
sendUserMessage: replacedSendUserMessage,
})
return { cancelled: false }
})
```
Dentro de `mockContext`, adicionar:
```ts
switchSession,
```
- [ ] **Passo 3: Testar envio para sessão ativa sem switch**
Manter o teste existente de `/api/messages/send` sem `sessionId` e ajustar expectativa para aceitar qualquer segundo argumento opcional:
```ts
expect(mockPi.sendUserMessage).toHaveBeenCalledWith('hello world')
expect(switchSession).not.toHaveBeenCalled()
```
- [ ] **Passo 4: Testar envio para sessão diferente com switch**
Depois do teste de envio ativo, adicionar:
```ts
const sendOtherRes = await fetch(`${baseUrl}/api/messages/send`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: otherSessionId,
text: 'hello other session',
model: { providerID: 'google', modelID: 'gemini-2.5-pro' }
})
})
expect(sendOtherRes.status).toBe(202)
expect(switchSession).toHaveBeenCalledWith(otherSessionFile, expect.objectContaining({
withSession: expect.any(Function),
}))
expect(replacedSendUserMessage).toHaveBeenCalledWith('hello other session')
```
- [ ] **Passo 5: Testar sessão inexistente**
Adicionar:
```ts
const missingSessionRes = await fetch(`${baseUrl}/api/messages/send`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'missing-session-id',
text: 'must not be sent',
model: { providerID: 'google', modelID: 'gemini-2.5-pro' }
})
})
expect(missingSessionRes.status).toBe(404)
expect(replacedSendUserMessage).not.toHaveBeenCalledWith('must not be sent')
```
- [ ] **Passo 6: Rodar teste da extensão**
Executar:
```bash
cd D:/Documents/Github/PiAgentUI/PiAgentUi
npx vitest run extensions/piagentui-server.test.ts
```
Resultado esperado: teste passa. Se o teste monolítico ficar instável por porta/servidor, dividir em `describe` separado com porta limpa e `session_shutdown` no final.
---
## Tarefa 4: Garantir que erros de envio sejam visíveis no frontend
**Arquivos:**
- Modificar: `PiAgentUi/src/api/message.ts`
- Testar: `PiAgentUi/src/api/piClient.test.ts` ou criar caso em `PiAgentUi/src/api/message.test.ts`
- [ ] **Passo 1: Confirmar comportamento de `piClient.post`**
Ler `PiAgentUi/src/api/piClient.ts`. Se `piClient.post` já lança erro em status `>=400`, não alterar código. Se não lança, ajustar para lançar com status e body.
O comportamento esperado para `sendMessageAsync()` é:
```ts
export async function sendMessageAsync(params: SendMessageParams): Promise<void> {
await piClient.post('/api/messages/send', {
sessionId: params.sessionId,
text: params.text,
model: params.model,
directory: params.directory,
})
}
```
- [ ] **Passo 2: Adicionar teste de erro HTTP se necessário**
Se `piClient` não tiver cobertura de erro, adicionar teste que simula `fetch` retornando `404` para `/api/messages/send` e espera rejeição.
Código esperado do teste:
```ts
it('rejects sendMessageAsync when the server rejects the target session', async () => {
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ error: 'Session not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
})))
await expect(sendMessageAsync({
sessionId: 'missing-session',
text: 'hello',
model: { providerID: 'google', modelID: 'gemini-2.5-pro' },
directory: '/mock/cwd',
})).rejects.toThrow()
})
```
- [ ] **Passo 3: Rodar testes de API relevantes**
Executar:
```bash
cd D:/Documents/Github/PiAgentUI/PiAgentUi
npx vitest run src/api/piClient.test.ts src/api/events.test.ts src/store/messageStore.test.ts
```
Resultado esperado: todos passam.
---
## Tarefa 5: Verificação integrada
**Arquivos:**
- Nenhum arquivo novo obrigatório.
- [ ] **Passo 1: Rodar typecheck completo do app**
```bash
cd D:/Documents/Github/PiAgentUI/PiAgentUi
npx tsc --noEmit
npx tsc --noEmit -p tsconfig.extension.json
```
Resultado esperado: ambos sem erro.
- [ ] **Passo 2: Rodar testes focados**
```bash
cd D:/Documents/Github/PiAgentUI/PiAgentUi
npx vitest run extensions/piagentui-server.test.ts src/api/events.test.ts src/store/messageStore.test.ts
```
Resultado esperado: todos passam.
- [ ] **Passo 3: Rodar build**
```bash
cd D:/Documents/Github/PiAgentUI/PiAgentUi
npm run build
```
Resultado esperado: build passa. Avisos de chunk grande do Vite são aceitáveis se não forem novos erros.
- [ ] **Passo 4: Verificação manual no navegador**
1. Reiniciar o Pi Agent para carregar a extensão atualizada.
2. Abrir PiAgentUI em `http://127.0.0.1:<porta>`.
3. Abrir sessão A e anotar o arquivo `.jsonl` dela.
4. Selecionar sessão B no sidebar.
5. Enviar mensagem `teste roteamento sessao B`.
6. Confirmar visualmente que user + assistant aparecem em B sem F5.
7. Confirmar que o arquivo `.jsonl` da sessão A não recebeu essa mensagem.
8. Confirmar que o arquivo `.jsonl` da sessão B recebeu essa mensagem.
9. Confirmar que `/global/event` continua com uma conexão viva e sem reconectar em loop.
- [ ] **Passo 5: Commit final**
```bash
cd D:/Documents/Github/PiAgentUI/PiAgentUi
git status --short
git add extensions/piagentui-server.ts extensions/piagentui-server.test.ts src/api/message.ts src/api/message.test.ts docs/superpowers/plans/2026-05-30-piagentui-active-session-switch.md
git commit -m "fix: route messages to selected Pi session"
```
Se a implementação modificar arquivos em `../pi`, fazer um commit separado dentro do repositório/árvore correspondente apenas se ele também for um repositório Git local. Como `D:/Documents/Github/PiAgentUI` não é repo e `PiAgentUi` é o repo local atual, registrar explicitamente os arquivos de Pi alterados no resumo final.
---
## Auto-revisão do plano
- Cobertura da spec: o plano cobre exposição de `switchSession`, roteamento backend, preservação do servidor, testes de erro e verificação manual.
- Placeholders: nenhum `TBD`, `TODO`, `implementar depois` ou instrução genérica sem ação concreta.
- Consistência de tipos: `SwitchableExtensionContext`, `switchSession`, `withSession`, `replacedCtx.sendUserMessage` e `sessionID` são usados consistentemente.
- Escopo: limitado ao sistema de sessão/roteamento; não redesenha UI nem lista de sessões.
@@ -0,0 +1,51 @@
# PiAgentUI SQL Settings and Voice Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Move backend-owned PiAgentUI settings to a local SQLite database and fix/extend voice settings for local OpenAI-compatible STT/TTS providers such as Speaches.
**Architecture:** The backend extension becomes the source of truth for backend-owned settings using `~/.pi/agent/piagentui.db`, with a SQL table for namespaced JSON settings and migration from the legacy `piagentui-settings.json`. Frontend voice settings continue using HTTP APIs, now with STT API-key-required control and TTS model/voice configuration.
**Tech Stack:** Node `node:sqlite` `DatabaseSync`, TypeScript, Vitest, React settings components.
---
## Files
- Create: `extensions/piagentui-settings-store.ts` — SQLite settings repository, schema, defaults, legacy JSON migration.
- Create: `extensions/piagentui-settings-store.test.ts` — unit tests for SQL persistence and migration.
- Modify: `extensions/piagentui-server.ts` — replace JSON helpers with SQLite store; add TTS settings/speech endpoints; allow keyless OpenAI-compatible STT when configured.
- Modify: `extensions/piagentui-server.test.ts` — integration coverage for keyless STT and TTS settings.
- Modify: `src/types/api/stt.ts`, `src/api/stt.ts`, `src/hooks/useVoiceTranscription.ts` — expose `apiKeyRequired` and avoid blocking local Speaches.
- Create/Modify: `src/types/api/tts.ts`, `src/api/tts.ts`, `src/features/settings/components/TtsSettings.tsx`, `src/features/settings/components/VoiceSettings.tsx` — TTS settings UI with voice selection.
- Modify tests under `src/hooks` and `src/features/settings/components`.
## Tasks
### Task 1: SQL settings store
- [ ] Write failing store tests for SQLite persistence and legacy JSON migration.
- [ ] Implement `extensions/piagentui-settings-store.ts` with `app_meta` and `settings` tables.
- [ ] Run targeted store tests and commit.
### Task 2: Backend voice behavior
- [ ] Write failing server/integration tests for keyless OpenAI-compatible STT and TTS settings sanitization.
- [ ] Update backend STT to require API key only when `apiKeyRequired` is true.
- [ ] Add TTS settings and `/api/tts/speech` proxy.
- [ ] Run extension tests and commit.
### Task 3: Frontend voice UI
- [ ] Write failing hook/settings tests for keyless STT start and TTS voice selection.
- [ ] Add STT `apiKeyRequired` toggle.
- [ ] Add TTS settings card with model, voice, format, endpoint, API key behavior.
- [ ] Run targeted frontend tests and commit.
### Task 4: Validation
- [ ] Run `npx tsc --noEmit`.
- [ ] Run `npm run typecheck:extensions`.
- [ ] Run targeted voice/storage tests.
- [ ] Run `npm run build`.
- [ ] Run `git diff --check`.
@@ -0,0 +1,93 @@
# PiAgentUI STT LAN Real Sessions Project Folders Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Implement real Pi session creation, project-folder session history, LAN access with QR code, and file-based STT voice input for PiAgentUI.
**Architecture:** Keep runtime changes inside PiAgentUI's extension backend and focused frontend surfaces. Add pure helpers for network/session grouping/STT settings, test them first, then wire endpoints and UI with minimal component changes.
**Tech Stack:** TypeScript, React 19, Vite 8, Vitest, Testing Library, Node HTTP extension server, browser MediaRecorder, OpenAI-compatible transcription API.
---
## File Map
- `extensions/piagentui-server-core.ts`: host/origin validation helpers, LAN URL discovery, STT URL/settings sanitization helpers.
- `extensions/piagentui-server-core.test.ts`: unit tests for backend helpers.
- `extensions/piagentui-server.ts`: real session creation, LAN settings endpoints, STT settings/transcription endpoints, dynamic boot base URL.
- `extensions/piagentui-server.test.ts`: integration tests for endpoints.
- `src/types/api/network.ts`: network access response/request types.
- `src/api/network.ts`, `src/api/network.test.ts`: client API for LAN access.
- `src/types/api/stt.ts`: STT settings/transcription types.
- `src/api/stt.ts`, `src/api/stt.test.ts`: client API for STT.
- `src/hooks/useVoiceTranscription.ts`, `src/hooks/useVoiceTranscription.test.tsx`: browser recording/transcription state machine.
- `src/features/chat/input/VoiceInputButton.tsx`, `src/features/chat/input/VoiceInputButton.test.tsx`: mic UI.
- `src/features/chat/InputBox.tsx`: accept voice transcript insertion.
- `src/features/chat/input/InputToolbar.tsx`: render voice button.
- `src/features/sessions/sessionProjectGroups.ts`, `src/features/sessions/sessionProjectGroups.test.ts`: pure project grouping.
- `src/features/sessions/SessionList.tsx`, `src/features/sessions/SessionList.test.tsx`: project folder rendering.
- `src/features/settings/components/NetworkAccessSettings.tsx`, `src/features/settings/components/NetworkAccessSettings.test.tsx`: LAN toggle/URL/QR UI.
- `src/features/settings/components/ServersSettings.tsx`: include network section.
- `src/locales/en/*.json`, `src/locales/zh-CN/*.json`: minimal labels.
- `package.json`: add `qrcode` only if frontend QR generation needs it.
## Task 1: Backend helper tests and helpers
- [ ] Write tests in `extensions/piagentui-server-core.test.ts` for LAN host validation, local URL discovery injection, and STT URL/settings sanitization.
- [ ] Implement helpers in `extensions/piagentui-server-core.ts`.
- [ ] Run `npx vitest run extensions/piagentui-server-core.test.ts`.
- [ ] Commit `test/feat: add network and stt backend helpers`.
## Task 2: Real session creation endpoint
- [ ] Add failing tests in `extensions/piagentui-server.test.ts` proving `POST /api/sessions` creates a new `.jsonl`, calls `switchSession`, returns the new id, and returns `501` without `switchSession`.
- [ ] Implement minimal real session file creation in `extensions/piagentui-server.ts`.
- [ ] Run `npx vitest run extensions/piagentui-server.test.ts`.
- [ ] Commit `feat: create real pi sessions from ui`.
## Task 3: Project folder grouping
- [ ] Add failing tests in `src/features/sessions/sessionProjectGroups.test.ts` for Windows/POSIX grouping, basename labels, ordering, and search behavior.
- [ ] Implement `sessionProjectGroups.ts`.
- [ ] Add/adjust `SessionList.test.tsx` for folders expand/collapse and selection.
- [ ] Update `SessionList.tsx` minimally to render project folders by default when grouped.
- [ ] Run `npx vitest run src/features/sessions/sessionProjectGroups.test.ts src/features/sessions/SessionList.test.tsx`.
- [ ] Commit `feat: group sessions by project folder`.
## Task 4: LAN access endpoints and UI
- [ ] Add backend endpoint tests for `GET/POST /api/network/access`, LAN host allowed only when enabled, and host-based boot data.
- [ ] Implement network settings persistence and endpoint behavior.
- [ ] Add frontend API tests for `src/api/network.ts`.
- [ ] Add `NetworkAccessSettings` tests for toggle, URL, and QR placeholder/rendering.
- [ ] Implement API and settings component.
- [ ] Wire it into `ServersSettings.tsx`.
- [ ] Run targeted backend/frontend tests.
- [ ] Commit `feat: expose local network access settings`.
## Task 5: STT settings and transcription backend/client
- [ ] Add backend tests for redacted settings, save semantics, invalid URLs, and transcription provider errors.
- [ ] Implement settings persistence and transcription endpoint.
- [ ] Add `src/api/stt.test.ts` and implement `src/api/stt.ts`.
- [ ] Run targeted tests.
- [ ] Commit `feat: add stt settings and transcription api`.
## Task 6: Voice recording UI
- [ ] Add hook tests for permission denied, record/transcribe success, and error path.
- [ ] Implement `useVoiceTranscription.ts`.
- [ ] Add `VoiceInputButton` tests and component.
- [ ] Wire `InputToolbar` and `InputBox` to append transcript to composer without auto-send.
- [ ] Run targeted tests.
- [ ] Commit `feat: add voice transcription composer control`.
## Task 7: Final validation and review
- [ ] Run `npx tsc --noEmit`.
- [ ] Run `npx tsc --noEmit -p tsconfig.extension.json`.
- [ ] Run `npm run test:run`.
- [ ] Run `npm run build`.
- [ ] Run `git diff --check`.
- [ ] Review changed files for unnecessary edits.
- [ ] Commit any final fixes.
@@ -0,0 +1,81 @@
# Roadmap Contexto, Skills e MCP Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** concluir a integração de contexto real, skills reais, avisos de compaction/overflow e MCP nativo no PiAgentUI.
**Architecture:** O backend da extensão PiAgentUI deve ser a fonte de verdade para dados do runtime Pi. A UI deve consumir endpoints pequenos e tipados, preservando fallback seguro quando dados runtime não existirem.
**Tech Stack:** TypeScript, React, Vite, Vitest, extensão PiAgentUI HTTP/SSE, Pi runtime APIs.
---
## Task 1: Context metadata e uso real
**Status:** concluído em `f5a3fa5 feat: enrich context details with model metadata`.
## Task 2: Tool results somente em tool cards
**Status:** concluído em `41adf23 fix: keep tool results in tool cards`.
## Task 3: Skills reais por origem
**Status:** concluído em `257d63c feat: show real skills by source`.
## Task 4: Avisos de overflow e compaction
**Files:**
- Modify: `src/hooks/useSessionStats.ts`
- Modify: `src/features/chat/sidebar/SidebarFooter.tsx`
- Modify: `src/features/chat/sidebar/ContextDetailsDialog.tsx`
- Test: `src/hooks/useSessionStats.test.tsx`
- [ ] Adicionar estado derivado para `contextStatus`: `unknown`, `normal`, `warning`, `danger`.
- [ ] Mostrar fonte do dado: runtime, tokens ou estimativa.
- [ ] Mostrar aviso quando `contextKnown=false` após compaction.
- [ ] Mostrar aviso quando uso >= 70% e >= 90%.
- [ ] Rodar testes focados e build.
- [ ] Commit: `feat: surface context overflow notices`.
## Task 5: MCP backend real
**Files:**
- Modify/Create: `extensions/piagentui-server.ts`
- Modify/Create: `src/api/mcp.ts`
- Test: `extensions/piagentui-server.test.ts`
- [ ] Ler configuração MCP do Pi/agent config quando disponível.
- [ ] Expor `/api/mcp/servers` com servidores, status e origem.
- [ ] Preservar fallback vazio seguro.
- [ ] Testar config válida, ausente e inválida.
- [ ] Commit: `feat: expose real mcp servers`.
## Task 6: MCP UI real
**Files:**
- Modify: `src/components/McpPanel.tsx`
- Modify: `src/types/api/mcp.ts`
- Test: `src/components/McpPanel.test.tsx` se existir; caso contrário criar.
- [ ] Agrupar servidores por status/origem.
- [ ] Mostrar tools quando disponíveis.
- [ ] Mostrar erro de inicialização/config.
- [ ] Commit: `feat: render real mcp server status`.
## Task 7: Skills avançado
**Files:**
- Modify: `extensions/piagentui-server.ts`
- Modify: `src/components/SkillPanel.tsx`
- [ ] Expor diagnósticos/collisions quando Pi disponibilizar.
- [ ] Mostrar `disableModelInvocation` quando disponível.
- [ ] Melhorar origem com package/source quando disponível.
- [ ] Commit: `feat: add skill diagnostics metadata`.
## Task 8: Hardening e docs
- [ ] Extrair parsers grandes de `extensions/piagentui-server.ts` se necessário.
- [ ] Atualizar Obsidian/task list.
- [ ] Rodar full test/build.
- [ ] Fazer checkpoint commit/tag local se solicitado.
@@ -0,0 +1,58 @@
# PiAgentUI Git Composer Control Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Implement a Git branch/control popover in the message composer using GitHub Octicons and safe backend Git endpoints.
**Architecture:** Add focused Git logic in `extensions/piagentui-git-manager.ts`, expose `/api/vcs/*` endpoints from the extension server, expand frontend VCS API/types, and add `GitComposerControl` inside `InputToolbar` before agent/variant selectors.
**Tech Stack:** React 19, TypeScript, Vite, Vitest, Node child_process, Git CLI, `@primer/octicons-react`.
---
## Files
- Create: `extensions/piagentui-git-manager.ts`
- Create: `extensions/piagentui-git-manager.test.ts`
- Modify: `extensions/piagentui-server.ts`
- Modify: `extensions/piagentui-server.test.ts`
- Modify: `src/types/api/vcs.ts`
- Modify: `src/types/api/index.ts`
- Modify: `src/api/vcs.ts`
- Create/modify: `src/api/vcs.test.ts`
- Create: `src/features/chat/input/GitComposerControl.tsx`
- Create: `src/features/chat/input/GitComposerControl.test.tsx`
- Modify: `src/features/chat/input/InputToolbar.tsx`
- Modify: `package.json`, `package-lock.json`
## Task 1 — Backend Git manager
- [ ] Write failing tests for branch parsing, status parsing, safe argv commands and commit validation.
- [ ] Implement `runGit`, `getGitStatus`, `listGitBranches`, `checkoutGitBranch`, `createGitBranch`, `stageGitPaths`, `unstageGitPaths`, `commitGitChanges`, `pullGitBranch`, `pushGitBranch`.
- [ ] Verify focused test passes.
## Task 2 — Backend endpoints
- [ ] Write failing server endpoint tests for `/api/vcs/status`, `/api/vcs/branches`, `/api/vcs/commit`.
- [ ] Add endpoint routing in `piagentui-server.ts` with request parsing and errors.
- [ ] Verify server tests pass.
## Task 3 — Frontend API contract
- [ ] Write failing API tests for query/body shapes.
- [ ] Expand `src/types/api/vcs.ts` and `src/api/vcs.ts`.
- [ ] Verify API tests pass.
## Task 4 — Composer UI
- [ ] Install `@primer/octicons-react`.
- [ ] Write failing component tests for branch chip, popover opening and commit action.
- [ ] Implement `GitComposerControl` with Octicons.
- [ ] Integrate into `InputToolbar` before agent selector.
- [ ] Verify component tests pass.
## Task 5 — Full verification
- [ ] Format touched files with `node node_modules/prettier/bin/prettier.cjs --write ...`.
- [ ] Run LSP diagnostics on touched files.
- [ ] Run `npm run typecheck`, `npm run typecheck:extensions`, focused tests, `npm run test:run`, `npm run build`, and `git diff --check`.
@@ -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`.