🎉 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
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:
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
|
||||
+565
@@ -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`.
|
||||
Reference in new issue
Block a user