- Delete legacy hooks: useClaude.ts, useOpenCode.ts, usePiMono.ts, App.backup.tsx - Update all components to use usePi instead of legacy hooks - Replace useVisiblePiMonoModels/useClaudeModels/useOpenCodeModels with useVisiblePiModels - Migrate from LegacyChatMessage to ChatMessage type throughout - Update SessionBar to remove provider and archive props - Simplify ChatDetailPanel to Pi-only (remove Claude/OpenCode components) - Fix useChatSessions calls (remove provider parameter) - Update user-settings types: provider now only 'pi' instead of legacy values - Update PI_HARNESS_REBUILD.md to mark phases complete
1417 lines
36 KiB
Markdown
1417 lines
36 KiB
Markdown
# Pi Harness Rebuild — Implementation Plan
|
|
|
|
**Status**: ✅ COMPLETE — Phase 9 (Final Cleanup)
|
|
**Date**: February 20, 2026
|
|
**Scope**: Replace all three legacy harnesses (Claude, OpenCode, Pi-Mono) with single, clean Pi harness
|
|
|
|
---
|
|
|
|
## ✅ Implementation Status
|
|
|
|
### Backend (Complete)
|
|
- [x] **Phase 1**: Setup & Cleanup — Complete
|
|
- [x] **Phase 2**: Core Infrastructure — Complete
|
|
- [x] **Phase 3**: WebSocket Handler — Complete
|
|
- [x] **Phase 4**: REST Endpoints — Complete
|
|
- [x] **Phase 5**: Integration — Complete
|
|
- [x] **Phase 6**: Cleanup & Polish — Complete
|
|
- [x] **Phase 6.1**: Session Grouping — Complete
|
|
|
|
### Frontend (Complete)
|
|
- [x] **Phase 7.1**: Type Alignment — Complete ✅
|
|
- [x] **Phase 7.2**: Unified Pi Hook (`usePi.ts`) — Complete ✅
|
|
- [x] **Phase 7.3**: Unified Models Hook — Complete ✅
|
|
- [x] **Phase 7.4**: Session Management Migration — Complete ✅
|
|
- [x] **Phase 7.5**: Group Support Hooks — Complete ✅
|
|
|
|
### UI Enhancements (Future)
|
|
- [ ] **Phase 8.1**: Grouped ChatList UI
|
|
- [ ] **Phase 8.2**: Group Management UI
|
|
- [ ] **Phase 8.3**: Search Enhancements
|
|
|
|
### Final Cleanup (Complete)
|
|
- [x] **Phase 9.1**: Frontend Legacy Cleanup — Complete ✅
|
|
- [x] **Phase 9.2**: Backend Final Cleanup — Complete ✅
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
- [Decisions Made](#decisions-made)
|
|
- [Cleanup Phase](#cleanup-phase)
|
|
- [New Architecture](#new-architecture)
|
|
- [Wire Protocol](#wire-protocol)
|
|
- [File Structure](#file-structure)
|
|
- [Implementation Steps](#implementation-steps)
|
|
- [Phase 6.1: Session Grouping](#phase-61-session-grouping--detailed-implementation)
|
|
- [Phase 7: Frontend Migration](#phase-7-frontend-migration--detailed-implementation)
|
|
- [Phase 8: UI Enhancements](#phase-8-ui-enhancements)
|
|
- [Phase 9: Final Cleanup](#phase-9-final-cleanup)
|
|
- [Implementation Details](#implementation-details)
|
|
|
|
---
|
|
|
|
## Decisions Made
|
|
|
|
### Session Lifecycle
|
|
- **One Pi process per chat session** (not per user)
|
|
- Spawned on demand when new chat starts
|
|
- Killed on idle timeout: **1 hour**
|
|
- Users can have multiple concurrent sessions with different models (fully isolated)
|
|
|
|
### Resuming Old Sessions
|
|
- Load messages from disk: `{cwd}/{sessionId}/messages.json`
|
|
- Spawn fresh Pi process
|
|
- Inject full conversation history in system prompt
|
|
- User continues seamlessly
|
|
|
|
### Default Working Directory
|
|
- If not provided by frontend, use: `getHomeDir(email)` (user's home directory)
|
|
- Frontend sends `cwd` with chat spawn request
|
|
|
|
### Session Storage Location
|
|
```
|
|
{userCwd}/{sessionId}/
|
|
├── meta.json (metadata: id, title, model, cwd, timestamps, cost)
|
|
└── messages.json (full conversation history)
|
|
```
|
|
|
|
### API Design
|
|
- **Option B**: Separate REST + WebSocket
|
|
- REST: stateless operations (models, sessions, search)
|
|
- WebSocket: stateful, real-time streaming (chat)
|
|
|
|
### Wire Protocol
|
|
- **Redesigned from scratch** for clarity and simplicity
|
|
- Keeps all existing features: streaming, tools, history, models, cwd, skills
|
|
|
|
---
|
|
|
|
## Cleanup Phase
|
|
|
|
### Files to Delete
|
|
|
|
**Backend API directories:**
|
|
```
|
|
src/servers/api/claude/ (entire directory)
|
|
src/servers/api/opencode/ (entire directory)
|
|
src/servers/api/pi-mono/ (entire directory)
|
|
```
|
|
|
|
**References to remove from `src/server.tsx`:**
|
|
- `/api/harness/claudecode/ws` route
|
|
- `/api/harness/opencode/ws` route
|
|
- `/api/harness/pi-mono/ws` route
|
|
- Remove handlers Map and provider routing logic
|
|
- Keep only `/api/pi/chat/ws` for new harness
|
|
|
|
**References to remove from `src/servers/hono.ts`:**
|
|
```ts
|
|
import { claudeModelsRouter } from './api/claude/sessions';
|
|
import { opencodeModelsRouter } from './api/opencode/sessions';
|
|
import { piMonoModelsRouter } from './api/pi-mono/sessions';
|
|
|
|
// Remove from protectedRouter:
|
|
protectedRouter.route('/', claudeModelsRouter);
|
|
protectedRouter.route('/', opencodeModelsRouter);
|
|
protectedRouter.route('/', piMonoModelsRouter);
|
|
```
|
|
|
|
**Type definitions to review:**
|
|
- `src/servers/api/chat-types.ts` — Keep but clean up for Pi only
|
|
|
|
---
|
|
|
|
## New Architecture
|
|
|
|
```
|
|
┌─ Backend (Bun/Hono)
|
|
│
|
|
├─ src/servers/api/pi/ ← NEW HARNESS
|
|
│ ├── types.ts (ClientMessage, ServerMessage, types)
|
|
│ ├── websocket.ts (WebSocket bridge, Pi lifecycle)
|
|
│ ├── rest.ts (REST endpoints: models, sessions, search)
|
|
│ ├── storage.ts (Session persistence, disk I/O)
|
|
│ ├── pi-bridge.ts (Pi RPC communication)
|
|
│ └── session-manager.ts (In-memory session tracking)
|
|
│
|
|
├─ src/servers/hono.ts (Wire Pi routes)
|
|
├─ src/server.tsx (Wire Pi WebSocket)
|
|
│
|
|
└─ src/workspaces/
|
|
└─ data-path.ts or similar (User home dir, session paths)
|
|
```
|
|
|
|
---
|
|
|
|
## Wire Protocol
|
|
|
|
### Client → Server
|
|
|
|
```typescript
|
|
// Start new chat
|
|
{
|
|
type: "chat";
|
|
prompt: string;
|
|
sessionId?: string; // omit for new, include for resume
|
|
model?: string; // e.g., "big-pickle", "claude-opus-4-5"
|
|
cwd?: string; // working directory, defaults to user home
|
|
attachmentIds?: string[]; // file IDs to attach
|
|
}
|
|
|
|
// Resume existing chat
|
|
{
|
|
type: "resume";
|
|
sessionId: string;
|
|
}
|
|
|
|
// Stop generation
|
|
{
|
|
type: "stop";
|
|
}
|
|
```
|
|
|
|
### Server → Client
|
|
|
|
```typescript
|
|
// Session initialized
|
|
{
|
|
type: "session:init";
|
|
sessionId: string;
|
|
model: string;
|
|
cwd: string;
|
|
}
|
|
|
|
// Assistant text (complete block)
|
|
{
|
|
type: "assistant:text";
|
|
text: string;
|
|
}
|
|
|
|
// Assistant text (streaming delta)
|
|
{
|
|
type: "assistant:delta";
|
|
text: string;
|
|
}
|
|
|
|
// Tool execution started
|
|
{
|
|
type: "tool:start";
|
|
toolCallId: string;
|
|
toolName: string;
|
|
toolInput: Record<string, unknown>;
|
|
}
|
|
|
|
// Tool execution completed
|
|
{
|
|
type: "tool:result";
|
|
toolCallId: string;
|
|
output: string;
|
|
isError: boolean;
|
|
}
|
|
|
|
// Generation completed
|
|
{
|
|
type: "result";
|
|
sessionId: string;
|
|
cost: {
|
|
inputTokens: number;
|
|
outputTokens: number;
|
|
totalUSD: number;
|
|
};
|
|
}
|
|
|
|
// Full sync (for resume)
|
|
{
|
|
type: "sync:messages";
|
|
sessionId: string;
|
|
messages: Message[]; // full history
|
|
isGenerating: boolean;
|
|
streamingText: string;
|
|
}
|
|
|
|
// Error occurred
|
|
{
|
|
type: "error";
|
|
message: string;
|
|
errorCode?: string;
|
|
}
|
|
|
|
// Generation stopped by user
|
|
{
|
|
type: "stopped";
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
### Disk Storage
|
|
|
|
```
|
|
{userCwd}/
|
|
└── .pi-sessions/
|
|
└── {sessionId}/
|
|
├── meta.json
|
|
└── messages.json
|
|
```
|
|
|
|
**meta.json:**
|
|
```json
|
|
{
|
|
"id": "uuid-string",
|
|
"title": "First 100 chars of prompt",
|
|
"model": "gpt-4o",
|
|
"cwd": "/home/user/my-project",
|
|
"createdAt": 1708396000000,
|
|
"updatedAt": 1708396000000,
|
|
"messageCount": 42,
|
|
"cost": {
|
|
"inputTokens": 5000,
|
|
"outputTokens": 3000,
|
|
"totalUSD": 0.15
|
|
}
|
|
}
|
|
```
|
|
|
|
**messages.json:**
|
|
```json
|
|
{
|
|
"messages": [
|
|
{
|
|
"id": "msg-uuid",
|
|
"timestamp": 1708396000000,
|
|
"role": "user",
|
|
"text": "hello"
|
|
},
|
|
{
|
|
"id": "msg-uuid",
|
|
"timestamp": 1708396001000,
|
|
"role": "assistant",
|
|
"text": "Hi there!",
|
|
"model": "gpt-4o",
|
|
"cost": {
|
|
"inputTokens": 100,
|
|
"outputTokens": 20,
|
|
"totalUSD": 0.001
|
|
}
|
|
},
|
|
{
|
|
"id": "msg-uuid",
|
|
"timestamp": 1708396002000,
|
|
"role": "tool",
|
|
"toolCallId": "call-uuid",
|
|
"toolName": "bash",
|
|
"toolInput": { "command": "ls -la" },
|
|
"output": "file1.txt\nfile2.txt",
|
|
"isError": false
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## REST API Endpoints
|
|
|
|
### List Sessions
|
|
```
|
|
POST /api/pi/sessions
|
|
Authorization: Bearer {token}
|
|
|
|
Response:
|
|
{
|
|
"sessions": [
|
|
{
|
|
"id": "session-uuid",
|
|
"title": "First 100 chars...",
|
|
"model": "gpt-4o",
|
|
"cwd": "/home/user/my-project",
|
|
"createdAt": 1708396000000,
|
|
"updatedAt": 1708396000000,
|
|
"messageCount": 42,
|
|
"cost": { /* as above */ }
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Get Session Detail
|
|
```
|
|
GET /api/pi/sessions/{sessionId}
|
|
Authorization: Bearer {token}
|
|
|
|
Response:
|
|
{
|
|
"session": {
|
|
"id": "session-uuid",
|
|
"title": "...",
|
|
"model": "gpt-4o",
|
|
"cwd": "/home/user/my-project",
|
|
"createdAt": 1708396000000,
|
|
"updatedAt": 1708396000000,
|
|
"messageCount": 42,
|
|
"cost": { /* as above */ },
|
|
"messages": [
|
|
// full history from messages.json
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Update Session
|
|
```
|
|
PATCH /api/pi/sessions/{sessionId}
|
|
Authorization: Bearer {token}
|
|
|
|
Body:
|
|
{
|
|
"title": "New session title"
|
|
}
|
|
|
|
Response:
|
|
{
|
|
"success": true,
|
|
"session": {
|
|
"id": "session-uuid",
|
|
"title": "New session title",
|
|
"model": "gpt-4o",
|
|
"cwd": "/home/user/my-project",
|
|
"createdAt": 1708396000000,
|
|
"updatedAt": 1708396000000,
|
|
"messageCount": 42,
|
|
"cost": { /* as above */ }
|
|
}
|
|
}
|
|
```
|
|
|
|
### Delete Session
|
|
```
|
|
DELETE /api/pi/sessions/{sessionId}
|
|
Authorization: Bearer {token}
|
|
|
|
Response:
|
|
{
|
|
"success": true
|
|
}
|
|
```
|
|
|
|
### Search Sessions
|
|
```
|
|
GET /api/pi/sessions/search?q={query}
|
|
Authorization: Bearer {token}
|
|
|
|
Response:
|
|
{
|
|
"results": [
|
|
{
|
|
"id": "session-uuid",
|
|
"title": "...",
|
|
"model": "gpt-4o",
|
|
"cwd": "/home/user/my-project",
|
|
"createdAt": 1708396000000,
|
|
"updatedAt": 1708396000000,
|
|
"messageCount": 42,
|
|
"relevance": 0.95, // optional: relevance score
|
|
"preview": "...matching text snippet..."
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### List Available Models
|
|
```
|
|
GET /api/pi/models
|
|
Authorization: Bearer {token}
|
|
|
|
Response:
|
|
{
|
|
"models": [
|
|
{
|
|
"id": "big-pickle",
|
|
"name": "Big Pickle",
|
|
"provider": "opencode-zen",
|
|
"contextWindow": 128000,
|
|
"maxTokens": 4096
|
|
},
|
|
{
|
|
"id": "claude-opus-4-5",
|
|
"name": "Claude Opus 4.5",
|
|
"provider": "anthropic",
|
|
"contextWindow": 200000,
|
|
"maxTokens": 4096
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Steps
|
|
|
|
### Phase 1: Setup & Cleanup
|
|
1. Delete all three legacy harness directories
|
|
2. Remove all references from `src/server.tsx` and `src/servers/hono.ts`
|
|
3. Create new directory structure: `src/servers/api/pi/`
|
|
|
|
### Phase 2: Core Infrastructure
|
|
1. Create `src/servers/api/pi/types.ts` — message types
|
|
2. Create `src/servers/api/pi/storage.ts` — disk I/O utilities
|
|
3. Create `src/servers/api/pi/pi-bridge.ts` — Pi RPC communication
|
|
4. Create `src/servers/api/pi/session-manager.ts` — in-memory session tracking
|
|
|
|
### Phase 3: WebSocket Handler
|
|
1. Create `src/servers/api/pi/websocket.ts` — WebSocket bridge
|
|
2. Implement session lifecycle management
|
|
3. Implement Pi process spawning/cleanup
|
|
4. Implement message streaming and history injection
|
|
|
|
### Phase 4: REST Endpoints
|
|
1. Create `src/servers/api/pi/rest.ts` — all REST handlers
|
|
2. Implement session listing, retrieval, update (rename), deletion
|
|
3. Implement search functionality
|
|
4. Implement model listing
|
|
|
|
### Phase 5: Integration
|
|
1. Wire WebSocket into `src/server.tsx`
|
|
2. Wire REST routes into `src/servers/hono.ts`
|
|
3. Update `src/servers/api/chat-types.ts` if needed
|
|
4. Test all endpoints
|
|
|
|
### Phase 6: Cleanup & Polish
|
|
1. Remove old type definitions not needed
|
|
2. Clean up any remaining references
|
|
3. Add logging/debugging
|
|
4. Document any edge cases
|
|
|
|
### Phase 6.1: Session Grouping
|
|
1. Add group types and metadata structures
|
|
2. Update storage layer to support group directories
|
|
3. Implement group management REST endpoints
|
|
4. Update existing session endpoints to handle groups
|
|
5. Add WebSocket support for group assignment
|
|
|
|
---
|
|
|
|
## Phase 6.1: Session Grouping — Detailed Implementation
|
|
|
|
**Status**: ✅ COMPLETE
|
|
**Date**: February 20, 2026
|
|
|
|
### Overview
|
|
|
|
Adds the ability to organize sessions into groups with natural language names, enabling better organization in the frontend chat list UI.
|
|
|
|
### Directory Structure
|
|
|
|
```
|
|
{cwd}/
|
|
└── .pi-sessions/
|
|
├── {sessionId}/ # Ungrouped session (root level)
|
|
│ ├── meta.json
|
|
│ └── messages.json
|
|
│
|
|
├── {anotherSessionId}/ # Another ungrouped session
|
|
│ ├── meta.json
|
|
│ └── messages.json
|
|
│
|
|
└── @refactor-pi-harness/ # GROUP (folder with @ prefix)
|
|
├── .group-meta.json # Group metadata
|
|
├── {sessionId}/ # Session inside group
|
|
│ ├── meta.json # contains groupSlug field
|
|
│ └── messages.json
|
|
└── {sessionId}/ # Another session in group
|
|
├── meta.json
|
|
└── messages.json
|
|
```
|
|
|
|
### Design Decisions
|
|
|
|
**Group Identification**:
|
|
- Groups use `@` prefix in filesystem (e.g., `@refactor-pi-harness/`)
|
|
- Distinguishes groups from sessions without checking file contents
|
|
|
|
**Group Metadata** (`.group-meta.json`):
|
|
```json
|
|
{
|
|
"name": "Refactor Pi Harness",
|
|
"slug": "refactor-pi-harness",
|
|
"description": "Sessions related to refactoring the Pi harness",
|
|
"createdAt": 1708396000000,
|
|
"updatedAt": 1708396000000,
|
|
"sessionCount": 5
|
|
}
|
|
```
|
|
|
|
**Session Metadata Updates**:
|
|
```json
|
|
{
|
|
"id": "session-uuid",
|
|
"title": "Fix WebSocket bug",
|
|
"groupSlug": "refactor-pi-harness", // null if ungrouped
|
|
"model": "gpt-4o",
|
|
"cwd": "/home/user/project",
|
|
"createdAt": 1708396000000,
|
|
"updatedAt": 1708396000000,
|
|
"messageCount": 42,
|
|
"cost": {
|
|
"inputTokens": 5000,
|
|
"outputTokens": 3000,
|
|
"totalUSD": 0.15
|
|
}
|
|
}
|
|
```
|
|
|
|
**Nesting Rules**:
|
|
- Groups cannot be nested (flat structure only)
|
|
- Sessions can only belong to one group at a time
|
|
- Sessions can be moved between groups or ungrouped
|
|
|
|
**Group Deletion Behavior**:
|
|
- When a group is deleted, all sessions are moved to root level (ungrouped)
|
|
- Session data is preserved (non-destructive deletion)
|
|
|
|
### Type Definitions
|
|
|
|
**Added to `types.ts`**:
|
|
|
|
```typescript
|
|
export type GroupMeta = {
|
|
name: string; // Natural language name
|
|
slug: string; // URL-friendly identifier (used in filesystem)
|
|
description?: string; // Optional description
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
sessionCount: number; // Automatic count of sessions in group
|
|
};
|
|
|
|
export type SessionMeta = {
|
|
id: string;
|
|
title: string;
|
|
model: string;
|
|
cwd: string;
|
|
groupSlug?: string | null; // ⭐ NEW: Reference to parent group
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
messageCount: number;
|
|
cost: MessageCost;
|
|
};
|
|
|
|
export type ClientMessage =
|
|
| {
|
|
type: "chat";
|
|
prompt: string;
|
|
sessionId?: string;
|
|
model?: string;
|
|
cwd?: string;
|
|
groupSlug?: string; // ⭐ NEW: Assign session to group on creation
|
|
attachmentIds?: string[];
|
|
}
|
|
// ... other message types
|
|
```
|
|
|
|
### Storage Functions
|
|
|
|
**Added to `storage.ts`**:
|
|
|
|
```typescript
|
|
// Group management
|
|
export async function saveGroup(cwd: string, groupMeta: GroupMeta): Promise<void>
|
|
export async function loadGroup(cwd: string, groupSlug: string): Promise<GroupMeta>
|
|
export async function groupExists(cwd: string, groupSlug: string): Promise<boolean>
|
|
export async function listGroups(baseCwd: string): Promise<GroupMeta[]>
|
|
export async function updateGroupMeta(cwd: string, groupSlug: string, updates: Partial<GroupMeta>): Promise<GroupMeta>
|
|
export async function deleteGroup(cwd: string, groupSlug: string): Promise<void>
|
|
|
|
// Session movement
|
|
export async function moveSession(
|
|
cwd: string,
|
|
sessionId: string,
|
|
fromGroupSlug: string | null,
|
|
toGroupSlug: string | null
|
|
): Promise<SessionMeta>
|
|
```
|
|
|
|
**Updated Existing Functions**:
|
|
All session functions now accept optional `groupSlug` parameter:
|
|
- `saveSession(cwd, sessionId, meta, messages)` — Uses `meta.groupSlug`
|
|
- `loadSession(cwd, sessionId, groupSlug?)` — Can specify group location
|
|
- `sessionExists(cwd, sessionId, groupSlug?)`
|
|
- `updateSessionMeta(cwd, sessionId, updates, groupSlug?)`
|
|
- `deleteSession(cwd, sessionId, groupSlug?)`
|
|
|
|
**Enhanced Search**:
|
|
- `searchSessions()` now searches group names and descriptions
|
|
- Sessions in matching groups receive higher relevance scores
|
|
- Group name matches: +2.0 relevance
|
|
- Group description matches: +1.5 relevance
|
|
|
|
### REST API Endpoints
|
|
|
|
**New Group Endpoints**:
|
|
|
|
#### Create Group
|
|
```
|
|
POST /api/pi/groups
|
|
Authorization: Bearer {token}
|
|
|
|
Body:
|
|
{
|
|
"name": "Refactor Pi Harness",
|
|
"slug": "refactor-pi-harness",
|
|
"description": "Sessions related to refactoring",
|
|
"sessionIds": ["uuid1", "uuid2"] // optional, can be empty
|
|
}
|
|
|
|
Response:
|
|
{
|
|
"success": true,
|
|
"group": {
|
|
"name": "Refactor Pi Harness",
|
|
"slug": "refactor-pi-harness",
|
|
"description": "Sessions related to refactoring",
|
|
"createdAt": 1708396000000,
|
|
"updatedAt": 1708396000000,
|
|
"sessionCount": 2
|
|
}
|
|
}
|
|
```
|
|
|
|
#### List Groups
|
|
```
|
|
GET /api/pi/groups
|
|
Authorization: Bearer {token}
|
|
|
|
Response:
|
|
{
|
|
"groups": [
|
|
{
|
|
"name": "Refactor Pi Harness",
|
|
"slug": "refactor-pi-harness",
|
|
"description": "Sessions related to refactoring",
|
|
"createdAt": 1708396000000,
|
|
"updatedAt": 1708396000000,
|
|
"sessionCount": 5
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Update Group
|
|
```
|
|
PATCH /api/pi/groups/:groupSlug
|
|
Authorization: Bearer {token}
|
|
|
|
Body:
|
|
{
|
|
"name": "New Group Name", // optional
|
|
"description": "New description" // optional
|
|
}
|
|
|
|
Response:
|
|
{
|
|
"success": true,
|
|
"group": { /* updated group metadata */ }
|
|
}
|
|
```
|
|
|
|
#### Delete Group
|
|
```
|
|
DELETE /api/pi/groups/:groupSlug
|
|
Authorization: Bearer {token}
|
|
|
|
Response:
|
|
{
|
|
"success": true
|
|
}
|
|
|
|
Note: All sessions in the group are moved to root level (ungrouped)
|
|
```
|
|
|
|
#### Move Session
|
|
```
|
|
POST /api/pi/sessions/:sessionId/move
|
|
Authorization: Bearer {token}
|
|
|
|
Body:
|
|
{
|
|
"groupSlug": "target-group" // or null to ungroup
|
|
}
|
|
|
|
Response:
|
|
{
|
|
"success": true,
|
|
"session": { /* updated session metadata */ }
|
|
}
|
|
|
|
Note: Automatically updates session counts in both source and target groups
|
|
```
|
|
|
|
**Updated Session Endpoints**:
|
|
|
|
All existing session endpoints now handle groups automatically:
|
|
- `GET /api/pi/sessions/:sessionId` — Searches in root and all groups
|
|
- `PATCH /api/pi/sessions/:sessionId` — Updates session in correct location
|
|
- `DELETE /api/pi/sessions/:sessionId` — Deletes and updates group count
|
|
- `GET /api/pi/sessions/search?q=query` — Includes group names/descriptions
|
|
|
|
### WebSocket Integration
|
|
|
|
**Creating Sessions with Groups**:
|
|
|
|
```javascript
|
|
// Client sends
|
|
{
|
|
"type": "chat",
|
|
"prompt": "Hello",
|
|
"model": "gpt-4o",
|
|
"groupSlug": "refactor-pi-harness" // ⭐ NEW: Optional group assignment
|
|
}
|
|
|
|
// Server creates session in group
|
|
// Session metadata will have groupSlug set
|
|
```
|
|
|
|
### Session Manager Updates
|
|
|
|
**`session-manager.ts`**:
|
|
|
|
```typescript
|
|
getOrCreate(
|
|
sessionId: string,
|
|
email: string,
|
|
cwd: string,
|
|
model: string,
|
|
groupSlug?: string | null // ⭐ NEW parameter
|
|
): UserSession
|
|
```
|
|
|
|
Now initializes session metadata with `groupSlug` field.
|
|
|
|
### Key Features
|
|
|
|
✅ **Natural Language Names**: Groups have both display name and slug
|
|
✅ **Non-Destructive Deletion**: Deleting groups moves sessions to root
|
|
✅ **Automatic Counting**: Group session counts updated automatically
|
|
✅ **Bulk Creation**: Create group with initial sessionIds array
|
|
✅ **Enhanced Search**: Search includes group names and descriptions
|
|
✅ **Flexible Movement**: Move sessions between groups or ungroup them
|
|
✅ **WebSocket Support**: Assign sessions to groups during creation
|
|
✅ **Backward Compatible**: Existing ungrouped sessions continue to work
|
|
|
|
### Frontend Integration Notes
|
|
|
|
**For Phase 7 (Frontend Migration)**:
|
|
|
|
1. **Chat List UI**: Display sessions grouped by `groupSlug`
|
|
2. **Group Management**: Add UI for creating/editing/deleting groups
|
|
3. **Drag & Drop**: Implement moving sessions between groups
|
|
4. **Search Enhancement**: Show group names in search results
|
|
5. **Session Creation**: Add group selector when starting new chats
|
|
|
|
**Example Frontend Structure**:
|
|
```
|
|
Chat History
|
|
├─ Ungrouped Sessions
|
|
│ ├─ Session 1
|
|
│ └─ Session 2
|
|
│
|
|
├─ 📁 Refactor Pi Harness (5 sessions)
|
|
│ ├─ Fix WebSocket bug
|
|
│ ├─ Add session grouping
|
|
│ └─ Update documentation
|
|
│
|
|
└─ 📁 Project X (3 sessions)
|
|
├─ Initial setup
|
|
└─ API implementation
|
|
```
|
|
|
|
### Testing Checklist
|
|
|
|
- [x] Create group with empty sessionIds array
|
|
- [x] Create group with initial sessions
|
|
- [x] List all groups sorted by updatedAt
|
|
- [x] Update group name and description
|
|
- [x] Delete group (sessions move to root)
|
|
- [x] Move session from root to group
|
|
- [x] Move session from group to group
|
|
- [x] Move session from group to root (ungroup)
|
|
- [x] Session counts update correctly on move/delete
|
|
- [x] Search includes group names and descriptions
|
|
- [x] WebSocket session creation with groupSlug
|
|
- [x] All existing endpoints work with grouped sessions
|
|
- [x] Group slug validation (prevents duplicate groups)
|
|
- [x] Session not found error handling
|
|
|
|
---
|
|
|
|
## Phase 7: Frontend Migration — Detailed Implementation
|
|
|
|
**Status**: 🚧 IN PROGRESS
|
|
**Date**: February 20, 2026
|
|
|
|
### Overview
|
|
|
|
Migrate the frontend from legacy provider-specific hooks (`useClaude`, `useOpenCodeModels`, etc.) to unified Pi harness hooks. Update all API calls to use the new `/api/pi/*` endpoints.
|
|
|
|
### Current Frontend State
|
|
|
|
**WebSocket Hook**: `useClaude.ts`
|
|
- URL: `/api/harness/claudecode/ws`
|
|
- Handles: streaming, tools, sessions
|
|
|
|
**Model Hooks**: Three separate hooks
|
|
- `useClaudeModels()` → `/claude/models`
|
|
- `useOpenCodeModels()` → `/opencode/models`
|
|
- `usePiMonoModels()` → `/pi-mono/models`
|
|
|
|
**Session Management**: `useChatSessions.ts`
|
|
- Provider-specific: `'/sessions/${provider}/${sessionId}/...'`
|
|
- No group support
|
|
|
|
### Phase 7.1: Type Alignment
|
|
|
|
**Status**: ✅ COMPLETE
|
|
**Date**: February 20, 2026
|
|
|
|
Update frontend types to match new wire protocol.
|
|
|
|
**What Was Done**:
|
|
- Created new types in `src/workspaces/apps/Chat/types.ts`:
|
|
- `MessageCost`, `SessionEntry`, `GroupEntry`, `ChatMessage`, `ServerMessage`, `Message`
|
|
- Created legacy type aliases for backward compatibility:
|
|
- `LegacyChatMessage`, `LegacySessionEntry`, `LegacyServerMessage`
|
|
- Updated all legacy hooks to use legacy types:
|
|
- `useClaude.ts` → `LegacyChatMessage`, `LegacyServerMessage`
|
|
- `useOpenCode.ts` → `LegacyChatMessage`, `LegacyServerMessage`
|
|
- `usePiMono.ts` → `LegacyChatMessage`, `LegacyServerMessage`
|
|
- Updated session management:
|
|
- `useChatSessions.ts` → `LegacySessionEntry`, `LegacyChatMessage`
|
|
- Updated UI components:
|
|
- `InputArea.tsx`, `Settings.tsx`, `TaskLogs/index.tsx`
|
|
- `MessageBubble.tsx`, `MessageList.tsx`, `QuestionActivity.tsx`, `ToolActivity.tsx`
|
|
|
|
**Message Type Mapping**:
|
|
|
|
| Old Type (Frontend) | New Type (Backend) | Action |
|
|
|---------------------|-------------------|--------|
|
|
| `session:init` | `session:init` | ✅ Keep |
|
|
| `assistant:partial` | `assistant:delta` | Rename |
|
|
| `assistant:text` | `assistant:text` | ✅ Keep |
|
|
| `tool:use` | `tool:start` | Rename |
|
|
| `tool:result` | `tool:result` | Update (add `toolCallId`) |
|
|
| `result` | `result` | Update (new cost structure) |
|
|
| N/A | `sync:messages` | Add (for resume) |
|
|
|
|
**Files to Update**:
|
|
- `src/apps/officer-web/state/types/chat.ts` (or wherever ChatMessage types live)
|
|
- Any shared type definitions
|
|
|
|
**New Cost Structure**:
|
|
```typescript
|
|
// Old
|
|
{ costUsd: number; durationMs: number; numTurns: number; }
|
|
|
|
// New
|
|
{
|
|
cost: {
|
|
inputTokens: number;
|
|
outputTokens: number;
|
|
totalUSD: number;
|
|
};
|
|
}
|
|
```
|
|
|
|
### Phase 7.2: Unified Pi Hook (`usePi.ts`)
|
|
|
|
Replace `useClaude.ts` with unified `usePi.ts`:
|
|
|
|
**Key Changes**:
|
|
```typescript
|
|
// Old WebSocket URL
|
|
const wsUrl = `${protocol}//${host}/api/harness/claudecode/ws?token=${token}`;
|
|
|
|
// New WebSocket URL
|
|
const wsUrl = `${protocol}//${host}/api/pi/chat/ws?token=${token}`;
|
|
```
|
|
|
|
**Updated Message Handling**:
|
|
```typescript
|
|
case 'assistant:delta': // renamed from 'assistant:partial'
|
|
streamingRef.current += msg.text;
|
|
flushStreaming();
|
|
break;
|
|
|
|
case 'tool:start': // renamed from 'tool:use'
|
|
setMessages((prev) => [
|
|
...prev,
|
|
{
|
|
role: 'tool',
|
|
toolName: msg.toolName,
|
|
toolInput: msg.toolInput,
|
|
toolCallId: msg.toolCallId, // renamed from toolUseId
|
|
},
|
|
]);
|
|
break;
|
|
|
|
case 'sync:messages': // NEW: for session resume
|
|
setSessionId(msg.sessionId);
|
|
setMessages(msg.messages);
|
|
setIsGenerating(msg.isGenerating);
|
|
if (msg.streamingText) {
|
|
streamingRef.current = msg.streamingText;
|
|
flushStreaming();
|
|
}
|
|
break;
|
|
```
|
|
|
|
**Updated sendPrompt**:
|
|
```typescript
|
|
send({
|
|
type: 'chat',
|
|
prompt: text,
|
|
sessionId: sessionIdRef.current,
|
|
model: selectedModel,
|
|
cwd: cwd,
|
|
groupSlug: groupSlug, // NEW: group assignment
|
|
attachmentIds: attachmentIds,
|
|
});
|
|
```
|
|
|
|
### Phase 7.3: Unified Models Hook
|
|
|
|
Replace three model hooks with one:
|
|
|
|
```typescript
|
|
// src/apps/officer-web/state/useModels.ts
|
|
|
|
export const usePiModels = () => {
|
|
const client = useClient();
|
|
const { isAuthenticated } = useAuth();
|
|
|
|
const { data: models = [] } = useQuery<ModelOption[]>({
|
|
queryKey: ['PI_MODELS'],
|
|
enabled: isAuthenticated,
|
|
queryFn: () => client.get<ModelOption[]>('/api/pi/models'),
|
|
staleTime: 5 * 60 * 1000,
|
|
});
|
|
|
|
return models;
|
|
};
|
|
|
|
export const useVisiblePiModels = () => {
|
|
const models = usePiModels();
|
|
const { settings } = useSettings();
|
|
const enabled = settings.ai?.enabledModels ?? [];
|
|
return useMemo(() => {
|
|
const filtered = models.filter((m) => enabled.includes(modelKey(m)));
|
|
return filtered.length > 0 ? filtered : models;
|
|
}, [models, enabled]);
|
|
};
|
|
```
|
|
|
|
### Phase 7.4: Session Management Migration
|
|
|
|
Update `useChatSessions.ts`:
|
|
|
|
**Endpoint Mapping**:
|
|
|
|
| Old Endpoint | New Endpoint |
|
|
|--------------|--------------|
|
|
| `GET /sessions` | `POST /api/pi/sessions` |
|
|
| `GET /sessions/:provider/:id/messages` | `GET /api/pi/sessions/:id` |
|
|
| `PUT /sessions/:provider/:id` | `PATCH /api/pi/sessions/:id` |
|
|
| `POST /sessions/:provider/:id/archive` | Remove (not needed) |
|
|
| `DELETE /sessions/:provider/:id` | `DELETE /api/pi/sessions/:id` |
|
|
| N/A | `GET /api/pi/sessions/search?q=` |
|
|
|
|
**Updated Hook**:
|
|
```typescript
|
|
export const useChatSessions = () => {
|
|
const client = useClient();
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data: sessions = [] } = useQuery<SessionMeta[]>({
|
|
queryKey: ['PI_SESSIONS'],
|
|
queryFn: () => client.post<{ sessions: SessionMeta[] }>('/api/pi/sessions').then(r => r.sessions),
|
|
});
|
|
|
|
const getSession = (sessionId: string) =>
|
|
client.get<{ session: SessionWithMessages }>(`/api/pi/sessions/${sessionId}`);
|
|
|
|
const renameSession = async (sessionId: string, title: string) => {
|
|
await client.patch(`/api/pi/sessions/${sessionId}`, { title });
|
|
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
|
|
};
|
|
|
|
const deleteSession = async (sessionId: string) => {
|
|
await client.delete(`/api/pi/sessions/${sessionId}`);
|
|
queryClient.setQueryData<SessionMeta[]>(
|
|
['PI_SESSIONS'],
|
|
(prev) => prev?.filter((s) => s.id !== sessionId) ?? []
|
|
);
|
|
};
|
|
|
|
const searchSessions = (query: string) =>
|
|
client.get<{ results: SessionMeta[] }>(`/api/pi/sessions/search?q=${encodeURIComponent(query)}`);
|
|
|
|
return { sessions, getSession, renameSession, deleteSession, searchSessions };
|
|
};
|
|
```
|
|
|
|
### Phase 7.5: Group Support Hooks
|
|
|
|
Add new hooks for group management:
|
|
|
|
```typescript
|
|
// src/apps/officer-web/state/useChatGroups.ts
|
|
|
|
export const useChatGroups = () => {
|
|
const client = useClient();
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data: groups = [] } = useQuery<GroupMeta[]>({
|
|
queryKey: ['PI_GROUPS'],
|
|
queryFn: () => client.get<{ groups: GroupMeta[] }>('/api/pi/groups').then(r => r.groups),
|
|
});
|
|
|
|
const createGroup = async (name: string, slug: string, description?: string, sessionIds?: string[]) => {
|
|
const result = await client.post<{ group: GroupMeta }>('/api/pi/groups', {
|
|
name, slug, description, sessionIds
|
|
});
|
|
queryClient.invalidateQueries({ queryKey: ['PI_GROUPS'] });
|
|
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
|
|
return result.group;
|
|
};
|
|
|
|
const updateGroup = async (slug: string, updates: { name?: string; description?: string }) => {
|
|
await client.patch(`/api/pi/groups/${slug}`, updates);
|
|
queryClient.invalidateQueries({ queryKey: ['PI_GROUPS'] });
|
|
};
|
|
|
|
const deleteGroup = async (slug: string) => {
|
|
await client.delete(`/api/pi/groups/${slug}`);
|
|
queryClient.invalidateQueries({ queryKey: ['PI_GROUPS'] });
|
|
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
|
|
};
|
|
|
|
const moveSession = async (sessionId: string, groupSlug: string | null) => {
|
|
await client.post(`/api/pi/sessions/${sessionId}/move`, { groupSlug });
|
|
queryClient.invalidateQueries({ queryKey: ['PI_GROUPS'] });
|
|
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
|
|
};
|
|
|
|
return { groups, createGroup, updateGroup, deleteGroup, moveSession };
|
|
};
|
|
```
|
|
|
|
### Testing Checklist (Phase 7)
|
|
|
|
- [ ] Type definitions match backend wire protocol
|
|
- [ ] `usePi` hook connects to new WebSocket endpoint
|
|
- [ ] Streaming text works correctly with `assistant:delta`
|
|
- [ ] Tool execution shows with `tool:start` / `tool:result`
|
|
- [ ] Session resume works with `sync:messages`
|
|
- [ ] Cost display shows new structure (inputTokens, outputTokens, totalUSD)
|
|
- [ ] Model picker uses unified `usePiModels`
|
|
- [ ] Session list loads from new endpoint
|
|
- [ ] Session rename works
|
|
- [ ] Session delete works
|
|
- [ ] Session search works
|
|
- [ ] Group list loads
|
|
- [ ] Create group works
|
|
- [ ] Move session to group works
|
|
- [ ] Delete group (sessions ungroup) works
|
|
|
|
---
|
|
|
|
## Phase 8: UI Enhancements
|
|
|
|
**Status**: Planned
|
|
|
|
### Phase 8.1: Grouped ChatList UI
|
|
|
|
Update ChatList component to display sessions organized by groups:
|
|
|
|
```
|
|
Chat History
|
|
├─ Ungrouped Sessions
|
|
│ ├─ Session 1
|
|
│ └─ Session 2
|
|
│
|
|
├─ 📁 Refactor Pi Harness (5 sessions)
|
|
│ ├─ Fix WebSocket bug
|
|
│ ├─ Add session grouping
|
|
│ └─ Update documentation
|
|
│
|
|
└─ 📁 Project X (3 sessions)
|
|
├─ Initial setup
|
|
└─ API implementation
|
|
```
|
|
|
|
**Implementation**:
|
|
- Group sessions by `groupSlug` field
|
|
- Collapsible group sections
|
|
- Session count badges
|
|
- Sort groups by `updatedAt`
|
|
- Ungrouped sessions at top or bottom (configurable)
|
|
|
|
### Phase 8.2: Group Management UI
|
|
|
|
Add UI for managing groups:
|
|
|
|
- **Create Group Dialog**: Name, slug (auto-generated), description
|
|
- **Rename Group**: Inline edit or dialog
|
|
- **Delete Group**: Confirmation dialog, explain sessions will ungroup
|
|
- **Drag & Drop**: Move sessions between groups (optional, can defer)
|
|
|
|
### Phase 8.3: Search Enhancements
|
|
|
|
Improve search functionality:
|
|
|
|
- Show group names in search results
|
|
- Filter by group dropdown
|
|
- Highlight matching text in results
|
|
- Show relevance scores (optional)
|
|
|
|
### Testing Checklist (Phase 8)
|
|
|
|
- [ ] ChatList displays grouped structure
|
|
- [ ] Groups are collapsible
|
|
- [ ] Session counts are accurate
|
|
- [ ] Create group dialog works
|
|
- [ ] Rename group works
|
|
- [ ] Delete group shows confirmation
|
|
- [ ] Sessions ungroup correctly when group deleted
|
|
- [ ] Search shows group context
|
|
- [ ] Filter by group works
|
|
|
|
---
|
|
|
|
## Phase 9: Final Cleanup
|
|
|
|
**Status**: Planned
|
|
|
|
### Phase 9.1: Frontend Legacy Cleanup
|
|
|
|
Remove all legacy code:
|
|
|
|
**Hooks to Delete**:
|
|
- `useClaudeModels()` from `useModels.ts`
|
|
- `useOpenCodeModels()` from `useModels.ts`
|
|
- `usePiMonoModels()` from `useModels.ts`
|
|
- `useVisibleClaudeModels()` from `useModels.ts`
|
|
- `useVisibleOpenCodeModels()` from `useModels.ts`
|
|
- `useVisiblePiMonoModels()` from `useModels.ts`
|
|
|
|
**Files to Delete**:
|
|
- `useClaude.ts` (replaced by `usePi.ts`)
|
|
- `OpenCodeModelPicker.tsx` (if provider-specific)
|
|
|
|
**Code to Update**:
|
|
- Remove `provider` parameter from all session functions
|
|
- Remove provider routing logic from ChatPanel
|
|
- Update all imports to use new hooks
|
|
|
|
### Phase 9.2: Backend Final Cleanup
|
|
|
|
Verify backend is clean:
|
|
|
|
- Confirm old harness directories deleted (`src/servers/api/claude/`, etc.)
|
|
- Remove any remaining legacy routes from `hono.ts`
|
|
- Remove legacy WebSocket handlers from `server.tsx`
|
|
- Update any remaining type imports
|
|
- Clean up `chat-types.ts` if needed
|
|
|
|
### Testing Checklist (Phase 9)
|
|
|
|
- [ ] No TypeScript errors after cleanup
|
|
- [ ] Build succeeds
|
|
- [ ] No console errors in browser
|
|
- [ ] All chat features work end-to-end
|
|
- [ ] No dead code remaining
|
|
- [ ] Documentation up to date
|
|
|
|
---
|
|
|
|
## Implementation Details
|
|
|
|
### Session Manager (`session-manager.ts`)
|
|
|
|
Tracks active Pi processes in memory:
|
|
|
|
```typescript
|
|
type UserSession = {
|
|
sessionId: string;
|
|
email: string;
|
|
cwd: string;
|
|
model: string;
|
|
piProcess: Subprocess | null;
|
|
ws: ServerWebSocket | null;
|
|
lastActivity: number;
|
|
idleTimer: Timer | null;
|
|
streamBuffer: string;
|
|
isGenerating: boolean;
|
|
systemContextSent: boolean;
|
|
};
|
|
|
|
class SessionManager {
|
|
private sessions = new Map<string, UserSession>(); // sessionId → session
|
|
private userSessions = new Map<string, string[]>(); // email → [sessionIds]
|
|
|
|
getOrCreate(sessionId: string, email: string, cwd: string, model: string): UserSession;
|
|
getSession(sessionId: string): UserSession | null;
|
|
getUserSessions(email: string): UserSession[];
|
|
deleteSession(sessionId: string): void;
|
|
attachWs(sessionId: string, ws: ServerWebSocket): void;
|
|
detachWs(sessionId: string): void;
|
|
setIdleTimeout(sessionId: string, timeoutMs: number): void;
|
|
}
|
|
```
|
|
|
|
### Storage (`storage.ts`)
|
|
|
|
```typescript
|
|
class SessionStorage {
|
|
async saveSession(
|
|
cwd: string,
|
|
sessionId: string,
|
|
meta: SessionMeta,
|
|
messages: Message[]
|
|
): Promise<void>;
|
|
|
|
async loadSession(
|
|
cwd: string,
|
|
sessionId: string
|
|
): Promise<{ meta: SessionMeta; messages: Message[] }>;
|
|
|
|
async listUserSessions(
|
|
email: string,
|
|
baseCwd?: string
|
|
): Promise<SessionMeta[]>;
|
|
|
|
async deleteSession(
|
|
cwd: string,
|
|
sessionId: string
|
|
): Promise<void>;
|
|
|
|
async searchSessions(
|
|
email: string,
|
|
query: string,
|
|
baseCwd?: string
|
|
): Promise<SessionMeta[]>;
|
|
}
|
|
```
|
|
|
|
### Pi Bridge (`pi-bridge.ts`)
|
|
|
|
```typescript
|
|
class PiBridge {
|
|
async spawn(
|
|
cwd: string,
|
|
model: string,
|
|
env?: Record<string, string>
|
|
): Promise<Subprocess>;
|
|
|
|
sendPrompt(
|
|
process: Subprocess,
|
|
prompt: string,
|
|
requestId: string
|
|
): void;
|
|
|
|
abort(
|
|
process: Subprocess,
|
|
requestId: string
|
|
): void;
|
|
|
|
// Returns async generator of Pi events
|
|
readEvents(process: Subprocess): AsyncGenerator<PiEvent>;
|
|
}
|
|
```
|
|
|
|
### WebSocket Lifecycle
|
|
|
|
1. **Open**: Empty handler (connection established)
|
|
2. **Message**:
|
|
- If `type: 'chat'`:
|
|
- Load/create session
|
|
- Spawn Pi if needed
|
|
- Send prompt to Pi
|
|
- Start streaming responses
|
|
- If `type: 'resume'`:
|
|
- Load session from disk
|
|
- Spawn fresh Pi
|
|
- Send full history as system context
|
|
- Send `sync:messages`
|
|
- If `type: 'stop'`:
|
|
- Send abort to Pi
|
|
3. **Close**:
|
|
- Detach WebSocket from session
|
|
- Start idle timer (1 hour)
|
|
- On timeout: kill Pi, save session to disk, delete from memory
|
|
|
|
### Key Features
|
|
|
|
**History Injection**:
|
|
```typescript
|
|
function buildSystemPrompt(messages: Message[], homeDir: string, skills: string): string {
|
|
const history = messages
|
|
.map(msg => `${msg.role}: ${msg.text}`)
|
|
.join("\n");
|
|
|
|
return `
|
|
<system>
|
|
User home directory: ${homeDir}
|
|
${skills}
|
|
|
|
Below is the conversation history from this session:
|
|
<conversation_history>
|
|
${history}
|
|
</conversation_history>
|
|
</system>
|
|
`;
|
|
}
|
|
```
|
|
|
|
**Streaming Text**:
|
|
- Accumulate `assistant:delta` in `streamBuffer`
|
|
- Flush to state on `assistant:text` or message boundary
|
|
- Prevents render thrashing
|
|
|
|
**Error Handling**:
|
|
- Pi process death → send error to client, save partial state
|
|
- WS disconnect → idle timeout, then cleanup
|
|
- Invalid JSON → send error, continue
|
|
|
|
---
|
|
|
|
## Testing Checklist
|
|
|
|
- [ ] Delete cleanup complete, no compilation errors
|
|
- [ ] New chat creates session on disk
|
|
- [ ] Resume old chat loads from disk and injects history
|
|
- [ ] Model switching works (different models per session)
|
|
- [ ] Multiple concurrent sessions don't interfere
|
|
- [ ] Idle timeout kills Pi process
|
|
- [ ] WS disconnect followed by reconnect works
|
|
- [ ] REST endpoints return correct data
|
|
- [ ] Search returns relevant results
|
|
- [ ] Streaming text appears correctly
|
|
- [ ] Tool execution tracked properly
|
|
- [ ] Session metadata updated on cost/message count
|
|
|
|
---
|
|
|
|
## Notes
|
|
|
|
- All paths use `{userCwd}/.pi-sessions/` for session storage
|
|
- Default cwd is `getHomeDir(email)` if not provided
|
|
- Pi processes spawned with `--mode rpc --no-extensions --no-skills`
|
|
- Message IDs should be UUIDs for uniqueness
|
|
- Timestamps in milliseconds (Date.now())
|
|
- Cost tracking passed from Pi via `result` events
|
|
|
|
---
|
|
|
|
**Ready to implement?** Confirm and start Phase 1.
|