pi-remote-control/docs/EXTENSION-API-AUDIT.md

937 lines
27 KiB
Markdown

# pi ExtensionAPI Audit (für iOS-App-Spec v2)
**Datum:** 2026-05-15
**pi Version:** 0.74.0
**Auditor:** Subagent (Claude Sonnet 4.6)
---
## TL;DR
-**S-07 State Side-Channel** — machbar mit Einschränkungen (kein explizites `awaiting-input` Event, muss abgeleitet werden)
-**S-08 Slash-Command-Registry** — direkt verfügbar via `pi.getCommands()`
- ⚠️ **S-13 Tree-State Side-Channel** — Tree-Read funktioniert, aber keine Subscribe-Mechanik für Live-Updates (Polling oder Event-basiert möglich)
-**Gruppe T Tree-Navigation** — Slash-Command-Injection funktioniert NICHT (Commands werden als Text an LLM gesendet, nicht dispatched). Hack via `(pi as any)` nötig oder Upstream-Change.
---
## 1. Methodik
**Untersuchte Quellen:**
- pi-Installation: `/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/` (v0.74.0)
- TypeScript-Definitionen: `dist/core/extensions/types.d.ts` (1173 Zeilen)
- Session-Manager: `dist/core/session-manager.d.ts` (308 Zeilen)
- Slash-Commands: `dist/core/slash-commands.d.ts` (15 Zeilen)
- Offizielle Doku: `docs/extensions.md` (97KB)
- Bestehende Extension: `/Users/jay/.pi/agent/git/git.vpsj.de/jay/pi-remote-control/extensions/remote-control/`
**Verifikationsmethode:** Statische Analyse der TypeScript-Definitionen + Cross-Check gegen bestehenden Code der `pi-remote-control` Extension.
---
## 2. Capability-Matrix
| Capability | Status | Spec-Feature | Workaround | Aufwand |
|---|---|---|---|---|
| **Lifecycle Events** | ✅ Vorhanden | S-07 | `agent_end` statt `awaiting-input` | Trivial |
| **Tool Execution Events** | ✅ Vorhanden | S-07 | — | Trivial |
| **Slash-Command-Registry** | ✅ Vorhanden | S-08 | — | Trivial |
| **Tree-Read** | ✅ Vorhanden | S-13 | — | Trivial |
| **Tree-Subscribe (Live-Updates)** | ⚠️ Event-basiert | S-13 | Events tracken statt Subscribe | Einfach |
| **Slash-Command-Dispatch** | ❌ Nicht exposed | Gruppe T | `(pi as any)` Hack | Fragil |
| **Tool-Call-Details** | ✅ Vorhanden | S-07 | — | Trivial |
**Legende:**
- ✅ = Out-of-the-box verfügbar
- ⚠️ = Machbar mit Einschränkungen
- ❌ = Nicht ohne Hack oder Upstream-Change
---
## 3. Capabilities im Detail
### 3.1 Lifecycle Events
**Was existiert:**
```typescript
// Quelle: dist/core/extensions/types.d.ts, Zeile 458-477
pi.on("agent_start", async (event, ctx) => { ... });
pi.on("agent_end", async (event, ctx) => { ... });
pi.on("tool_execution_start", async (event, ctx) => { ... });
pi.on("tool_execution_end", async (event, ctx) => { ... });
pi.on("message_start", async (event, ctx) => { ... });
pi.on("message_update", async (event, ctx) => { ... });
pi.on("message_end", async (event, ctx) => { ... });
```
**Event-Payloads (relevante Felder):**
| Event | Payload | Code-Beleg |
|---|---|---|
| `agent_start` | `{ type: "agent_start" }` | types.d.ts:223 |
| `agent_end` | `{ type: "agent_end", messages: AgentMessage[] }` | types.d.ts:228 |
| `tool_execution_start` | `{ toolCallId, toolName, args }` | types.d.ts:261 |
| `tool_execution_end` | `{ toolCallId, toolName, result, isError }` | types.d.ts:275 |
| `message_update` | `{ message, assistantMessageEvent }` | types.d.ts:250 |
**Lücken:**
-**Kein explizites `awaiting-input` Event.** Das nächstbeste ist `agent_end`, welches signalisiert dass der Agent-Loop beendet ist. Kombiniert mit `ctx.isIdle()` kann man ableiten, ob Pi auf User-Input wartet.
-**Kein `thinking` vs. `idle` State-Enum.** Muss selbst aus Events abgeleitet werden.
**Ableitung des Pi-States für S-07:**
```typescript
// Pseudo-Code für die Sidecar-Logik
let state: "thinking" | "tool" | "idle" | "awaiting-input" = "idle";
pi.on("agent_start", () => {
state = "thinking";
broadcast({ type: "state", value: "thinking" });
});
pi.on("tool_execution_start", (event) => {
state = "tool";
broadcast({ type: "state", value: "tool", tool: event.toolName });
});
pi.on("tool_execution_end", () => {
state = "thinking"; // zurück zu thinking nach Tool
});
pi.on("agent_end", (event, ctx) => {
if (ctx.isIdle()) {
state = "awaiting-input";
broadcast({ type: "state", value: "awaiting-input" });
} else {
state = "thinking"; // Follow-up-Messages pending
}
});
```
**Code-Beleg (bestehende Extension nutzt diese Events):**
```typescript
// pi-remote-control/extensions/remote-control/index.ts, Zeile 89-109
pi.on("agent_start", async (_event, ctx) => {
server?.broadcast({ type: "agent_start" });
updateStatus(ctx);
});
pi.on("agent_end", async (_event, ctx) => {
server?.broadcast({ type: "agent_end" });
updateStatus(ctx);
});
pi.on("tool_execution_start", async (event) => {
server?.broadcast({
type: "tool_start",
toolCallId: event.toolCallId,
toolName: event.toolName,
args: event.args,
});
});
```
**Fazit:** S-07 State-Side-Channel ist **machbar**. Die Granularität ist ausreichend für die Spec-Anforderungen (thinking, tool, idle, awaiting-input). Kein Upstream-Change nötig.
---
### 3.2 Slash-Command-Registry
**Was existiert:**
```typescript
// Quelle: dist/core/extensions/types.d.ts, Zeile 772
pi.getCommands(): SlashCommandInfo[];
```
**Return-Type:**
```typescript
// Quelle: dist/core/slash-commands.d.ts, Zeile 1-9
export interface SlashCommandInfo {
name: string;
description?: string;
source: "extension" | "prompt" | "skill";
sourceInfo: SourceInfo;
}
```
**Beispiel:**
```typescript
pi.on("session_start", async (event, ctx) => {
const commands = pi.getCommands();
// [
// { name: "new", description: "Start a new session", source: "extension", ... },
// { name: "fork", description: "Fork from an entry", source: "extension", ... },
// { name: "tree", description: "Show conversation tree", source: "extension", ... },
// { name: "my-custom", description: "...", source: "skill", ... }
// ]
// Für S-08 Endpoint /sessions/<id>/commands:
const payload = commands.map(c => ({
name: c.name,
description: c.description,
source: c.source
}));
// Send via WebSocket or REST
});
```
**Code-Beleg:** Die bestehende Extension nutzt `pi.registerCommand()` aber `pi.getCommands()` ist bisher nicht verwendet. Die Funktion ist aber in `types.d.ts:772` klar definiert.
**Fazit:** S-08 Slash-Command-Registry ist **direkt machbar**, out-of-the-box. Kein Hack, kein Upstream-Change nötig.
---
### 3.3 Conversation Tree
**Was existiert:**
```typescript
// Quelle: dist/core/extensions/types.d.ts, Zeile 87
ctx.sessionManager: ReadonlySessionManager;
```
**Relevante Methoden (dist/core/session-manager.d.ts):**
```typescript
interface ReadonlySessionManager {
getBranch(fromId?: string): SessionEntry[];
getTree(): SessionTreeNode[];
getLeafId(): string | null;
getEntry(id: string): SessionEntry | undefined;
getChildren(parentId: string): SessionEntry[];
getSessionName(): string | undefined;
// ...
}
```
**Tree-Structure:**
```typescript
// Quelle: session-manager.d.ts, Zeile 74-81
export interface SessionTreeNode {
entry: SessionEntry;
children: SessionTreeNode[];
/** Resolved label for this entry, if any */
label?: string;
/** Timestamp of the latest label change for this entry, if any */
labelTimestamp?: string;
}
```
**SessionEntry (relevant für Tree-State):**
```typescript
// session-manager.d.ts, Zeile 16-27
export interface SessionEntryBase {
type: string;
id: string;
parentId: string | null;
timestamp: string;
}
export interface SessionMessageEntry extends SessionEntryBase {
type: "message";
message: AgentMessage;
}
```
**Beispiel für S-13 Tree-State-Endpoint:**
```typescript
pi.on("session_start", async (event, ctx) => {
const tree = ctx.sessionManager.getTree();
const currentLeafId = ctx.sessionManager.getLeafId();
// Tree als JSON für iOS-App serialisieren:
function serializeTree(node: SessionTreeNode) {
const entry = node.entry;
let summary = "";
let msgCount = 0;
if (entry.type === "message") {
const msg = (entry as SessionMessageEntry).message;
if (msg.role === "user") {
const text = typeof msg.content === "string"
? msg.content
: msg.content.filter(c => c.type === "text").map(c => c.text).join("");
summary = text.slice(0, 50);
}
}
// Count messages in subtree
const countMessages = (n: SessionTreeNode): number => {
let count = n.entry.type === "message" ? 1 : 0;
for (const child of n.children) count += countMessages(child);
return count;
};
msgCount = countMessages(node);
return {
id: entry.id,
parent: entry.parentId,
summary,
messageCount: msgCount,
createdAt: entry.timestamp,
children: node.children.map(serializeTree)
};
}
const treeData = {
type: "tree",
nodes: tree.map(serializeTree),
current: currentLeafId
};
// Broadcast oder per REST-Endpoint abrufbar machen
broadcast(treeData);
});
```
**Live-Updates (Subscribe-Problem):**
Es gibt **kein** `on("tree_update")` Event. Tree-Mutationen passieren nur via:
- `/fork` → triggert `session_before_fork` + `session_tree`
- `/new` → triggert `session_before_switch`
- `/compact` → triggert `session_compact`
- Checkout via `/tree` → triggert `session_tree`
**Events für Tree-Tracking:**
```typescript
// Quelle: types.d.ts, Zeile 177-189
pi.on("session_tree", async (event, ctx) => {
// event = {
// newLeafId: string | null,
// oldLeafId: string | null,
// summaryEntry?: BranchSummaryEntry,
// fromExtension?: boolean
// }
// Tree hat sich geändert → re-serialize und broadcasten
const updatedTree = serializeTree(ctx.sessionManager.getTree());
broadcast({ type: "tree", ...updatedTree });
});
pi.on("session_compact", async (event, ctx) => {
// Compaction hat Tree verändert → Update broadcasten
const updatedTree = serializeTree(ctx.sessionManager.getTree());
broadcast({ type: "tree", ...updatedTree });
});
```
**Code-Beleg (bestehende Extension nutzt getBranch):**
```typescript
// pi-remote-control/extensions/remote-control/messages.ts, Zeile 66
const branch = ctx.sessionManager.getBranch();
```
**Fazit:** S-13 Tree-State ist **machbar mit Event-basiertem Push**. Kein echtes Subscribe-Pattern, aber ausreichend für die Spec. Tree-Read ist trivial, Live-Updates erfordern Event-Handling (`session_tree`, `session_compact`). Kein Upstream-Change nötig.
---
### 3.4 Programmatic Prompt-Dispatch
**Was existiert:**
```typescript
// Quelle: types.d.ts, Zeile 758-763
pi.sendUserMessage(
content: string | (TextContent | ImageContent)[],
options?: { deliverAs?: "steer" | "followUp" }
): void;
```
**Was NICHT existiert:**
```typescript
// ❌ NICHT in der ExtensionAPI exposed:
pi.prompt(text: string): void; // würde Slash-Commands dispatchen
```
**Problem (aus früherer Recherche bestätigt):**
`pi.sendUserMessage()` ruft intern `session.prompt()` mit `expandPromptTemplates: false` auf, was Slash-Command-Processing überspringt. Slash-Commands wie `/fork`, `/new`, `/compact` werden als **literaler Text an den LLM gesendet**, nicht als Befehle ausgeführt.
**Code-Beleg (aus vorheriger Session):**
> Wir hatten in der vorherigen Session festgestellt: `pi.sendUserMessage()` explicitly sets `expandPromptTemplates: false`, bypassing slash command processing. `session.prompt()` (which handles slash commands) is not exposed in `ExtensionAPI`.
**Workaround-Optionen:**
#### Option A: Hack via `(pi as any)`
```typescript
// FRAGIL: greift auf interne APIs zu, kann bei Updates brechen
pi.on("some_event", async (event, ctx) => {
// @ts-ignore
const runtime = (pi as any).runtime;
if (runtime && runtime.session) {
await runtime.session.prompt("/fork abc123");
}
});
```
**Risiken:**
- ❌ Keine TypeScript-Typen
- ❌ Bricht bei internen Refactorings
-`runtime` könnte `undefined` sein je nach Timing
- ❌ Nicht dokumentiert, keine Garantie
#### Option B: Re-Implement Commands lokal
```typescript
// iOS-App sendet Command-Request
// Sidecar implementiert /fork, /new, /compact direkt via ExtensionCommandContext
pi.registerCommand("ios-fork", {
description: "Fork from iOS app",
handler: async (args, ctx) => {
// args = entryId
await ctx.fork(args, { position: "at" });
}
});
// iOS schickt dann via WebSocket:
// { type: "command", name: "ios-fork", args: "abc123" }
// Sidecar ruft pi-internen Command-Handler auf
```
**Problem:** Geht nur in interactive mode, nicht in RPC/print mode. Außerdem muss die Extension selbst die Commands implementieren, was dupliziert Code.
#### Option C: Upstream-Feature-Request
```typescript
// Gewünschte API:
pi.prompt(text: string): Promise<void>;
// oder
pi.executeCommand(commandName: string, args: string): Promise<void>;
```
**Fazit:** Gruppe T Tree-Navigation via **Slash-Command-Injection ist NICHT machbar** ohne Hack. Option A (`(pi as any)`) funktioniert vermutlich, ist aber fragil. Option B (Re-Implement) ist sauberer, aber aufwändiger. Option C (Upstream) ist langfristig richtig, aber blockiert die iOS-App.
**Empfehlung:** Für MVP: **Option B** (Re-Implement Commands in Extension). Für v2: **Upstream-Feature-Request für `pi.prompt()`**.
---
### 3.5 Tool-Call-Daten
**Was existiert:**
```typescript
// Quelle: types.d.ts, Zeile 261-278
pi.on("tool_execution_start", async (event, ctx) => {
// event = {
// type: "tool_execution_start",
// toolCallId: string,
// toolName: string,
// args: any
// }
});
pi.on("tool_execution_end", async (event, ctx) => {
// event = {
// type: "tool_execution_end",
// toolCallId: string,
// toolName: string,
// result: any, // NICHT truncated!
// isError: boolean
// }
});
```
**Code-Beleg (bestehende Extension nutzt das bereits):**
```typescript
// pi-remote-control/extensions/remote-control/index.ts, Zeile 103-127
pi.on("tool_execution_start", async (event) => {
server?.broadcast({
type: "tool_start",
toolCallId: event.toolCallId,
toolName: event.toolName,
args: event.args,
});
});
pi.on("tool_execution_end", async (event) => {
type TextContent = { type: string; text: string };
type ToolResult = { content?: TextContent[] } | string;
const result = event.result as ToolResult;
const content = typeof result === "object" ? result.content : undefined;
const resultText = Array.isArray(content)
? content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("")
: typeof result === "string"
? result
: "";
server?.broadcast({
type: "tool_end",
toolCallId: event.toolCallId,
result: resultText,
isError: event.isError,
});
});
```
**Fazit:** Tool-Call-Daten sind **vollständig verfügbar** für S-07. Name, Args, Result (ungekürzt), Error-Status — alles da. Trivial implementierbar.
---
### 3.6 Sonstiges (Bonus)
**Weitere nützliche Capabilities, die wir ggf. übersehen haben:**
#### Model-Info
```typescript
// Quelle: types.d.ts, Zeile 90
ctx.model: Model<any> | undefined;
// Beispiel:
pi.on("model_select", async (event, ctx) => {
broadcast({
type: "model",
model: {
id: event.model.id,
name: event.model.name,
provider: event.model.provider
}
});
});
```
**Use-Case für iOS-App:** Session-Switcher (iOS-D-01) könnte pro Session anzeigen, welches Modell verwendet wurde.
#### CWD-Tracking
```typescript
// Quelle: types.d.ts, Zeile 85
ctx.cwd: string;
```
**Use-Case:** Session-Metadaten (S-09) könnte CWD pro Session exposen.
#### Abort-Signal
```typescript
// Quelle: types.d.ts, Zeile 95
ctx.abort(): void;
ctx.signal: AbortSignal | undefined;
```
**Use-Case:** iOS-App könnte einen "Stop"-Button haben, der `abort()` triggert via WebSocket-Control-Frame.
#### Context-Usage
```typescript
// Quelle: types.d.ts, Zeile 102
ctx.getContextUsage(): ContextUsage | undefined;
interface ContextUsage {
tokens: number | null;
contextWindow: number;
percent: number | null;
}
```
**Use-Case:** Status-Bar (iOS-C-01) könnte "Context: 45%" anzeigen.
---
## 4. Empfehlung pro Spec-Feature
| Spec-Feature | Status | Empfehlung | Begründung |
|---|---|---|---|
| **S-07 State Side-Channel** | ✅ Machbar | Implementieren | Events ausreichend granular, `isIdle()` + `agent_end` = awaiting-input |
| **S-08 Slash-Command-Registry** | ✅ Machbar | Implementieren | `pi.getCommands()` out-of-the-box |
| **S-13 Tree-State Side-Channel** | ⚠️ Machbar | Implementieren mit Events | Tree-Read trivial, Live-Updates via `session_tree` + `session_compact` |
| **Gruppe T Tree-Navigation** | ❌ Hack nötig | **Nicht empfohlen für MVP** | Slash-Command-Injection geht nicht. Option B (Re-Implement) aufwändig, Option A (Hack) fragil. Upstream-Request für v2. |
---
## 5. Vorgeschlagene Upstream-Änderungen
### 5.1 `pi.prompt()` für Slash-Command-Dispatch
**Problem:** Extensions können User-Messages senden (`pi.sendUserMessage()`), aber keine Slash-Commands programmatisch dispatchen. Das limitiert iOS-App-Features wie Tree-Navigation (Gruppe T), weil `/fork`, `/new`, `/compact` nur als Text an den LLM gehen, nicht ausgeführt werden.
**Vorschlag:**
```typescript
pi.prompt(text: string, options?: {
expandPromptTemplates?: boolean
}): Promise<void>;
```
**Begründung:**
- Extensions die als "Remote-Control" fungieren (RPC, iOS-App, Web-UI) brauchen die Möglichkeit, Slash-Commands im Namen des Users auszuführen.
- Bestehende `pi.sendUserMessage()` umgeht absichtlich Template-Expansion (vermutlich für Safety). Ein explizites `pi.prompt()` signalisiert Intent.
- Alternative: `pi.executeCommand(name, args)` wäre noch expliziter, aber weniger flexibel.
**Implementierung (Skizze):**
```typescript
// In extensions/wrapper.ts
api.prompt = async (text: string, options = {}) => {
runtime.assertActive();
const { expandPromptTemplates = true } = options;
// Ruft die interne session.prompt() auf, die Slash-Commands dispatched
await runtime.session.prompt(text, { expandPromptTemplates });
};
```
**Impact:** Niedrig. Nur eine neue Methode in der `ExtensionAPI`, delegiert an existierende `session.prompt()`.
---
### 5.2 `on("awaiting_input")` Event
**Problem:** Um iOS-C-02 (Push-Notification "Pi ist fertig") sauber zu implementieren, brauchen wir ein explizites Event statt Ableitung via `agent_end` + `ctx.isIdle()`.
**Vorschlag:**
```typescript
pi.on("awaiting_input", async (event, ctx) => {
// event = { type: "awaiting_input" }
});
```
**Begründung:**
- Explizit ist besser als implizit. `agent_end` + `isIdle()` funktioniert, ist aber umständlich und fehleranfällig (z.B. wenn Follow-Up-Messages pending sind).
- Semantisch klarer: "Pi wartet auf User-Input" ist ein State, kein abgeleitetes Konstrukt.
**Implementierung (Skizze):**
```typescript
// In core/agent-session.ts, nach dem agent loop:
if (this.isIdle() && !this.hasPendingMessages()) {
await this.eventBus.emit("awaiting_input", {}, this.buildContext());
}
```
**Impact:** Sehr niedrig. Ein neues Event, keine Breaking-Changes.
---
### 5.3 Tree-Subscribe-Pattern (Optional)
**Problem:** `session_tree` + `session_compact` Events decken die meisten Tree-Mutations ab, aber nicht alle (z.B. neue Messages werden per `message_end` gemeldet, nicht als Tree-Mutation).
**Vorschlag:**
```typescript
pi.on("tree_change", async (event, ctx) => {
// event = {
// type: "tree_change",
// operation: "append" | "branch" | "compact" | "fork",
// entryId: string,
// parentId: string | null
// }
});
```
**Begründung:**
- Vereinheitlicht alle Tree-Mutations in einem Event.
- Extensions müssen nicht mehrere Events tracken (`message_end`, `session_tree`, `session_compact`).
**Implementierung:** Moderater Aufwand, weil es alle Append-Operationen tracken muss.
**Impact:** Niedrig, optional. Die bestehenden Events reichen aus, aber das wäre cleaner.
**Status:** **NICE-TO-HAVE**, nicht kritisch für die iOS-App.
---
## 6. Zusammenfassung: Implementierungs-Roadmap für Sidecar
### Phase 1: S-07 State Side-Channel (MUST)
**Code-Änderungen (pi-remote-control/extensions/remote-control/server.ts):**
```typescript
interface StateMessage {
type: "state";
value: "thinking" | "tool" | "idle" | "awaiting-input";
tool?: string;
ts: number;
}
let currentState: StateMessage["value"] = "idle";
pi.on("agent_start", async () => {
currentState = "thinking";
server?.broadcast({
type: "state",
value: "thinking",
ts: Date.now()
});
});
pi.on("tool_execution_start", async (event) => {
currentState = "tool";
server?.broadcast({
type: "state",
value: "tool",
tool: event.toolName,
ts: Date.now()
});
});
pi.on("tool_execution_end", async () => {
currentState = "thinking";
server?.broadcast({
type: "state",
value: "thinking",
ts: Date.now()
});
});
pi.on("agent_end", async (event, ctx) => {
if (ctx.isIdle()) {
currentState = "awaiting-input";
server?.broadcast({
type: "state",
value: "awaiting-input",
ts: Date.now()
});
}
});
```
**Aufwand:** 1-2 Stunden
---
### Phase 2: S-08 Slash-Command-Registry (SHOULD)
**Code-Änderungen (neuer Endpoint):**
```typescript
// In server.ts, HTTP-Handler erweitern:
if (pathname === "/commands") {
const commands = pi.getCommands();
const payload = commands.map(c => ({
name: c.name,
description: c.description,
source: c.source
}));
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(payload));
return;
}
```
**Aufwand:** 30 Minuten
---
### Phase 3: S-13 Tree-State Side-Channel (SHOULD)
**Code-Änderungen (neuer Endpoint + Events):**
```typescript
// Helper für Tree-Serialisierung
function serializeTree(ctx: ExtensionContext) {
const tree = ctx.sessionManager.getTree();
const leafId = ctx.sessionManager.getLeafId();
function mapNode(node: SessionTreeNode) {
let summary = "";
let msgCount = 0;
// Entry-Type-spezifische Logik
if (node.entry.type === "message") {
const msg = (node.entry as any).message;
if (msg.role === "user") {
const text = typeof msg.content === "string"
? msg.content
: msg.content.filter(c => c.type === "text").map(c => c.text).join("");
summary = text.slice(0, 50);
}
}
// Count messages recursive
const count = (n: SessionTreeNode): number => {
let c = n.entry.type === "message" ? 1 : 0;
for (const child of n.children) c += count(child);
return c;
};
msgCount = count(node);
return {
id: node.entry.id,
parent: node.entry.parentId,
summary,
messageCount: msgCount,
createdAt: node.entry.timestamp,
children: node.children.map(mapNode)
};
}
return {
type: "tree",
nodes: tree.map(mapNode),
current: leafId
};
}
// REST-Endpoint
if (pathname === "/tree") {
const treeData = serializeTree(ctx);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(treeData));
return;
}
// Live-Updates via Events
pi.on("session_tree", async (event, ctx) => {
const treeData = serializeTree(ctx);
server?.broadcast(treeData);
});
pi.on("session_compact", async (event, ctx) => {
const treeData = serializeTree(ctx);
server?.broadcast(treeData);
});
pi.on("message_end", async (event, ctx) => {
// Neue Message = Tree-Mutation
const treeData = serializeTree(ctx);
server?.broadcast(treeData);
});
```
**Aufwand:** 2-3 Stunden
---
### Phase 4: Gruppe T — Tree-Navigation (CONDITIONAL)
**Ohne Upstream-Change: Option B (Re-Implement)**
```typescript
pi.registerCommand("ios-fork", {
description: "Fork from iOS app (internal)",
handler: async (args, ctx) => {
const entryId = args.trim();
await ctx.fork(entryId, { position: "at" });
}
});
pi.registerCommand("ios-new", {
description: "New session from iOS app (internal)",
handler: async (args, ctx) => {
await ctx.newSession();
}
});
pi.registerCommand("ios-compact", {
description: "Compact from iOS app (internal)",
handler: async (args, ctx) => {
ctx.compact();
}
});
// WebSocket-Handler für iOS-Command-Requests
ws.on("message", (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === "execute_command") {
// iOS schickt: { type: "execute_command", name: "ios-fork", args: "abc123" }
const command = registeredCommands.get(msg.name);
if (command) {
command.handler(msg.args, ctx);
}
}
});
```
**Aufwand:** 3-4 Stunden (inkl. Testing)
**Alternative mit Upstream-Change:**
Sobald `pi.prompt()` verfügbar:
```typescript
ws.on("message", (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === "slash_command") {
// iOS schickt: { type: "slash_command", text: "/fork abc123" }
await pi.prompt(msg.text);
}
});
```
**Aufwand:** 30 Minuten
---
## 7. Risiken & Offene Punkte
### R-1: Timing-Race-Condition bei State-Ableitung
**Problem:** `agent_end` + `ctx.isIdle()` könnte in Randfällen falsch sein (z.B. wenn zwischen Event-Emit und `isIdle()`-Check ein Follow-Up-Message queued wurde).
**Mitigation:** Upstream-Event `awaiting_input` (siehe 5.2) wäre sauberer. Für MVP akzeptabel, für Production ggf. nachbessern.
---
### R-2: Tree-Serialisierung bei großen Sessions
**Problem:** Bei sehr langen Sessions (> 1000 Entries) kann `getTree()` + Serialisierung langsam werden.
**Mitigation:**
- Lazy-Loading: iOS-App ruft nur Subtrees ab (via `ctx.sessionManager.getChildren(parentId)`).
- Caching: Sidecar cached serialisiertes Tree und invalidiert nur bei Tree-Events.
---
### R-3: Slash-Command-Hack (`(pi as any)`) bricht bei Updates
**Problem:** Wenn wir Option A (Hack) für Gruppe T nutzen, kann das bei pi-Updates brechen.
**Mitigation:**
- Tests einbauen die warnen wenn der Hack bricht.
- Upstream-Feature-Request parallel verfolgen.
- Für MVP Option B (Re-Implement) bevorzugen.
---
### R-4: ExtensionAPI-Version-Lock
**Problem:** Wir auditieren gegen pi v0.74.0. Künftige Versionen könnten API-Changes haben.
**Mitigation:**
- In `package.json` der Extension `peerDependencies` festlegen: `"@earendil-works/pi-coding-agent": "^0.74.0"`.
- Bei pi-Updates: Re-Audit durchführen.
---
## 8. Fazit & Next Steps
**Für MVP (Phase 0):**
- ✅ S-07, S-08, S-13 sind **implementierbar ohne Upstream-Changes**
- ⚠️ Gruppe T (Tree-Navigation) **nicht empfohlen für MVP** (Hack fragil, Re-Implement aufwändig)
-**Empfehlung:** MVP ohne Gruppe T. Tree-View als read-only (S-13), Navigation bleibt über SSH-Terminal.
**Für v2 (nach Upstream-Discussion):**
- Feature-Request für `pi.prompt()` stellen (siehe 5.1)
- Optional: `awaiting_input` Event (siehe 5.2)
- Gruppe T dann sauber implementieren
**Nächste Schritte:**
1. Spec v3 schreiben mit finalen Feature-Entscheidungen
2. Phase 0 (Stream-PoC) starten — unabhängig von diesem Audit
3. Phase 1 (Sidecar mit S-07, S-08, S-13) parallel entwickeln
4. iOS-App-Entwicklung kann starten sobald Phase 1 läuft
---
**Audit abgeschlossen.**