Compare commits
5 Commits
94b4dc7a41
...
f6cbf17078
| Author | SHA1 | Date |
|---|---|---|
|
|
f6cbf17078 | |
|
|
0f946d56ea | |
|
|
cf61b2ba1b | |
|
|
36938a66c4 | |
|
|
7c0e94cb57 |
|
|
@ -0,0 +1,128 @@
|
||||||
|
# Phase 0 — Spike: tmux Stream PoC
|
||||||
|
|
||||||
|
> **Status:** ready to start.
|
||||||
|
> **Owner:** single agent, end-to-end (too small to parallelise).
|
||||||
|
> **Branch:** `feat/spike-stream`.
|
||||||
|
> **Estimated effort:** ~1 day.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Verify the foundational assumption of the entire spec: that we can run `pi`
|
||||||
|
inside `tmux`, tee the pane output via `pipe-pane`, push it as a binary
|
||||||
|
WebSocket stream, and consume it from a client without rendering artefacts
|
||||||
|
or unacceptable latency.
|
||||||
|
|
||||||
|
Output is a decision: green light for Phase 1, or list of blockers that need
|
||||||
|
spec revision.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- A new branch `feat/spike-stream` in `pi-remote-control`.
|
||||||
|
- A CLI invocation (e.g. `pi-remote spike`) that:
|
||||||
|
- Spawns a tmux session `pi-spike` running `pi`.
|
||||||
|
- Pipes the pane via `pipe-pane` to a WS endpoint on `ws://127.0.0.1:7799/spike`.
|
||||||
|
- Attaches the local terminal to the same tmux session.
|
||||||
|
- A test client (raw `wscat` script or a tiny throwaway HTML page) that
|
||||||
|
connects, dumps incoming binary frames to stdout (or a hex viewer) and
|
||||||
|
optionally re-renders them via `xterm.js`.
|
||||||
|
- A written PoC report `docs/reference/PHASE-0-report.md` answering:
|
||||||
|
- **R-1.** Does pi run cleanly inside tmux? (Ink redraws OK, no escape
|
||||||
|
sequence loss, no crashes during 10min uptime.)
|
||||||
|
- **R-2.** Does alternate-screen-buffer (`\e[?1049h`) work? Is the stream
|
||||||
|
parseable on the other side?
|
||||||
|
- **R-3.** Is per-chunk latency acceptable (< 50ms localhost,
|
||||||
|
< 200ms WAN)?
|
||||||
|
- **R-4.** Does the SSH session attached to the same tmux pane stay in
|
||||||
|
sync with the WS stream byte-for-byte?
|
||||||
|
- **R-5.** Edge cases observed (mouse mode, title sequences, very wide
|
||||||
|
output, etc.).
|
||||||
|
|
||||||
|
## Out of Scope for Spike-0
|
||||||
|
|
||||||
|
- No authentication, no TLS — bind to 127.0.0.1 only.
|
||||||
|
- No reconnect, sequence numbers, snapshot or buffer.
|
||||||
|
- No `send-keys` direction (read-only stream is enough to verify rendering).
|
||||||
|
- No multi-session — one fixed `pi-spike` session.
|
||||||
|
- No iOS code.
|
||||||
|
|
||||||
|
## Task Breakdown
|
||||||
|
|
||||||
|
### T-0.1 — Branch + skeleton
|
||||||
|
Create `feat/spike-stream`. Add a new file
|
||||||
|
`extensions/remote-control/spike.ts` and a CLI entry (a new flag
|
||||||
|
`--spike` on the existing extension or a separate npm-script —
|
||||||
|
whichever is faster).
|
||||||
|
|
||||||
|
### T-0.2 — tmux helper
|
||||||
|
Spawn tmux session, attach pipe-pane to a Unix FIFO or a pseudo-stream we
|
||||||
|
can read from Node. Reference command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tmux new-session -d -s pi-spike 'pi'
|
||||||
|
mkfifo /tmp/pi-spike.fifo
|
||||||
|
tmux pipe-pane -t pi-spike -o "cat > /tmp/pi-spike.fifo"
|
||||||
|
```
|
||||||
|
|
||||||
|
Node opens the FIFO read-side (`fs.createReadStream`) and exposes the byte
|
||||||
|
stream.
|
||||||
|
|
||||||
|
### T-0.3 — WS server
|
||||||
|
Stand up a minimal `ws` server on port 7799, route `/spike`, send the FIFO
|
||||||
|
bytes as binary frames. No backpressure handling, no permessage-deflate yet.
|
||||||
|
|
||||||
|
### T-0.4 — Test client
|
||||||
|
Two options, pick whichever is faster:
|
||||||
|
- **(a)** `wscat -b ws://127.0.0.1:7799/spike` and pipe through `od -c` for
|
||||||
|
raw inspection.
|
||||||
|
- **(b)** A 50-line HTML page with `xterm.js`, plain WebSocket, no styling.
|
||||||
|
|
||||||
|
### T-0.5 — Attach + dual-render test
|
||||||
|
Open a second terminal, run `tmux attach -t pi-spike`. Type into pi. Verify
|
||||||
|
that what you see in the SSH attach is identical to what arrives on the WS
|
||||||
|
client.
|
||||||
|
|
||||||
|
### T-0.6 — Stress / edge cases
|
||||||
|
Briefly try:
|
||||||
|
- Resize the SSH terminal — see how tmux/pi react.
|
||||||
|
- Run a slash command that opens a full-screen menu (alternate screen).
|
||||||
|
- Paste a multi-line block.
|
||||||
|
- Let pi do a long tool call.
|
||||||
|
|
||||||
|
### T-0.7 — Report
|
||||||
|
Write `docs/reference/PHASE-0-report.md`. One paragraph per R-question,
|
||||||
|
plus a "go / no-go for Phase 1" verdict.
|
||||||
|
|
||||||
|
## File Plan
|
||||||
|
|
||||||
|
- New: `extensions/remote-control/spike.ts`
|
||||||
|
- New: `docs/reference/PHASE-0-report.md`
|
||||||
|
- Modified: `extensions/remote-control/index.ts` (add `--spike` flag or
|
||||||
|
separate entry).
|
||||||
|
- No changes to existing server.ts / html.ts / messages.ts.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `tmux` installed on the dev host. macOS: already present or
|
||||||
|
`brew install tmux`.
|
||||||
|
- `ws` library: already in `package.json`.
|
||||||
|
- `mkfifo` shell command (POSIX): already present on macOS/Linux.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- **R-A.** Ink may refuse to run inside tmux due to TTY detection. If so,
|
||||||
|
set `FORCE_COLOR=1`, `TERM=xterm-256color`, pass `-tt` to tmux. Fall back
|
||||||
|
to spawning pi via `unbuffer` if necessary.
|
||||||
|
- **R-B.** FIFO can have buffering issues. If line buffering causes
|
||||||
|
visible lag, switch to a Unix domain socket and have Node read directly
|
||||||
|
from the socket.
|
||||||
|
- **R-C.** tmux's `pipe-pane` reproduces ANSI but may drop sequences during
|
||||||
|
bursts. If lossy, the alternative is to run pi inside our own `node-pty`
|
||||||
|
(a much larger change, but a fallback option).
|
||||||
|
|
||||||
|
## Exit / Handover
|
||||||
|
|
||||||
|
When Phase 0 closes:
|
||||||
|
- Merge `feat/spike-stream` into `main` only if PoC code is reusable for
|
||||||
|
Phase 1; otherwise close the branch and keep the report.
|
||||||
|
- Update `SYNC.md` with the verdict and any spec revisions needed.
|
||||||
|
- Trigger Phase 1.
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
# Phase 1 — Sidecar Production-Ready
|
||||||
|
|
||||||
|
> **Status:** blocked on Phase 0 verdict.
|
||||||
|
> **Owners:** parallelisable across multiple agents — see task table.
|
||||||
|
> **Branch base:** `main` after Phase 0 merge. Feature branches per work
|
||||||
|
> stream (see `SYNC.md`).
|
||||||
|
> **Spec reference:** [`reference/SPEC-ios-app.md`](./reference/SPEC-ios-app.md) §4.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
The `pi-remote-control` extension is extended into a full sidecar that can
|
||||||
|
serve the iOS app. End state: a single Node process, started alongside pi
|
||||||
|
(or as a system service), that exposes a WebSocket API for:
|
||||||
|
|
||||||
|
- Stream attach/detach with reconnect.
|
||||||
|
- Send-keys input.
|
||||||
|
- Multi-session lifecycle (spawn, list, rename, kill).
|
||||||
|
- Snapshot, disk-buffered replay.
|
||||||
|
- State, slash-command-registry side-channel.
|
||||||
|
- QR-based pairing, bearer-token auth, self-signed TLS with pinning.
|
||||||
|
- Health endpoint.
|
||||||
|
|
||||||
|
After Phase 1 we can drive everything from `wscat` or a small Web UI.
|
||||||
|
The iOS app is **not** required to validate Phase 1.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
For each S-feature listed below: implemented, manually exercised, basic
|
||||||
|
test (smoke test minimum). Plus:
|
||||||
|
|
||||||
|
- `pi-remote pair` prints a working QR.
|
||||||
|
- Two parallel sessions can be spawned, switched between, and one can be
|
||||||
|
killed without disturbing the other.
|
||||||
|
- WebSocket-level integration smoke test: a script that opens a stream,
|
||||||
|
sends keys, receives output, drops the connection, reconnects with
|
||||||
|
`lastSeq`, observes a clean delta.
|
||||||
|
- `wss://` works against the self-signed cert; the fingerprint matches the
|
||||||
|
QR contents.
|
||||||
|
- Sidecar survives restart and reattaches to all existing tmux sessions
|
||||||
|
without losing state.
|
||||||
|
|
||||||
|
## Architecture Sketch
|
||||||
|
|
||||||
|
```
|
||||||
|
extensions/remote-control/
|
||||||
|
├── index.ts — extension entry point (existing, extended)
|
||||||
|
├── server/ — NEW: HTTP/WS server, split into route modules
|
||||||
|
│ ├── server.ts — bootstrap, TLS, middleware
|
||||||
|
│ ├── routes/
|
||||||
|
│ │ ├── stream.ts — S-02 binary stream + S-04 sequence + S-05 snapshot
|
||||||
|
│ │ ├── input.ts — S-03 send-keys
|
||||||
|
│ │ ├── sessions.ts — S-09 multi-session CRUD
|
||||||
|
│ │ ├── commands.ts — S-08 slash-command registry
|
||||||
|
│ │ ├── side.ts — S-07 state side-channel
|
||||||
|
│ │ └── health.ts — S-12 health
|
||||||
|
│ └── upgrade.ts — WS upgrade routing per session/topic
|
||||||
|
├── tmux/ — NEW: tmux wrapper
|
||||||
|
│ ├── manager.ts — spawn/list/kill, metadata via @options
|
||||||
|
│ ├── pipe.ts — pipe-pane, FIFO read, byte streaming
|
||||||
|
│ ├── input.ts — send-keys translation
|
||||||
|
│ └── snapshot.ts — capture-pane wrapper
|
||||||
|
├── buffer/ — NEW: disk ringbuffer per session
|
||||||
|
│ ├── writer.ts — append, cap enforcement, watchdog
|
||||||
|
│ └── reader.ts — range read for snapshot fallback
|
||||||
|
├── sequence.ts — NEW: monotonic chunk numbering shared by stream + buffer
|
||||||
|
├── auth/ — auth/pairing module
|
||||||
|
│ ├── tokens.ts — bearer-token CRUD (extends existing auth.ts)
|
||||||
|
│ ├── pairing.ts — pi-remote pair, QR rendering, exchange
|
||||||
|
│ └── tls.ts — self-signed cert generation + fingerprint
|
||||||
|
├── pi/ — adapter to pi ExtensionAPI
|
||||||
|
│ ├── events.ts — subscribe agent_start/end, tool_*, session_*
|
||||||
|
│ ├── commands.ts — pi.getCommands() wrapper
|
||||||
|
│ └── autoname.ts — S-09a, spawn pi -p subprocess
|
||||||
|
└── cli/ — CLI entrypoints (pi-remote attach/pair/auth/health)
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
`html.ts`, `messages.ts`, the existing `server.ts` and `config.ts` remain
|
||||||
|
for the legacy HTML client during the transition; they are tagged as
|
||||||
|
*legacy* in code comments. They will be retired after Phase 2 ships.
|
||||||
|
|
||||||
|
## Task Breakdown
|
||||||
|
|
||||||
|
Tasks are numbered `T-1.<n>`. The "Parallel With" column shows which other
|
||||||
|
tasks can be in flight simultaneously without merge pain. The "Touches"
|
||||||
|
column lists the files an agent may modify.
|
||||||
|
|
||||||
|
| ID | Task | Touches | Depends on | Parallel With |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| T-1.0 | **Server refactor scaffold.** Carve `server.ts` into the `server/` and route modules above; existing HTML behaviour must still work; CI green. | `extensions/remote-control/server/**`, minimal edit of `index.ts` | — | none — must land first |
|
||||||
|
| T-1.1 | **tmux/manager + tmux/pipe + tmux/snapshot.** Spawn, list, kill, metadata via `@description`. Pipe-pane FIFO reader. Snapshot via `capture-pane`. | `tmux/**` | T-1.0 | T-1.2, T-1.3, T-1.4, T-1.5, T-1.6 |
|
||||||
|
| T-1.2 | **Sequence module + buffer/writer + buffer/reader.** Monotone chunk IDs, disk ringbuffer with caps (100MB/session, 1GB global, free-space watchdog), idle-cleanup. | `sequence.ts`, `buffer/**` | T-1.0 | T-1.1, T-1.3, T-1.4, T-1.5, T-1.6 |
|
||||||
|
| T-1.3 | **Auth: tokens + pairing + TLS.** Self-signed cert generation, fingerprint, bearer-token CRUD, `pi-remote pair` CLI + QR rendering, `pi-remote auth list/revoke/name`. | `auth/**`, `cli/index.ts` (subcommands only) | T-1.0 | T-1.1, T-1.2, T-1.4, T-1.5, T-1.6 |
|
||||||
|
| T-1.4 | **pi adapter.** Subscribe ExtensionAPI events, expose `getCommands`, implement `autoname.ts` spawning `pi -p`. | `pi/**`, edits in `index.ts` to wire subscriptions | T-1.0 | T-1.1, T-1.2, T-1.3, T-1.5, T-1.6 |
|
||||||
|
| T-1.5 | **Stream + input + snapshot routes (S-02/S-03/S-04/S-05).** WS upgrade routing, binary stream, sequence cursor resume, send-keys with bracketed-paste. | `server/routes/stream.ts`, `server/routes/input.ts`, `server/upgrade.ts` | T-1.0, T-1.1, T-1.2 | T-1.6, T-1.7 |
|
||||||
|
| T-1.6 | **Side-channel + commands + sessions routes (S-07/S-08/S-09).** | `server/routes/side.ts`, `server/routes/commands.ts`, `server/routes/sessions.ts` | T-1.0, T-1.1, T-1.4 | T-1.5, T-1.7 |
|
||||||
|
| T-1.7 | **Health endpoint + config + watchdog (S-12).** Disk watchdog ties buffer caps to global state. | `server/routes/health.ts`, new `config.toml` schema in `config.ts` | T-1.0, T-1.2 | T-1.5, T-1.6 |
|
||||||
|
| T-1.8 | **Integration smoke harness.** Node script under `scripts/smoke/` that spawns a sidecar, opens a stream, sends keys, drops + reconnects, verifies delta. | `scripts/smoke/**` | T-1.5, T-1.6 | none |
|
||||||
|
| T-1.9 | **Docs: operator guide.** README section "Running pi-remote as a sidecar", config sample, troubleshooting. | `README.md`, optionally `docs/reference/OPERATOR.md` | T-1.5, T-1.6, T-1.7 | parallel with T-1.8 |
|
||||||
|
| T-1.10 | **APNs scaffold (deferred but cheap).** `apns/` module: config schema, JWT generation, push primitive. Stub the device-token registry — flesh out in Phase 2 when iOS app provides tokens. | `apns/**`, edits in `auth/tokens.ts` to store device-tokens | T-1.3 | T-1.5..T-1.7 |
|
||||||
|
|
||||||
|
## Interface Contracts (lock early to enable parallelism)
|
||||||
|
|
||||||
|
These are the contracts that downstream tasks depend on. They must be
|
||||||
|
agreed and frozen at the start of Phase 1 — see `SYNC.md` for the freeze
|
||||||
|
protocol.
|
||||||
|
|
||||||
|
### IC-1 — WebSocket frames
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// binary frame : raw ANSI stream bytes (output direction only).
|
||||||
|
// text frame : JSON, type-discriminated.
|
||||||
|
|
||||||
|
type ClientToServer =
|
||||||
|
| { type: "resume"; lastSeq: number | null }
|
||||||
|
| { type: "key"; name: string } // "escape" | "tab" | "up" | "down" | "left" | "right" | "enter" | "shift-enter"
|
||||||
|
| { type: "keys"; data: string } // literal text, sent via send-keys -l
|
||||||
|
| { type: "paste"; data: string } // wrapped in bracketed-paste
|
||||||
|
| { type: "snapshot-request" };
|
||||||
|
|
||||||
|
type ServerToClient =
|
||||||
|
| { type: "state"; value: "thinking" | "tool" | "idle" | "awaiting-input"; tool?: string; ts: number }
|
||||||
|
| { type: "tree"; nodes: TreeNode[]; current: string } // optional, read-only
|
||||||
|
| { type: "snapshot"; seq: number; data: string } // base64 ANSI snapshot
|
||||||
|
| { type: "session-meta"; name: string; description?: string; createdAt: string }
|
||||||
|
| { type: "error"; code: string; message: string };
|
||||||
|
```
|
||||||
|
|
||||||
|
Binary frames carry an out-of-band `seq` via a leading 8-byte
|
||||||
|
big-endian header. Owner: T-1.5.
|
||||||
|
|
||||||
|
### IC-2 — HTTP REST shape
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /health → { ok, sessions, bufferBytes, ... }
|
||||||
|
POST /sessions → { id, name }
|
||||||
|
GET /sessions → [{ id, name, description, state, lastOutputAt }, …]
|
||||||
|
PATCH /sessions/:id → updates @description
|
||||||
|
DELETE /sessions/:id → kills tmux session, optionally clears buffer
|
||||||
|
GET /sessions/:id/commands → [{ name, description, args }]
|
||||||
|
GET /sessions/:id/thumbnail → text/plain capture-pane (40×12)
|
||||||
|
```
|
||||||
|
|
||||||
|
All endpoints behind bearer token, all responses `application/json` unless
|
||||||
|
noted. Owner: T-1.5..T-1.7.
|
||||||
|
|
||||||
|
### IC-3 — Pairing payload
|
||||||
|
|
||||||
|
QR encodes a `pi-remote://` URL:
|
||||||
|
|
||||||
|
```
|
||||||
|
pi-remote://<host>:<port>?pair=<pairing-token>&fp=<sha256-hex>&name=<sidecar-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Pairing exchange: client `POST /pair` with `{ pairingToken, deviceToken?, environment?, deviceName? }` → server replies `{ bearerToken, sidecarId }`. Owner: T-1.3.
|
||||||
|
|
||||||
|
### IC-4 — Config schema (TOML)
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[server]
|
||||||
|
host = "0.0.0.0"
|
||||||
|
port = 7777
|
||||||
|
state_dir = "~/.local/share/pi-remote"
|
||||||
|
|
||||||
|
[buffer]
|
||||||
|
per_session_mb = 100
|
||||||
|
global_gb = 1
|
||||||
|
free_min_gb = 1
|
||||||
|
idle_days = 30
|
||||||
|
|
||||||
|
[tmux]
|
||||||
|
default_width = 120
|
||||||
|
default_height = 40
|
||||||
|
|
||||||
|
[apns]
|
||||||
|
team_id = "..."
|
||||||
|
key_id = "..."
|
||||||
|
key_path = "..."
|
||||||
|
bundle_id = "..."
|
||||||
|
|
||||||
|
[autoname]
|
||||||
|
enabled = true
|
||||||
|
trigger_after = 3 # user messages
|
||||||
|
model = "claude-haiku-4-5"
|
||||||
|
```
|
||||||
|
|
||||||
|
Owner: T-1.7.
|
||||||
|
|
||||||
|
## Branching Strategy
|
||||||
|
|
||||||
|
- Each task is a feature branch off `main`, named `feat/p1-<task-id>-<slug>`,
|
||||||
|
e.g. `feat/p1-t1-1-tmux-manager`.
|
||||||
|
- Open a PR as soon as a task is ready for review. Squash-merge.
|
||||||
|
- T-1.0 (refactor) lands first, then T-1.1..T-1.4 can run truly parallel.
|
||||||
|
- T-1.5..T-1.7 each consume one or more of the lower-layer modules; they
|
||||||
|
start as soon as the dependency PR is in `main`.
|
||||||
|
|
||||||
|
## Test Strategy
|
||||||
|
|
||||||
|
- Unit: per-module pure-logic tests under `extensions/remote-control/**/__tests__/`.
|
||||||
|
- Integration smoke: T-1.8 script, runnable locally and in CI.
|
||||||
|
- Manual: each task PR lists manual-verification steps.
|
||||||
|
- No iOS testing in this phase.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- **R1.** Disk-buffer cap math races vs. global watchdog. Mitigation:
|
||||||
|
serialise buffer writes through a single async queue per session, lock
|
||||||
|
the global cap behind a mutex.
|
||||||
|
- **R2.** ExtensionAPI event names might shift in future pi versions.
|
||||||
|
Mitigation: pin pi version range in `package.json`, isolate adapter in
|
||||||
|
`pi/events.ts`.
|
||||||
|
- **R3.** `pi -p` auto-name calls cost money. Mitigation: gate behind
|
||||||
|
`[autoname] enabled`, debounce, skip if user already named the session.
|
||||||
|
|
||||||
|
## Exit / Handover
|
||||||
|
|
||||||
|
- All T-1.x merged.
|
||||||
|
- Smoke harness passes locally and in CI.
|
||||||
|
- Operator guide complete.
|
||||||
|
- A short `docs/reference/PHASE-1-report.md` summarising deviations from
|
||||||
|
the plan, especially anything that affects Phase 2 contracts.
|
||||||
|
- Update `SYNC.md` to unblock Phase 2.
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
# Phase 2 — iOS App MVP
|
||||||
|
|
||||||
|
> **Status:** blocked on Phase 1 (sidecar must be reachable).
|
||||||
|
> **Owners:** parallelisable; see task table.
|
||||||
|
> **Repo:** new repository `pi-remote-ios` adjacent to `pi-remote-control`,
|
||||||
|
> at `git.vpsj.de/jay/pi-remote-ios`. Reason: Swift project, separate
|
||||||
|
> tooling, separate release cadence.
|
||||||
|
> **Spec reference:** [`reference/SPEC-ios-app.md`](./reference/SPEC-ios-app.md) §5
|
||||||
|
> Groups A, B (sans hardware-keyboard), C-01, C-02, D-01 + a/b, E, F.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
A SwiftUI iOS app that:
|
||||||
|
|
||||||
|
- Pairs with a sidecar via QR scan.
|
||||||
|
- Renders a single pi session 1:1 via SwiftTerm.
|
||||||
|
- Sends keystrokes back via the IC-1 protocol.
|
||||||
|
- Survives backgrounding and reconnect within the < 1s P-3 target.
|
||||||
|
- Switches between multiple sessions with pre-connect cache.
|
||||||
|
- Receives push notifications when pi reaches `awaiting-input`.
|
||||||
|
|
||||||
|
After Phase 2 the app is usable in the user's daily workflow, replacing
|
||||||
|
the legacy HTML client. Augmentations (slash palette, voice, themes,
|
||||||
|
search, …) come in Phase 3.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Apple Developer enrolment complete, App ID with Push capability + APNs
|
||||||
|
Auth Key (`.p8`) generated.
|
||||||
|
- App builds and runs on the user's iPhone via Xcode (sandbox APNs).
|
||||||
|
- App pairs via QR, persists bearer token + cert pinning across launches.
|
||||||
|
- Foreground rendering: SwiftTerm shows pi 1:1, input round-trips.
|
||||||
|
- Background → foreground: < 1s to live stream, no visible empty screen.
|
||||||
|
- Three named sessions, switcher works, pre-connect makes switching
|
||||||
|
feel instant.
|
||||||
|
- Push notification fires when pi state transitions to `awaiting-input`
|
||||||
|
while app is backgrounded.
|
||||||
|
- TestFlight build distributable (production APNs route exercised).
|
||||||
|
- Face-ID gate available as opt-in setting.
|
||||||
|
|
||||||
|
## Project Layout
|
||||||
|
|
||||||
|
New repo `pi-remote-ios`:
|
||||||
|
|
||||||
|
```
|
||||||
|
pi-remote-ios/
|
||||||
|
├── README.md
|
||||||
|
├── Package.swift — SwiftPM (deps: SwiftTerm, Starscream)
|
||||||
|
├── Apps/
|
||||||
|
│ └── piRemote/ — main app target
|
||||||
|
│ ├── piRemoteApp.swift — @main entry
|
||||||
|
│ ├── Resources/
|
||||||
|
│ │ ├── Themes/ — bundled .json theme files
|
||||||
|
│ │ ├── Fonts/ — JetBrains Mono, Hack, etc.
|
||||||
|
│ │ └── Assets.xcassets
|
||||||
|
│ └── Info.plist
|
||||||
|
├── Sources/
|
||||||
|
│ ├── Core/ — networking, state, persistence
|
||||||
|
│ │ ├── Network/
|
||||||
|
│ │ │ ├── WebSocketClient.swift — Starscream wrapper, permessage-deflate
|
||||||
|
│ │ │ ├── FrameCodec.swift — IC-1 encode/decode
|
||||||
|
│ │ │ ├── ResumeCursor.swift — lastSeq tracking per session
|
||||||
|
│ │ │ └── PinnedTrust.swift — TLS pinning from QR fingerprint
|
||||||
|
│ │ ├── Auth/
|
||||||
|
│ │ │ ├── Keychain.swift
|
||||||
|
│ │ │ └── Pairing.swift — QR parse, exchange
|
||||||
|
│ │ ├── Sessions/
|
||||||
|
│ │ │ ├── SessionRegistry.swift — list, spawn, kill (talks to /sessions)
|
||||||
|
│ │ │ ├── SessionConnection.swift — one WS per session
|
||||||
|
│ │ │ └── PreConnectPool.swift — D-01a strategy
|
||||||
|
│ │ ├── Push/
|
||||||
|
│ │ │ ├── NotificationDelegate.swift
|
||||||
|
│ │ │ └── DeviceTokenRegistrar.swift — sends token + env to sidecar
|
||||||
|
│ │ └── Persistence/
|
||||||
|
│ │ ├── ScrollbackCache.swift — rolling 5MB per session, on disk
|
||||||
|
│ │ └── Preferences.swift
|
||||||
|
│ ├── UI/
|
||||||
|
│ │ ├── Terminal/
|
||||||
|
│ │ │ ├── TerminalView.swift — UIViewRepresentable wrapping SwiftTerm
|
||||||
|
│ │ │ ├── ThemeStore.swift — bundled themes, currently selected
|
||||||
|
│ │ │ └── FontStore.swift
|
||||||
|
│ │ ├── Input/
|
||||||
|
│ │ │ ├── ModifierBar.swift — [Ctrl][Esc][Tab][←↑↓→][⇧↵][🎙][📋]
|
||||||
|
│ │ │ ├── ModifierState.swift — sticky Ctrl + repeat handling
|
||||||
|
│ │ │ └── PasteSheet.swift — confirm-before-paste
|
||||||
|
│ │ ├── Status/
|
||||||
|
│ │ │ └── StatusBar.swift — connection + pi state
|
||||||
|
│ │ ├── Sessions/
|
||||||
|
│ │ │ ├── SessionSwitcher.swift — list, spawn, switch
|
||||||
|
│ │ │ └── SessionRow.swift — name + state badge (no thumbnail in MVP)
|
||||||
|
│ │ ├── Pairing/
|
||||||
|
│ │ │ ├── QRScannerView.swift
|
||||||
|
│ │ │ └── PairingFlowView.swift
|
||||||
|
│ │ └── Settings/
|
||||||
|
│ │ └── SettingsView.swift — Face-ID toggle, sidecar info
|
||||||
|
│ └── Voice/ — empty placeholder, populated in Phase 3
|
||||||
|
├── Tests/
|
||||||
|
│ └── CoreTests/
|
||||||
|
│ ├── FrameCodecTests.swift
|
||||||
|
│ └── ResumeCursorTests.swift
|
||||||
|
└── docs/
|
||||||
|
├── BUILD.md
|
||||||
|
└── DISTRIBUTION.md — TestFlight steps
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task Breakdown
|
||||||
|
|
||||||
|
| ID | Task | Touches | Depends on | Parallel With |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| T-2.0 | **Repo + Xcode project scaffold + Apple Developer setup.** Create repo on git.vpsj.de, generate App ID + APNs Auth Key, commit `.p8` instructions (key itself stays out of git). Empty SwiftUI shell that boots and shows "Hello pi". | repo root, `Apps/piRemote/` | Phase 1 sidecar reachable | none — must land first |
|
||||||
|
| T-2.1 | **WebSocketClient + FrameCodec.** Starscream, permessage-deflate enabled, encode/decode IC-1 frames, basic ping/pong keepalive. Unit-tested. | `Sources/Core/Network/` | T-2.0 | T-2.2, T-2.3, T-2.4 |
|
||||||
|
| T-2.2 | **Pairing flow + Keychain + TLS pinning.** QR scanner (AVFoundation), parse `pi-remote://`, exchange via Pairing.swift, store bearer + fingerprint in Keychain, install PinnedTrust into URLSession + Starscream. | `Sources/Core/Auth/`, `Sources/Core/Network/PinnedTrust.swift`, `Sources/UI/Pairing/` | T-2.0 | T-2.1, T-2.3 |
|
||||||
|
| T-2.3 | **TerminalView + Theme/Font store.** Wrap SwiftTerm as UIViewRepresentable, render incoming binary chunks, expose Pinch-Zoom gesture (iOS-B-05), Selection/Copy (iOS-B-04). Bundle JetBrains Mono + Hack + default themes. | `Sources/UI/Terminal/`, `Apps/piRemote/Resources/` | T-2.0 | T-2.1, T-2.2, T-2.4 |
|
||||||
|
| T-2.4 | **ModifierBar + Input pipeline.** Layout `[Ctrl][Esc][Tab][←↑↓→][⇧↵][🎙][📋]`, sticky Ctrl, long-press repeat, paste sheet stub (full Smart-Paste in Phase 3). Wires keys into IC-1. | `Sources/UI/Input/` | T-2.1 | T-2.2, T-2.3 |
|
||||||
|
| T-2.5 | **SessionConnection + ResumeCursor + ScrollbackCache.** One WS per session, persist lastSeq, write incoming bytes into a rolling on-disk file per session. Snapshot fallback on gap. | `Sources/Core/Sessions/SessionConnection.swift`, `Sources/Core/Network/ResumeCursor.swift`, `Sources/Core/Persistence/ScrollbackCache.swift` | T-2.1 | T-2.6, T-2.7 |
|
||||||
|
| T-2.6 | **SessionRegistry + SessionSwitcher UI.** Talks to `/sessions`, list/spawn/rename/kill, switcher UI, basic SessionRow. No thumbnails or pre-connect yet. | `Sources/Core/Sessions/SessionRegistry.swift`, `Sources/UI/Sessions/` | T-2.1, T-2.5 | T-2.7 |
|
||||||
|
| T-2.7 | **PreConnectPool + Optimistic Switch + Stale-Frame.** All known sessions hold a hot WS + last frame; switching shows the cached frame instantly with a "syncing…" pill. | `Sources/Core/Sessions/PreConnectPool.swift`, `Sources/UI/Terminal/TerminalView.swift` (cache hooks) | T-2.5, T-2.6 | T-2.8 |
|
||||||
|
| T-2.8 | **StatusBar + side-channel consumption.** Subscribe to `state` frames, render `● thinking` / `▶ awaiting` / `⏸ idle`, session-name display. | `Sources/UI/Status/`, `Sources/Core/Sessions/SessionConnection.swift` (event surface) | T-2.5 | T-2.7 |
|
||||||
|
| T-2.9 | **Push: NotificationDelegate + DeviceTokenRegistrar.** Request user permission, register for remote notifications, ship `{ deviceToken, environment }` to sidecar at pair-time and on every launch. Foreground-handler suppresses banners when relevant session is visible. | `Sources/Core/Push/`, edits in pairing/Settings flow | T-2.2, Phase 1 T-1.10 | T-2.8 |
|
||||||
|
| T-2.10 | **Background lifecycle.** App-foreground triggers reconnect + delta pull, stale-frame freezes during sync, keep-alive ping in foreground only. | `Sources/Core/Sessions/SessionConnection.swift`, app delegate | T-2.5, T-2.7 | parallel with T-2.9 |
|
||||||
|
| T-2.11 | **Face-ID gate + Settings.** Opt-in toggle, gate appears on cold launch and on resume after > N seconds backgrounded. | `Sources/UI/Settings/`, `Sources/Core/Auth/Keychain.swift` | T-2.0 | parallel with most |
|
||||||
|
| T-2.12 | **TestFlight pipeline.** Build script, archive, upload, internal testers list. Verify production APNs path. | `docs/DISTRIBUTION.md`, Fastlane or shell scripts | T-2.0, T-2.9 | parallel with everything once T-2.0 is in |
|
||||||
|
| T-2.13 | **MVP smoke test.** Manual checklist run on the user's iPhone: pair → render → input → backgrounded → push → reopen < 1s → session-switch round-trip. Document any deviations. | `docs/PHASE-2-report.md` | all above | none |
|
||||||
|
|
||||||
|
## Interface Contracts
|
||||||
|
|
||||||
|
iOS consumes the IC-1..IC-4 contracts defined in Phase 1. Any deviation
|
||||||
|
discovered while building is fixed in the sidecar, not the app, and must
|
||||||
|
be communicated via `SYNC.md` (lock change protocol).
|
||||||
|
|
||||||
|
Additional iOS-internal contract:
|
||||||
|
|
||||||
|
### IC-2.1 — SessionConnection surface
|
||||||
|
|
||||||
|
```swift
|
||||||
|
protocol SessionConnection {
|
||||||
|
var id: String { get }
|
||||||
|
var state: AnyPublisher<PiState, Never> { get }
|
||||||
|
var stream: AnyPublisher<Data, Never> { get } // ANSI bytes, in order
|
||||||
|
func send(_ frame: ClientToServer) async throws
|
||||||
|
func resume(from lastSeq: UInt64?) async throws
|
||||||
|
func suspend() async // tear down WS but keep state
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Owners of `SessionConnection`: T-2.5. Consumers: T-2.6, T-2.7, T-2.8, T-2.10.
|
||||||
|
|
||||||
|
## Branching Strategy
|
||||||
|
|
||||||
|
- All work on `pi-remote-ios`, off `main`.
|
||||||
|
- One branch per T-2.x task, `feat/p2-<task-id>-<slug>`.
|
||||||
|
- T-2.0 must land first.
|
||||||
|
- T-2.1, T-2.2, T-2.3, T-2.4, T-2.11, T-2.12 can start in parallel right
|
||||||
|
after T-2.0.
|
||||||
|
- T-2.5..T-2.10 form a dependency cluster but most can interleave.
|
||||||
|
- T-2.13 last.
|
||||||
|
|
||||||
|
## Test Strategy
|
||||||
|
|
||||||
|
- Unit tests for FrameCodec, ResumeCursor, theme parsing.
|
||||||
|
- UI snapshot tests for ModifierBar, SessionRow, StatusBar.
|
||||||
|
- Manual on-device testing via T-2.13 checklist.
|
||||||
|
- No XCUITest in MVP — too brittle for the time invested.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- **R1.** Apple Developer enrolment delays. Workaround: dev with personal
|
||||||
|
team + free sideloading for the first 1-2 weeks; switch to paid account
|
||||||
|
before T-2.12.
|
||||||
|
- **R2.** Starscream's permessage-deflate compat with our `ws` library
|
||||||
|
needs verification with a smoke test early — block T-2.1 PR until
|
||||||
|
proven.
|
||||||
|
- **R3.** SwiftTerm's alternate-screen handling vs. our scrollback cache.
|
||||||
|
Cache must skip bytes while alternate-screen is active. Spec calls for
|
||||||
|
this; implementation needs care.
|
||||||
|
- **R4.** Push notification permission UX. If user declines, iOS-C-02
|
||||||
|
degrades to silent. Provide a Settings deep-link to re-enable.
|
||||||
|
|
||||||
|
## Exit / Handover
|
||||||
|
|
||||||
|
- All T-2.x merged.
|
||||||
|
- T-2.13 report green.
|
||||||
|
- App on user's iPhone in daily use.
|
||||||
|
- `docs/PHASE-2-report.md` in this repo, summary mirrored into `SYNC.md`.
|
||||||
|
- Trigger Phase 3.
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
# Phase 3 — iOS Augmentation
|
||||||
|
|
||||||
|
> **Status:** blocked on Phase 2 MVP shipping.
|
||||||
|
> **Owners:** highly parallelisable — features are largely independent.
|
||||||
|
> **Repo:** `pi-remote-ios`.
|
||||||
|
> **Spec reference:** [`reference/SPEC-ios-app.md`](./reference/SPEC-ios-app.md) §5
|
||||||
|
> Groups B-06, B-07, B-08, B-09, C-03, C-04, C-05, D-01c, D-02, A-05 extensions.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make the iOS app distinctly nicer to use than a generic terminal client.
|
||||||
|
Each Phase 3 feature is independently shippable; no global blocker.
|
||||||
|
Features can land in any order, driven by daily use feedback.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
Per-feature checklist (each feature ships when its row passes):
|
||||||
|
|
||||||
|
| Feature | Acceptance |
|
||||||
|
|---|---|
|
||||||
|
| Slash-Command Palette | Long-press modifier bar opens palette, fuzzy search works, command injects correctly, argument forms render for commands with args. |
|
||||||
|
| Voice-to-Prompt | Mic button → preview → send works offline (iOS Speech). |
|
||||||
|
| Predictive Thumbnails | Switcher list shows live 40×12 capture-pane previews refreshed on open. |
|
||||||
|
| Scrollback Search | Cmd-F (HW kb) or pull-down gesture opens search; jump-to-match highlights and centres. |
|
||||||
|
| Hardware Keyboard Shortcuts | Cmd-K, Cmd-T, Cmd-1..9, Cmd-F, Cmd-Shift-P, Cmd-, route correctly. |
|
||||||
|
| Reachability | iPhone landscape: modifier bar mirrored for one-handed use. |
|
||||||
|
| Smart Paste (full) | Clipboard preview chip, multi-line preview sheet, bracketed-paste correctness. |
|
||||||
|
| Haptic Feedback | Subtle haptic on thinking→idle and thinking→awaiting transitions. |
|
||||||
|
| Theme + Font Picker UI | Settings UI exposes all bundled themes and fonts; iCloud-sync for custom. |
|
||||||
|
|
||||||
|
## Task Breakdown
|
||||||
|
|
||||||
|
| ID | Task | Touches | Depends on | Parallel With |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| T-3.1 | **Slash-Command Palette** (iOS-C-04). Long-press recogniser on ModifierBar, palette sheet, fuzzy-search engine, argument form generator from JSON schema returned by sidecar. | `Sources/UI/Input/SlashPalette/`, sidecar `/sessions/:id/commands` already exists (S-08) | Phase 2 | all others |
|
||||||
|
| T-3.2 | **Voice-to-Prompt** (iOS-C-05). `Sources/Voice/`, SFSpeechRecognizer, microphone permission, preview-edit-send flow. | `Sources/Voice/`, `Sources/UI/Input/ModifierBar.swift` (🎙 wiring) | Phase 2 | all others |
|
||||||
|
| T-3.3 | **Predictive Thumbnails** (iOS-D-01c). Add `GET /sessions/:id/thumbnail` on sidecar if not already (Phase 1 IC-2 includes it); poll on switcher open; render small SwiftTerm in `SessionRow`. | `Sources/UI/Sessions/SessionRow.swift`, `Sources/Core/Sessions/SessionRegistry.swift` | Phase 2 | all others |
|
||||||
|
| T-3.4 | **Scrollback Search** (iOS-D-02). Search bar over `ScrollbackCache`, in-memory index (linear search is fine at 5MB), highlight + jump in TerminalView. | `Sources/UI/Terminal/Search/`, `Sources/Core/Persistence/ScrollbackCache.swift` (read API) | Phase 2 | all others |
|
||||||
|
| T-3.5 | **Hardware Keyboard Shortcuts** (iOS-B-06). Register `UIKeyCommand` set in piRemoteApp + scene; route to app actions. Caps→Esc opt-in. | `Apps/piRemote/piRemoteApp.swift`, individual view controllers via scene delegate | Phase 2 | T-3.1 (Cmd-Shift-P depends on slash palette existing) |
|
||||||
|
| T-3.6 | **Reachability / One-Hand-Mode** (iOS-B-07). Landscape layout in `ModifierBar` mirrored; settings toggle. | `Sources/UI/Input/ModifierBar.swift` | Phase 2 | all others |
|
||||||
|
| T-3.7 | **Smart Paste full** (iOS-B-08 + iOS-B-09 bracketed-paste). Extend stub PasteSheet from Phase 2 with multi-line preview, char/line counter; track `\e[?2004h/l` from stream, switch paste-frame type accordingly. | `Sources/UI/Input/PasteSheet.swift`, `Sources/Core/Sessions/SessionConnection.swift` (state tracker) | Phase 2 | all others |
|
||||||
|
| T-3.8 | **Haptic Feedback** (iOS-C-03). `UIImpactFeedbackGenerator` hook in StatusBar state change. Setting to disable. | `Sources/UI/Status/StatusBar.swift`, Settings | Phase 2 | all others |
|
||||||
|
| T-3.9 | **Theme + Font Picker** (iOS-A-05 UI). Settings panes for theme/font selection, custom-theme editor (JSON or color pickers), iCloud KVS sync for custom. | `Sources/UI/Settings/`, `Sources/UI/Terminal/ThemeStore.swift` extensions | Phase 2 | all others |
|
||||||
|
|
||||||
|
## Inter-Task Conflicts
|
||||||
|
|
||||||
|
Most Phase 3 tasks touch unrelated files. Watch zones:
|
||||||
|
|
||||||
|
- **ModifierBar.swift** — T-3.1 (long-press), T-3.2 (mic), T-3.6 (mirror),
|
||||||
|
T-3.7 (paste). Coordinate via SYNC.md if more than one of these is in
|
||||||
|
flight simultaneously. Recommended order: T-3.1 → T-3.7 → T-3.6 → T-3.2.
|
||||||
|
- **SessionConnection.swift** — T-3.7 (bracketed-paste state) and any
|
||||||
|
follow-up to Phase 2 IC-2.1. Coordinate.
|
||||||
|
- **Settings UI** — T-3.6, T-3.8, T-3.9 all extend the same settings
|
||||||
|
surface. Land in series or merge carefully.
|
||||||
|
|
||||||
|
## Test Strategy
|
||||||
|
|
||||||
|
Per feature: a manual checklist row in `docs/PHASE-3-checklist.md`. No
|
||||||
|
heavy automation — these are visual / experiential features.
|
||||||
|
|
||||||
|
Critical regressions to watch for:
|
||||||
|
- Slash palette must not break input flow when dismissed (focus return).
|
||||||
|
- Voice must not steal focus from the WebSocket stream.
|
||||||
|
- Thumbnails must not block switcher rendering on slow links.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- **R1.** SFSpeechRecognizer offline accuracy varies. Mitigation: allow
|
||||||
|
on-device-only mode (slower, more private) vs. server-assisted toggle.
|
||||||
|
- **R2.** UIKeyCommand routing is finicky across scenes. Mitigation:
|
||||||
|
centralise key handling in a single `KeyCommandRouter` actor.
|
||||||
|
- **R3.** Custom theme JSON schema drift between iCloud devices. Mitigation:
|
||||||
|
versioned schema, migrate on read.
|
||||||
|
|
||||||
|
## Exit
|
||||||
|
|
||||||
|
Phase 3 has no hard exit — features land continuously. A "Phase 3 closed"
|
||||||
|
event is when every task above is shipped or explicitly deferred. At that
|
||||||
|
point write `docs/PHASE-3-report.md` summarising what made it, what didn't,
|
||||||
|
and ideas that came out of daily use for a future Phase 4.
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Implementation Docs
|
||||||
|
|
||||||
|
This folder drives the implementation work for the pi-remote iOS app and
|
||||||
|
its sidecar. Background / spec / audit material lives in
|
||||||
|
[`reference/`](./reference/).
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`PHASE-0-spike-stream.md`](./PHASE-0-spike-stream.md) | Stream PoC — verify tmux + pipe-pane + WebSocket. ~1 day, single agent. |
|
||||||
|
| [`PHASE-1-sidecar.md`](./PHASE-1-sidecar.md) | Sidecar production-ready: all S-features, multi-agent parallel work. |
|
||||||
|
| [`PHASE-2-ios-mvp.md`](./PHASE-2-ios-mvp.md) | iOS app MVP — Groups A, B, C-01/02, D, E, F. Multi-agent parallel. |
|
||||||
|
| [`PHASE-3-ios-augmentation.md`](./PHASE-3-ios-augmentation.md) | iOS feature polish — slash palette, voice, thumbnails, search, etc. |
|
||||||
|
| [`SYNC.md`](./SYNC.md) | Live multi-agent coordination — claims, file ownership, contract changes. |
|
||||||
|
|
||||||
|
## Order of work
|
||||||
|
|
||||||
|
1. Phase 0 first, single agent.
|
||||||
|
2. Phase 1 starts after Phase 0 green-lights; multi-agent parallel.
|
||||||
|
3. Phase 2 starts after Phase 1 is production-ready; multi-agent parallel.
|
||||||
|
4. Phase 3 is continuous after Phase 2 MVP ships.
|
||||||
|
|
||||||
|
See `SYNC.md` for the current state.
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
# SYNC — Multi-Agent Coordination
|
||||||
|
|
||||||
|
> **Purpose:** allow several agents (human or AI) to work concurrently on
|
||||||
|
> this codebase without stepping on each other.
|
||||||
|
>
|
||||||
|
> **Scope:** all phases. This document is the live coordination surface;
|
||||||
|
> the phase plans (`PHASE-0..PHASE-3`) are immutable plans, this file
|
||||||
|
> tracks who is doing what *right now*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How this works
|
||||||
|
|
||||||
|
1. Every concrete work item lives in a phase plan as `T-<phase>.<n>`.
|
||||||
|
2. Before starting work on a task, an agent:
|
||||||
|
- Pulls latest `main`.
|
||||||
|
- Edits the **Active Claims** table below to add a row claiming the
|
||||||
|
task with its branch name, owner handle, and timestamp.
|
||||||
|
- Commits that edit on `main` directly (small, low-conflict).
|
||||||
|
- Then opens the feature branch and works.
|
||||||
|
3. When the task is done (PR merged) the agent removes its claim row and
|
||||||
|
appends a one-line entry to **History**.
|
||||||
|
4. If a task needs to **change a frozen interface contract** (IC-1..IC-4
|
||||||
|
from Phase 1, IC-2.1 from Phase 2), the agent must:
|
||||||
|
- Open a section under **Contract Change Requests** below.
|
||||||
|
- Wait for at least one other active agent (or the orchestrator) to
|
||||||
|
acknowledge by editing the row to `acked: <handle>`.
|
||||||
|
- Only then implement the change.
|
||||||
|
|
||||||
|
The point: no central scheduler is required. A short structured edit on
|
||||||
|
`main` is the lock.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase Gate
|
||||||
|
|
||||||
|
| Phase | Status | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Phase 0 — Spike Stream | not started | First task. See `PHASE-0-spike-stream.md`. |
|
||||||
|
| Phase 1 — Sidecar | blocked on Phase 0 | Can begin only after Phase 0 verdict is green. |
|
||||||
|
| Phase 2 — iOS MVP | blocked on Phase 1 | Sidecar must be reachable and stable. |
|
||||||
|
| Phase 3 — iOS Augmentation | blocked on Phase 2 | Continuous after MVP ships. |
|
||||||
|
|
||||||
|
Update the **Status** column when a phase transitions. Allowed states:
|
||||||
|
`not started`, `in progress`, `blocked on …`, `done`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Active Claims
|
||||||
|
|
||||||
|
| Task | Branch | Owner | Claimed at | ETA | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| _(none)_ | | | | | |
|
||||||
|
|
||||||
|
Example of a filled row:
|
||||||
|
```
|
||||||
|
| T-1.1 | feat/p1-t1-1-tmux-manager | @jay | 2026-05-20 14:00 | +2d | starting with manager.ts |
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- **One row per task.** A task can have only one active owner.
|
||||||
|
- **Owner** = the agent's handle (`@jay`, `@worker-1`, `@scout`, etc.).
|
||||||
|
- **ETA** is a rough estimate; missing it is OK, but if a row is stale > 2× ETA, anybody may reclaim after pinging.
|
||||||
|
- **Branch** must exist on the remote within 24h of the claim, otherwise
|
||||||
|
the row is considered abandoned and may be removed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Ownership Map
|
||||||
|
|
||||||
|
For each high-traffic file, the table below lists the tasks that may
|
||||||
|
legitimately modify it. If you need to touch a file outside this list,
|
||||||
|
add a row or open a Contract Change Request.
|
||||||
|
|
||||||
|
| File | Authorised Tasks |
|
||||||
|
|---|---|
|
||||||
|
| `extensions/remote-control/index.ts` | T-1.0, T-1.4 (events wiring only) |
|
||||||
|
| `extensions/remote-control/server.ts` (legacy) | nobody after T-1.0; legacy frozen |
|
||||||
|
| `extensions/remote-control/server/**` | T-1.0 (refactor), T-1.5, T-1.6, T-1.7 |
|
||||||
|
| `extensions/remote-control/tmux/**` | T-1.1 |
|
||||||
|
| `extensions/remote-control/buffer/**` | T-1.2 |
|
||||||
|
| `extensions/remote-control/sequence.ts` | T-1.2 |
|
||||||
|
| `extensions/remote-control/auth/**` | T-1.3, T-1.10 (device tokens only) |
|
||||||
|
| `extensions/remote-control/pi/**` | T-1.4 |
|
||||||
|
| `extensions/remote-control/apns/**` | T-1.10, Phase-2 T-2.9 (when iOS supplies tokens) |
|
||||||
|
| `extensions/remote-control/cli/**` | T-1.3, T-1.7 |
|
||||||
|
| `extensions/remote-control/config.ts` | T-1.7 |
|
||||||
|
| `docs/SYNC.md` | all (this file) |
|
||||||
|
| `docs/PHASE-*.md` | nobody once a phase has started (frozen plan) — open a CCR to amend |
|
||||||
|
| `docs/reference/**` | nobody during implementation — archival |
|
||||||
|
|
||||||
|
For the iOS repo `pi-remote-ios`, an analogous map will be added when
|
||||||
|
Phase 2 kicks off.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frozen Interface Contracts
|
||||||
|
|
||||||
|
| ID | Defined in | Owner of changes |
|
||||||
|
|---|---|---|
|
||||||
|
| IC-1 — WebSocket frame protocol | `PHASE-1-sidecar.md` §Interface Contracts | T-1.5 lead, with sign-off from any active T-2.x owner |
|
||||||
|
| IC-2 — HTTP REST shape | `PHASE-1-sidecar.md` §Interface Contracts | T-1.5..T-1.7 leads |
|
||||||
|
| IC-3 — Pairing payload | `PHASE-1-sidecar.md` §Interface Contracts | T-1.3 lead |
|
||||||
|
| IC-4 — Config TOML schema | `PHASE-1-sidecar.md` §Interface Contracts | T-1.7 lead |
|
||||||
|
| IC-2.1 — `SessionConnection` Swift surface | `PHASE-2-ios-mvp.md` §Interface Contracts | T-2.5 lead |
|
||||||
|
|
||||||
|
Once a contract is *frozen* (i.e. at least one consumer task has started
|
||||||
|
work that depends on it), changes require a CCR.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contract Change Requests (CCR)
|
||||||
|
|
||||||
|
Format:
|
||||||
|
|
||||||
|
```
|
||||||
|
### CCR-YYYY-MM-DD-<slug>
|
||||||
|
- **Contract**: IC-1 / IC-2 / …
|
||||||
|
- **Proposer**: @handle
|
||||||
|
- **Motivation**: 1-3 sentences.
|
||||||
|
- **Proposed change**: diff or prose.
|
||||||
|
- **Affected tasks**: list.
|
||||||
|
- **Status**: open | acked by @… | merged | rejected
|
||||||
|
```
|
||||||
|
|
||||||
|
_(none open)_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Phase Notes
|
||||||
|
|
||||||
|
Threads that don't belong in a single phase plan.
|
||||||
|
|
||||||
|
- **Spec deviations.** If implementation reveals that a spec item is
|
||||||
|
wrong or unbuildable, write a short note here under **History**, plus
|
||||||
|
open a CCR if it changes a frozen contract. The spec itself stays
|
||||||
|
immutable until a v4 review round.
|
||||||
|
- **Risks materialised.** When a `Risks` row from a phase plan actually
|
||||||
|
hits, log it here with the workaround used.
|
||||||
|
- **Tools / shared scripts.** Anything added under `scripts/` that's
|
||||||
|
reused across tasks gets a one-liner here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
Append-only log of completed work and notable events. One line each.
|
||||||
|
|
||||||
|
```
|
||||||
|
yyyy-mm-dd @handle T-x.y what was done
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
2026-05-15 @jay init docs reorganised; phase plans + SYNC created
|
||||||
|
```
|
||||||
|
|
||||||
|
(populated as work happens)
|
||||||
|
|
@ -0,0 +1,936 @@
|
||||||
|
# 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.**
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Reference Documents
|
||||||
|
|
||||||
|
Background and design artefacts. The implementation work is driven by the
|
||||||
|
phase plans in the parent `docs/` directory.
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`SPEC-ios-app.md`](./SPEC-ios-app.md) | Final spec v3 for the iOS app and sidecar. The source of truth for what is being built. |
|
||||||
|
| [`EXTENSION-API-AUDIT.md`](./EXTENSION-API-AUDIT.md) | Audit of pi's ExtensionAPI — what's exposed, what's not, workarounds. Drives the realisability of S-07, S-08 and similar. |
|
||||||
|
| [`SPEC-ios-app-review-v1.md`](./SPEC-ios-app-review-v1.md) | Archived review thread of spec v1 → v2 with inline discussions. Historical, do not edit. |
|
||||||
|
| [`ARCHITECTURE.md`](./ARCHITECTURE.md) | Original architecture document for the existing `pi-remote-control` extension (HTML/WebSocket client). |
|
||||||
|
|
@ -0,0 +1,749 @@
|
||||||
|
# pi-remote — iOS Native App Spec
|
||||||
|
|
||||||
|
> **Review-Modus:** Jedes Feature unten hat eine ID und kann einzeln gestrichen
|
||||||
|
> werden. Sektionen "Vision", "Principles", "Architecture" sind das Rückgrat —
|
||||||
|
> wenn die fallen, kollabiert der Rest. Alles unter "Server Features" und "iOS
|
||||||
|
> Features" ist verhandelbar. Features tragen einen Priority-Hinweis
|
||||||
|
> (`MUST` / `SHOULD` / `NICE`) als Vorschlag, nicht als Festlegung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Vision
|
||||||
|
|
||||||
|
Eine iOS-App, die eine laufende `pi`-Session **byte-genau spiegelt**, wie sie
|
||||||
|
im SSH-Terminal aussieht — und durch native iOS-Mittel (Touch, Sprache,
|
||||||
|
Notifications, System-Integration) die Bedienung dieser Terminal-Umgebung
|
||||||
|
unterwegs angenehm macht.
|
||||||
|
|
||||||
|
**Kein Hybrid-Rendering.** Was im Terminal ANSI ist, bleibt in der App ANSI.
|
||||||
|
Die App fügt eine *Augmentation-Schicht* hinzu, kein paralleles UI.
|
||||||
|
|
||||||
|
**Kernszenario:** Eine Pi-Session läuft tagelang. Der Nutzer "besucht" sie
|
||||||
|
unregelmäßig — vom Mac via SSH, vom iPhone via App. Beide sehen jederzeit
|
||||||
|
denselben State. Reattach ist instant.
|
||||||
|
|
||||||
|
joe: anderer use case ist das spawnen und wechseln aktiver sessions. es muss
|
||||||
|
super easy sein, sessions zu spawnen und zu switchen. das session handling auf server seite sollte robust sein.
|
||||||
|
|
||||||
|
claude: Großer Punkt — das verändert die Gewichtung deutlich. Konkret:
|
||||||
|
- S-09 (Multi-Session) wird von SHOULD zu **MUST**, ebenso iOS-E-01.
|
||||||
|
- Wir brauchen explizite Server-Endpoints: `POST /sessions` (spawn), `DELETE /sessions/<id>` (kill), `PATCH /sessions/<id>` (rename), `GET /sessions` (list inkl. Status).
|
||||||
|
- Robustheit heißt für mich konkret: jede Session überlebt Sidecar-Restart (tmux-Sessions bleiben sowieso, wir reattach beim Boot), getrennte Ringbuffer/Log-Files pro Session, Crash einer Session zieht die anderen nicht runter.
|
||||||
|
- Spawn-Flow: User tippt in der App "+" → optional Name oder leer (dann Auto-Name, siehe S-09-Kommentar) → Server macht `tmux new-session -d -s <id> 'pi'` → Stream direkt verfügbar.
|
||||||
|
- Switching-UX siehe iOS-E-01-Reply.
|
||||||
|
|
||||||
|
Ich trage das in v2 der Spec ein und ziehe S-09 + iOS-E-01 hoch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Principles (locked)
|
||||||
|
|
||||||
|
- **P-1** — Der Terminal-Stream ist die einzige Wahrheit. Strukturierte Events
|
||||||
|
dienen ausschließlich Statusanzeigen und Notifications, nie dem Rendering
|
||||||
|
des Inhalts.
|
||||||
|
- **P-2** — SSH-Erfahrung bleibt unverändert. Der Mac-Workflow ändert sich
|
||||||
|
durch dieses Projekt nicht spürbar.
|
||||||
|
- **P-3** — Reconnect ist die wichtigste Operation. Sie muss < 1s nach
|
||||||
|
App-Wake sichtbares Ergebnis liefern. joe: warum is das so wichtig?
|
||||||
|
|
||||||
|
claude: Weil iOS deine App alle paar Minuten suspended und du sie dutzendfach
|
||||||
|
am Tag öffnest. Wenn jedes Öffnen 3-5s mit einem Spinner verbringt, fühlt
|
||||||
|
sich die App "kaputt" an, selbst wenn technisch nichts kaputt ist. Bei < 1s
|
||||||
|
ist es "die App war nie weg". Das ist das Gefühl, das du willst, wenn du
|
||||||
|
in einer langen Session immer wieder reinguckst.
|
||||||
|
|
||||||
|
Wenn du das anders gewichtest, können wir es zu "< 2s" lockern — aber
|
||||||
|
irgendein hartes Ziel sollten wir haben, sonst optimieren wir es nicht.
|
||||||
|
Soll das raus / weicher werden?
|
||||||
|
- **P-4** — Touch-UX wird *zur Terminal-Bedienung* gebaut, nicht *anstelle*
|
||||||
|
von Terminal-Bedienung.
|
||||||
|
- **P-5** — Solo-Use. Multi-User, Sharing, Org-Features sind out of scope.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Architecture (locked)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────┐
|
||||||
|
│ Server │
|
||||||
|
│ │
|
||||||
|
│ pi (Ink TUI) ◄─► tmux session ◄─► SSH-Client (Mac) │
|
||||||
|
│ │ │
|
||||||
|
│ │ pipe-pane (raw bytes) │
|
||||||
|
│ │ send-keys │
|
||||||
|
│ ▼ │
|
||||||
|
│ pi-remote sidecar │
|
||||||
|
│ │ │
|
||||||
|
│ │ WebSocket │
|
||||||
|
│ │ ├─ raw ANSI stream │
|
||||||
|
│ │ ├─ control (send-keys) │
|
||||||
|
│ │ └─ side-channel (state, meta) │
|
||||||
|
└─────────────────────────┼────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ iOS App │
|
||||||
|
│ ┌────────────────────┐ │
|
||||||
|
│ │ SwiftTerm renderer │ │
|
||||||
|
│ └────────────────────┘ │
|
||||||
|
│ ┌────────────────────┐ │
|
||||||
|
│ │ Augmentation layer │ │
|
||||||
|
│ │ (slash, snippets, │ │
|
||||||
|
│ │ status, voice, │ │
|
||||||
|
│ │ markers, ...) │ │
|
||||||
|
│ └────────────────────┘ │
|
||||||
|
└──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Komponenten:**
|
||||||
|
|
||||||
|
- **tmux** — Session-Persistenz, Multi-Client-Attach, Pane-Pipe, Send-Keys.
|
||||||
|
Nicht selbst gebaut.
|
||||||
|
- **Sidecar** (`pi-remote-control` erweitert) — Node-Prozess. Spawn/Reattach
|
||||||
|
tmux, exponiert WebSocket-API, hält Ringbuffer für Replay, leitet
|
||||||
|
ExtensionAPI-Metadaten als Side-Channel weiter.
|
||||||
|
- **iOS-App** — SwiftUI-Shell, SwiftTerm als Renderer-View, eigener
|
||||||
|
Augmentation-Layer.
|
||||||
|
|
||||||
|
**Datenfluss Stream:** `pi` → tmux pipe-pane → sidecar ringbuffer → WS → iOS
|
||||||
|
SwiftTerm.
|
||||||
|
|
||||||
|
**Datenfluss Input:** iOS → WS → sidecar → `tmux send-keys` → tmux pane → pi
|
||||||
|
stdin. Der Mac-SSH-Client sieht denselben Input.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Server Features (Sidecar)
|
||||||
|
|
||||||
|
### S-01 — tmux launcher / attach
|
||||||
|
**MUST.** Sidecar startet eine tmux-Session (`pi-main` oder pro Projekt
|
||||||
|
benannt), spawned `pi` darin. Falls Session existiert → reattach. Existiert
|
||||||
|
als CLI-Befehl `pi-remote attach`.
|
||||||
|
|
||||||
|
*Rationale:* P-2 — User-Workflow bleibt fast identisch.
|
||||||
|
*Dependencies:* —
|
||||||
|
|
||||||
|
### S-02 — Raw ANSI WebSocket-Endpoint
|
||||||
|
**MUST.** Endpoint `/stream` liefert die Bytes von `tmux pipe-pane` als
|
||||||
|
binäre WS-Frames. Eine Verbindung = ein Pane.
|
||||||
|
|
||||||
|
joe: warum können wir sicher sein, dass der stream der kommt von der ios app bzw der library so angenommen werden? sind hier fallstricke, die wir noch nicht sehen? ist der binary stream das beste für den use case? bringt uns compression hier was?
|
||||||
|
|
||||||
|
claude: Drei Teilfragen, separat beantwortet.
|
||||||
|
|
||||||
|
**(1) Kann SwiftTerm den Stream verarbeiten?**
|
||||||
|
Ja, mit Caveat. SwiftTerm ist eine vollständige VT100/xterm-Implementierung — alles was tmux/Ink rauswirft (CSI/SGR/cursor-positioning/alternate-screen-buffer/title) wird verarbeitet. **Fallstricke** die ich sehe:
|
||||||
|
- *Alternate Screen Buffer*: pi/Ink nutzt `\e[?1049h` für Full-Screen-Modus. SwiftTerm handhabt das, aber wir müssen den Scrollback-Cache (iOS-A-04) korrekt davon trennen (alternate-screen-Inhalt nicht im scrollback persistieren).
|
||||||
|
- *Mouse-Mode*: pi nutzt vermutlich kein Mouse-Tracking, aber falls eine Extension das tut, schicken wir die ANSI-Sequenzen blind an SwiftTerm — könnte auf Touch komisch sein. Lösung: Mouse-Tracking-Sequenzen optional ignorieren.
|
||||||
|
- *Terminal-Size-Mismatch*: tmux ist auf eine feste Größe konfiguriert. Wenn iPhone-Portrait nur 40 Spalten zeigt, sieht die Ausgabe gequetscht aus. Lösung: tmux per Session auf eine sinnvolle Größe (z.B. 120×40) fixen, Client rendert mit Pinch-Zoom passend.
|
||||||
|
- *Truecolor / 24bit*: SwiftTerm + Ink unterstützen beide, sollte sauber sein.
|
||||||
|
- *Title sequences*: pi setzt evtl. den Terminal-Title — wir können das als Side-Channel-Hinweis nutzen oder ignorieren. joe: brauchen wir nicht
|
||||||
|
|
||||||
|
**(2) Binär vs. Text-Frame?**
|
||||||
|
Binär. Begründung: ANSI ist 8-bit-clean (256-color codes, UTF-8 in vielen Bytes) — als Text-Frame müssten wir Base64'en (33% Overhead) oder JSON-escapen (variabel, oft schlimmer). Binär ist auch das, was WebSocket-Stacks für `ArrayBuffer`/`Data` direkt vertragen. Side-Channel-Events bleiben separate Text-Frames (JSON). Multiplexing per Frame-Typ.
|
||||||
|
|
||||||
|
**(3) Compression?**
|
||||||
|
Ja, lohnt sich. `permessage-deflate` (WebSocket-Standard-Extension) ist genau dafür gemacht. ANSI-Streams sind hochgradig redundant (gleiche SGR-Codes, gleiche Sequenzen), typische Kompressionsrate 3-5×. Für Mobile-Daten und Reconnect-Snapshots (die schnell mehrere MB groß werden) macht das einen spürbaren Unterschied. Auf Server-Seite billig in `ws` (Node-Library), auf iOS-Seite muss der WS-Client `permessage-deflate` unterstützen — `Starscream` (Swift WS-Lib) tut das.
|
||||||
|
|
||||||
|
**Empfehlung:** binär + `permessage-deflate` an, alternate-screen sauber tracken, Terminal-Size auf 120×40 fixieren als Default. Soll ich das in der Spec konkret festschreiben?
|
||||||
|
|
||||||
|
*Rationale:* P-1 — Direkter Stream als einzige Render-Quelle.
|
||||||
|
*Dependencies:* S-01
|
||||||
|
|
||||||
|
### S-03 — Send-Keys-Endpoint
|
||||||
|
**MUST.** WS-Nachrichten vom Client mit Keystroke-Payload werden in
|
||||||
|
`tmux send-keys -l` (literal) oder Key-Namen übersetzt und injiziert.
|
||||||
|
|
||||||
|
*Rationale:* Input-Pfad ohne den der Stream nutzlos ist.
|
||||||
|
*Dependencies:* S-01
|
||||||
|
|
||||||
|
### S-04 — Sequence-Cursor & Delta-Replay
|
||||||
|
**MUST.** Sidecar nummeriert ausgehende Bytes (oder Chunks). Client sendet
|
||||||
|
bei Reconnect `lastSeq`, Server liefert ab dort weiter.
|
||||||
|
|
||||||
|
*Rationale:* P-3 — Reconnect-Performance.
|
||||||
|
*Dependencies:* S-02
|
||||||
|
|
||||||
|
### S-05 — Snapshot-Endpoint
|
||||||
|
**MUST.** Wenn `lastSeq` außerhalb des Ringbuffers liegt: Server liefert
|
||||||
|
`tmux capture-pane -p -e -S -10000` als Snapshot + neuen Start-Seq.
|
||||||
|
|
||||||
|
*Rationale:* Fallback wenn App lange weg war.
|
||||||
|
*Dependencies:* S-01, S-04
|
||||||
|
|
||||||
|
### S-06 — Ringbuffer auf Disk
|
||||||
|
**SHOULD.** Sidecar persistiert die letzten N MB ANSI-Bytes pro Pane auf
|
||||||
|
Disk (z.B. `/var/lib/pi-remote/buffer/<session>.log`). Erlaubt Replay
|
||||||
|
weiter zurück als tmux-Scrollback.
|
||||||
|
|
||||||
|
joe: fokus hier auch auf das log file management. ich würde vermeiden wollen das die platte irgendwann vollläuft.
|
||||||
|
|
||||||
|
claude: Berechtigt. Konkretisierungsvorschlag für die Spec:
|
||||||
|
- **Pro Session**: Ringbuffer-Datei `/var/lib/pi-remote/buffer/<session>.log`, hartes Limit (default 100MB), Rotation in 10MB-Chunks, älteste Chunks werden gelöscht sobald Limit überschritten. joe: keine rotation. eine file pro session. wenn die 100mb überlaufen, dann is das halt so.
|
||||||
|
- **Global**: Hartes Total-Cap (default 1GB) über alle Sessions; wenn überschritten, werden die ältesten Chunks der größten Session gekürzt.
|
||||||
|
- **Idle-Cleanup**: Sessions die seit > 30 Tagen keinen Output erzeugt UND keinen Client gesehen haben → Buffer wird komprimiert/archiviert oder gelöscht (konfigurierbar).
|
||||||
|
- **Disk-Watchdog**: Sidecar checkt periodisch verfügbaren Platz; wenn < 1GB frei, schaltet er Buffer-Schreiben ab und meldet Health-Status "degraded" (S-13).
|
||||||
|
- Konfigurierbar via `~/.config/pi-remote/config.toml`.
|
||||||
|
|
||||||
|
Fertig in die Spec übernehmen? joe: ja
|
||||||
|
|
||||||
|
*Rationale:* "Sessions die tagelang laufen" können sehr großen Output
|
||||||
|
ansammeln; tmux-history wird truncated.
|
||||||
|
*Dependencies:* S-02
|
||||||
|
|
||||||
|
### S-07 — State Side-Channel
|
||||||
|
**SHOULD.** Sidecar abonniert pi-ExtensionAPI-Events (`thinking`,
|
||||||
|
`tool-start`, `tool-end`, `idle`, `awaiting-input`) und sendet sie über
|
||||||
|
denselben WS als JSON-Control-Frames.
|
||||||
|
|
||||||
|
*Rationale:* Quelle für Status-Bar und Notifications. Nicht für Rendering.
|
||||||
|
*Dependencies:* S-01
|
||||||
|
|
||||||
|
### S-08 — Slash-Command-Registry
|
||||||
|
**SHOULD.** Endpoint `/commands` liefert JSON-Liste der verfügbaren
|
||||||
|
Slash-Commands (Name, Beschreibung, Argument-Schema) aus pi's Registry.
|
||||||
|
Dynamisch — Extensions die Commands hinzufügen erscheinen automatisch.
|
||||||
|
|
||||||
|
*Rationale:* Speist die iOS-Command-Palette (iOS-D-01).
|
||||||
|
*Dependencies:* S-01
|
||||||
|
|
||||||
|
### S-09 — Multi-Session-Management
|
||||||
|
**SHOULD.** Sidecar kann mehrere tmux-Sessions parallel verwalten
|
||||||
|
(z.B. eine pro Projekt). Endpoint `/sessions` listet sie, Client wählt
|
||||||
|
beim Connect.
|
||||||
|
|
||||||
|
joe: client (also ios ux) sollte am besten alle sessions vom looka nd feel so anzeigen als wären sie direkt connected/zugreifbar. können wir mit tmux da auch irgendwie session namen oder descriptions vergeben? optional dynamisch von pi benannt (mini prompt, wenig tokens)?
|
||||||
|
|
||||||
|
claude: Drei separate Themen, alle ja:
|
||||||
|
|
||||||
|
**(a) Sessions "als wären sie connected":**
|
||||||
|
Die App connectet im Hintergrund parallel zu allen bekannten Sessions, hält für jede einen rolling Mini-Buffer (z.B. letzte 100 Zeilen) im RAM, plus die letzte gerenderte Frame-Snapshot. Switch = Render-View wechselt instant, im Hintergrund läuft Sync für volle Historie wenn nötig. → siehe auch iOS-E-01-Reply.
|
||||||
|
|
||||||
|
**(b) Session-Namen & Descriptions in tmux:**
|
||||||
|
- Name: tmux-Sessions HABEN bereits Namen (`tmux new -s <name>`). Wir nutzen den als stable ID.
|
||||||
|
- Description: tmux unterstützt User-Options, `tmux set-option -t <session> @description "…"`. Lesbar via `tmux show-options -t <session> -v @description`. Beliebige Metadaten so anhängbar (z.B. `@project`, `@created`).
|
||||||
|
- Wir können das mit unserem eigenen JSON-State-File im Sidecar kombinieren wenn wir mehr brauchen. joe: keinen weiteren state selbst handlen. tmux oder gar nicht
|
||||||
|
|
||||||
|
**(c) Auto-Naming via Pi (mini-prompt):**
|
||||||
|
Gute Idee. Konkret: nach ~3 User-Messages in einer namenlosen Session feuert der Sidecar einen Cheap-LLM-Call (haiku-tier, ~50 Tokens Output) gegen den bisherigen Verlauf: "Gib einen 2-4-Wort-Titel für diese Konversation." Ergebnis landet als `@description`. Manuelles Umbenennen aus der App jederzeit möglich und überschreibt das.
|
||||||
|
Open Question: Welcher Mini-LLM, welche Credentials? Wahrscheinlich derselbe Provider wie pi selbst — wir borgen pi's Konfiguration aus der Extension. Soll ich das als sub-feature S-09a notieren? joe: wir nutzen pi direkt. die cli bietet one shot an. als model nutzen wir haiku. anthropic auth ist in pi schon configuriert.
|
||||||
|
|
||||||
|
*Rationale:* Speist Session-Switcher (iOS-E-01).
|
||||||
|
*Dependencies:* S-01
|
||||||
|
|
||||||
|
### S-10 — Bearer-Token-Auth
|
||||||
|
**MUST.** WS-Verbindungen erfordern Bearer-Token aus Sidecar-Config.
|
||||||
|
Per-Device-Tokens möglich.
|
||||||
|
|
||||||
|
joe: auth mit mbile sollte einfach gehen, qr code im terminal oder so.
|
||||||
|
|
||||||
|
claude: Ja, sauberer Pfad. Konkret:
|
||||||
|
- Sidecar hat einen CLI-Befehl `pi-remote pair`. Generiert ein Pairing-Token (kurzlebig, z.B. 5min gültig).
|
||||||
|
- Druckt einen QR-Code im Terminal (Unicode block chars, funktioniert per SSH).
|
||||||
|
- QR enthält: WS-URL + Pairing-Token + (siehe S-11-Reply) TLS-Fingerprint.
|
||||||
|
- iOS-App scannt → tauscht Pairing-Token gegen permanenten Bearer-Token, speichert im Keychain (iOS-G-01).
|
||||||
|
- Token kann per Device benannt sein ("jay's iPhone"), revocable via CLI (`pi-remote auth list/revoke`).
|
||||||
|
|
||||||
|
QR-Library Server-seitig: `qrcode-terminal` (Node) — winzig, kein Aufwand.
|
||||||
|
|
||||||
|
*Rationale:* Sidecar ist ggf. öffentlich erreichbar.
|
||||||
|
*Dependencies:* —
|
||||||
|
|
||||||
|
### S-11 — TLS-Termination (Hinweis)
|
||||||
|
**MUST in Production.** WS via `wss://` hinter Reverse-Proxy oder
|
||||||
|
nativ. Kein Plaintext über öffentliche Netze.
|
||||||
|
|
||||||
|
joe: einfaches handling von self signed certificates. pinning via onboarding qr code oder so.
|
||||||
|
kein lust auf pki xD
|
||||||
|
|
||||||
|
claude: Genau der Plan. Trust-on-First-Use via QR:
|
||||||
|
- Sidecar generiert beim ersten Start ein selbstsigniertes Cert (ED25519 oder RSA-2048), persistent auf Disk.
|
||||||
|
- `pi-remote pair` QR enthält neben Token auch den SHA-256-Fingerprint des Server-Certs.
|
||||||
|
- iOS-App pinnt den Fingerprint beim Pairing fest. Jede künftige Connection verifiziert dagegen.
|
||||||
|
- Cert läuft nie ab (oder 10 Jahre — egal, wir validieren ja nur den Fingerprint, nicht die CA-Chain).
|
||||||
|
- Kein Let's Encrypt, kein DNS, kein Reverse-Proxy nötig — solange du eine IP/Hostname hast die das iPhone erreicht.
|
||||||
|
- Falls Cert rotiert wird (z.B. neuer Sidecar-Host): re-pairing via neuem QR.
|
||||||
|
|
||||||
|
Kein PKI, kein CA, kein Browser-Warning-Hell. Klar?
|
||||||
|
|
||||||
|
### S-12 — Marker-Detection / Prompt-Grenzen
|
||||||
|
**NICE.** Sidecar versucht heuristisch oder via Extension-Hook,
|
||||||
|
Prompt-Grenzen im Stream zu erkennen und als Marker im Side-Channel zu
|
||||||
|
publizieren (`{type: "marker", seq: 12345, label: "user-prompt-start"}`).
|
||||||
|
|
||||||
|
joe: wie robust ist das. gibt pi hier consistente marker?
|
||||||
|
|
||||||
|
claude: Ehrlich: ich weiß es nicht sicher. Vermutung: pi/Ink gibt **keine** expliziten ANSI-Marker-Sequenzen aus, die wir parsen könnten — Ink redrawt komplette Frames, das ist alles ein einziger Stream-of-Updates. Heuristisch ginge nur "Zeile beginnt mit `> ` oder `❯`" o.ä., das ist brüchig.
|
||||||
|
|
||||||
|
DU hast in Q-4 schon gesagt "gar nicht rein als feature". Damit fällt S-12 + iOS-E-03 + iOS-B-04 zusammen. Cleaner so. → Ich streiche S-12 in v2.
|
||||||
|
|
||||||
|
*Rationale:* Speist Jump-to-Marker (iOS-E-03).
|
||||||
|
*Dependencies:* S-07
|
||||||
|
|
||||||
|
### S-13 — Health & Metrics
|
||||||
|
**NICE.** `/health` Endpoint mit Session-Count, Buffer-Size, Connected
|
||||||
|
Clients. Für Monitoring.
|
||||||
|
|
||||||
|
*Rationale:* Selbst-Debugging im Long-Running-Setup.
|
||||||
|
*Dependencies:* —
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. iOS Client Features
|
||||||
|
|
||||||
|
### Gruppe A — Rendering & Stream
|
||||||
|
|
||||||
|
#### iOS-A-01 — SwiftTerm-Renderer
|
||||||
|
**MUST.** Vollwertiger ANSI-Terminal-View, der den raw Stream rendert.
|
||||||
|
Konfigurierbar: Font (monospace), Größe, Farbschema.
|
||||||
|
|
||||||
|
*Rationale:* P-1 — der Stream wird gerendert, nicht interpretiert.
|
||||||
|
*Dependencies:* S-02
|
||||||
|
|
||||||
|
#### iOS-A-02 — Sequence-Cursor & Reconnect
|
||||||
|
**MUST.** Client speichert lokal die letzte `seq` pro Session.
|
||||||
|
Bei WS-Reconnect: schickt `lastSeq`, verarbeitet Delta oder Snapshot.
|
||||||
|
|
||||||
|
*Rationale:* P-3.
|
||||||
|
*Dependencies:* S-04, S-05
|
||||||
|
|
||||||
|
#### iOS-A-03 — Stale-Frame-Display
|
||||||
|
**SHOULD.** Während Sync nach App-Wake: letzten gerenderten Frame
|
||||||
|
einfrieren, "syncing…" als subtile Overlay-Pill anzeigen. Kein leerer
|
||||||
|
Screen.
|
||||||
|
|
||||||
|
*Rationale:* Wahrnehmung: App fühlt sich immer "live" an.
|
||||||
|
*Dependencies:* iOS-A-02
|
||||||
|
|
||||||
|
#### iOS-A-04 — Local Scrollback-Cache
|
||||||
|
**SHOULD.** App puffert empfangene Bytes lokal (rolling, z.B. 5MB) für
|
||||||
|
Offline-Scrolling und Suche, auch wenn WS offline.
|
||||||
|
|
||||||
|
*Rationale:* Speist iOS-E-02 (Scrollback-Search). Reduziert Server-Hits.
|
||||||
|
*Dependencies:* iOS-A-01
|
||||||
|
|
||||||
|
#### iOS-A-05 — Theme-Editor
|
||||||
|
**NICE.** Color-Schemes (Solarized, Dracula, custom), Font-Size,
|
||||||
|
Line-Height. iCloud-Sync der Preferences.
|
||||||
|
|
||||||
|
joe: gerne eine klassische liste bekannter defaults mitliefern (inkl. monokai o.vgl.)
|
||||||
|
|
||||||
|
claude: 👍 — ich schreibe in v2 eine konkrete Liste ein: **Solarized Light/Dark, Monokai, Dracula, Nord, Gruvbox Dark/Light, Tomorrow Night, GitHub Light/Dark, System (folgt iOS Dark Mode), Default**. Plus "Custom" für eigene Farbtabellen. iCloud-Sync nur für Custom-Themes.
|
||||||
|
joe: top. ich will auch unbedingt mind. "Hack" und Jetbrains Mono als font.
|
||||||
|
|
||||||
|
*Rationale:* Lange Lesephasen → Augenkomfort.
|
||||||
|
*Dependencies:* iOS-A-01
|
||||||
|
|
||||||
|
### Gruppe B — Input & Modifier
|
||||||
|
|
||||||
|
#### iOS-B-01 — Software-Keyboard + Text-Input
|
||||||
|
**MUST.** Standard-iOS-Keyboard. Text wird beim Tippen oder beim
|
||||||
|
Enter-Tap an `send-keys` geschickt.
|
||||||
|
|
||||||
|
joe: leeres enter sendet new line im editor
|
||||||
|
|
||||||
|
claude: Hmm, ich muss das nochmal verstehen — meinst du:
|
||||||
|
|
||||||
|
**(a)** Eingabefeld der App ist ein **mehrzeiliges Compose-Feld** (du tippst kompletten Prompt mit Newlines, drückst dann einen separaten "Send"-Button). Enter im Feld = Newline einfügen.
|
||||||
|
Vorteil: bequem für längere Prompts, kein Risiko versehentlich abzuschicken.
|
||||||
|
Nachteil: nicht 1:1 wie im Terminal — pi sieht den Prompt erst beim Send als ein Block.
|
||||||
|
|
||||||
|
**(b)** Eingabe ist **direkt-passthrough** (jeder Tastendruck geht sofort als `send-keys`). Enter sendet `\r` an pi → pi terminiert Eingabe und schickt sie an den Agent. Wenn du in pi's eigenem multi-line-Modus bist (Shift-Enter o.ä.), entsprechend.
|
||||||
|
|
||||||
|
Ich hatte (b) im Kopf, weil P-1/P-4 (Terminal-Bedienung). Aber dein Kommentar klingt nach (a) mit einer Hybrid-Regel: "leeres Enter = newline einfügen" (also Modus a, nur smart).
|
||||||
|
|
||||||
|
Welche Variante meinst du? Mein Vorschlag: **(b) als Default, (a) optional togglebar** — weil wir sonst pi's eigenes Input-Handling (z.B. multi-line-Mode in pi) duplizieren und Bugs kriegen.
|
||||||
|
|
||||||
|
joe: du hast recht. es ist b. aber ich will den shift+enter easily available haben. termius hat shortcuts die man auf volume down und up legen kann. oder doch besser in der status leiste?
|
||||||
|
|
||||||
|
*Rationale:* Die Basis.
|
||||||
|
*Dependencies:* S-03
|
||||||
|
|
||||||
|
#### iOS-B-02 — Modifier-Bar
|
||||||
|
**MUST.** Akzessoriumsleiste über der Tastatur mit: `ESC`, `Tab`,
|
||||||
|
`Ctrl`, `Alt`, `↑↓←→`, `Enter`. Sticky-Toggle für Modifier (ein Tap =
|
||||||
|
hält bis zur nächsten Eingabe).
|
||||||
|
|
||||||
|
joe: welche modifier machen hier sinn? wir sind ja pi agent centric. gibts noch alternativen. der platz ist ja begrenzt.
|
||||||
|
|
||||||
|
claude: Guter Punkt — generischer Terminal-Approach passt nicht zu pi-centric. Konkret was pi tatsächlich braucht:
|
||||||
|
|
||||||
|
**Aus pi-Usage tatsächlich essenziell:**
|
||||||
|
- `Esc` — pi abbrechen / aus Modi raus
|
||||||
|
- `↑/↓` — History scrollen, in Auswahllisten navigieren
|
||||||
|
- `Tab` — Autocomplete in pi-Eingabe (Dateien, Commands)
|
||||||
|
- `Enter` — senden
|
||||||
|
- `Ctrl-C` — laufendes Tool abbrechen / pi-Action stoppen joe: macht das nicht Esc eh schon?
|
||||||
|
|
||||||
|
**Nice-to-have:**
|
||||||
|
- `Ctrl-D` — pi beenden (vielleicht versteckt, zu gefährlich für leichte Touch)
|
||||||
|
- `Ctrl-L` — clear
|
||||||
|
|
||||||
|
**Vorschlag finale Modifier-Bar (pi-tuned):**
|
||||||
|
```
|
||||||
|
[Esc] [Tab] [↑] [↓] [Ctrl-C] [/] [🎙] joe: lieber ctrl als ctrl+c. und / kann raus.
|
||||||
|
```
|
||||||
|
- `/` ist Shortcut: öffnet Slash-Command-Palette (iOS-C-04) ODER tippt einfach `/` ein
|
||||||
|
- `🎙` ist Voice-Input (iOS-C-06)
|
||||||
|
- Pfeile nur ↑/↓ weil ←/→ via Touch-Cursor selten (und Touch-Cursor flog ja raus) joe: link rechts pfeile brauche ich eben weil touch cursor rausflog)
|
||||||
|
- Ctrl-C als eigener Button weil häufigste Aktion
|
||||||
|
- Kein Alt/Meta, kein Ctrl-als-Modifier-Toggle — wir mappen 2-3 wichtige Ctrl-Combos auf eigene Buttons, fertig joe: s.o.
|
||||||
|
|
||||||
|
Wenn man im pi-internen Editor (für längere Prompts) ist, könnten ←/→ einblendbar sein context-aware. Aber MVP ohne.
|
||||||
|
|
||||||
|
Passt das? Was fehlt dir noch in deinem täglichen pi-Flow?
|
||||||
|
|
||||||
|
*Rationale:* Ohne ESC und Pfeile ist iOS-Terminal-Nutzung untauglich.
|
||||||
|
*Dependencies:* iOS-B-01
|
||||||
|
|
||||||
|
#### iOS-B-03 — Long-Press-Repeat
|
||||||
|
**SHOULD.** Long-Press auf Pfeiltasten / Backspace → repeat mit
|
||||||
|
Beschleunigung.
|
||||||
|
|
||||||
|
*Rationale:* Lange Navigation in Prompts.
|
||||||
|
*Dependencies:* iOS-B-02
|
||||||
|
|
||||||
|
#### iOS-B-04 — Tap-to-Cursor
|
||||||
|
**SHOULD.** Tippen auf eine Position in der aktuellen pi-Eingabezeile →
|
||||||
|
App schickt entsprechend viele `←`/`→` um den Cursor dahin zu bewegen.
|
||||||
|
|
||||||
|
*Rationale:* Cursorbewegung per Touch — einer der "killer" Hebel den ein
|
||||||
|
dummes Terminal nicht hat.
|
||||||
|
*Dependencies:* iOS-A-01, iOS-B-02. Heuristik: aktuelle Zeile = letzte
|
||||||
|
Zeile vor Prompt-Marker; ohne S-12 nur best-effort.
|
||||||
|
|
||||||
|
joe: brauchen wir nicht fliegt raus.
|
||||||
|
|
||||||
|
claude: ✂️ raus. Konsistent mit dem Marker-Drop.
|
||||||
|
|
||||||
|
#### iOS-B-05 — Selection & Copy
|
||||||
|
**MUST.** Doppel-Tap → Wort. Tripel-Tap → Zeile. Lange-Press + Drag →
|
||||||
|
Range. Native iOS-Copy-Menu.
|
||||||
|
|
||||||
|
*Rationale:* Output ist oft das Wertvolle (Code, IDs, Pfade).
|
||||||
|
*Dependencies:* iOS-A-01
|
||||||
|
|
||||||
|
#### iOS-B-06 — Pinch-Zoom Font
|
||||||
|
**SHOULD.** Pinch in der Terminal-View → Font-Size live.
|
||||||
|
*Rationale:* Lesbarkeit.
|
||||||
|
*Dependencies:* iOS-A-01
|
||||||
|
|
||||||
|
joe: love it
|
||||||
|
|
||||||
|
claude: 🤝
|
||||||
|
|
||||||
|
#### iOS-B-07 — Hardware-Keyboard-Support
|
||||||
|
**SHOULD.** Externe iPad-Keyboards: Caps→Esc-Remap (optional),
|
||||||
|
Modifier-Pass-Through (Cmd, Option, Ctrl), Shortcuts wie Cmd-K → klar
|
||||||
|
definierte App-Aktionen.
|
||||||
|
|
||||||
|
*Rationale:* iPad mit Magic Keyboard = ernsthafte Pi-Workstation.
|
||||||
|
*Dependencies:* iOS-B-01
|
||||||
|
|
||||||
|
joe: love it
|
||||||
|
|
||||||
|
claude: 🤝 — wird in v2 noch konkretisiert welche Cmd-K Shortcuts sinnvoll sind (Cmd-K Clear, Cmd-T new session, Cmd-1..9 session switch, Cmd-F search).
|
||||||
|
|
||||||
|
#### iOS-B-08 — Reachability / One-Hand-Mode
|
||||||
|
**NICE.** iPhone-Querformat: Modifier-Bar + Action-Buttons auf einer
|
||||||
|
Seite gespiegelt, daumengerecht.
|
||||||
|
|
||||||
|
*Rationale:* Einhandbedienung.
|
||||||
|
*Dependencies:* iOS-B-02
|
||||||
|
|
||||||
|
#### iOS-B-09 — Smart Paste
|
||||||
|
**SHOULD.** Clipboard-Indicator in der Modifier-Bar. Tap → Preview
|
||||||
|
(insb. multi-line), Confirm → paste. Bei Pi-detection (z.B. bracketed
|
||||||
|
paste mode an) korrekt einrahmen.
|
||||||
|
|
||||||
|
joe: check ich nicht. erkläre nochmal
|
||||||
|
|
||||||
|
claude: Sorry, zu kryptisch. Zwei Sachen die zusammenhängen:
|
||||||
|
|
||||||
|
**"Smart Paste" = Confirm-vor-Paste**
|
||||||
|
Problem: du kopierst auf dem iPhone aus Safari einen Stacktrace, wechselst in die pi-App, tappst aus Versehen auf "Paste" → riesiger Blob wird an pi geschickt, möglicherweise mit ANSI-Escape-Codes oder Geheimnissen drin. Schwer zu undo.
|
||||||
|
Lösung: Clipboard-Indicator in der Modifier-Bar zeigt einen kleinen Vorschau-Chip ("📋 12 lines, 847 chars"). Tap → Sheet mit vollem Preview + "Insert" / "Cancel". Erst dann geht's an pi.
|
||||||
|
|
||||||
|
**"Bracketed Paste Mode" = pi unterscheidet getippte vs. gepastete Eingabe**
|
||||||
|
Technischer Hintergrund: moderne Terminals wrappen gepastete Eingabe in spezielle ESC-Sequenzen (`\e[200~` vor Paste, `\e[201~` danach). pi/Ink kann dann erkennen "das war Paste, nicht getippt" und z.B. den ganzen Block als einen Prompt nehmen statt nach jedem Newline zu submitten. Wenn pi diesen Modus aktiviert (`\e[?2004h` im Stream), muss unsere App das mitkriegen und beim Paste die richtigen Sequenzen drumherum schicken.
|
||||||
|
|
||||||
|
Die beiden Sachen sind orthogonal — Confirm-vor-Paste ist UX, Bracketed-Paste ist Protocol-Correctness. Ich hatte sie in einem Bullet gemischt, das war unklar. In v2 trenne ich sie in **iOS-B-09a (Paste-Confirm)** und **iOS-B-09b (Bracketed-Paste-Compliance)**.
|
||||||
|
|
||||||
|
Macht das mehr Sinn? Beide drin lassen, oder nur eines?
|
||||||
|
joe: passt.
|
||||||
|
|
||||||
|
*Rationale:* Sicherer als blindes Paste.
|
||||||
|
*Dependencies:* iOS-B-01
|
||||||
|
|
||||||
|
### Gruppe C — Pi-aware Augmentation
|
||||||
|
|
||||||
|
#### iOS-C-01 — Status-Bar
|
||||||
|
**MUST.** Top-Bar zeigt Connection-Status, Session-Name und
|
||||||
|
abgeleiteten Pi-State (`● thinking` / `⏵ tool: Edit` / `▶ waiting` /
|
||||||
|
`⏸ idle`) basierend auf Side-Channel-Events.
|
||||||
|
|
||||||
|
*Rationale:* Schneller Blick: was tut Pi gerade?
|
||||||
|
*Dependencies:* S-07
|
||||||
|
|
||||||
|
#### iOS-C-02 — Push-Notification bei State-Change
|
||||||
|
**MUST.** Wenn App im Background und Pi wechselt zu
|
||||||
|
`awaiting-input` → Push-Notification "Pi ist fertig". Tap →
|
||||||
|
App in der richtigen Session.
|
||||||
|
|
||||||
|
*Rationale:* Killer-Feature für "Session läuft lange, kommt wieder
|
||||||
|
wenn fertig".
|
||||||
|
*Dependencies:* S-07, APNs-Setup
|
||||||
|
|
||||||
|
#### iOS-C-03 — Haptic Feedback bei State-Wechseln
|
||||||
|
**NICE.** Subtile Vibration bei `thinking → idle` Übergang.
|
||||||
|
*Rationale:* Fühlt sich "lebendig" an.
|
||||||
|
*Dependencies:* S-07
|
||||||
|
|
||||||
|
#### iOS-C-04 — Slash-Command-Palette
|
||||||
|
**SHOULD.** Swipe-up-Sheet mit nativer Liste aller Slash-Commands
|
||||||
|
(aus S-08), fuzzy-search, Tap → injiziert via `send-keys`. Bei
|
||||||
|
Commands mit Argumenten → Formular-View für die Eingabe.
|
||||||
|
|
||||||
|
*Rationale:* Slash-Commands sind im Terminal mühsam zu tippen.
|
||||||
|
*Dependencies:* S-03, S-08
|
||||||
|
|
||||||
|
#### iOS-C-05 — Snippet-Library
|
||||||
|
**SHOULD.** Vorgefertigte Prompts mit Variablen ("review {{branch}}
|
||||||
|
for security"). iCloud-Sync. Native Edit-UI.
|
||||||
|
|
||||||
|
*Rationale:* Wiederkehrende Prompts.
|
||||||
|
*Dependencies:* iOS-B-01
|
||||||
|
|
||||||
|
joe: will ich mit pi internen tools lösen, kann raus
|
||||||
|
|
||||||
|
claude: ✂️ raus. Macht Sinn — Snippets gehören thematisch zu pi, nicht zum Remote-Client.
|
||||||
|
|
||||||
|
#### iOS-C-06 — Voice-to-Prompt
|
||||||
|
**NICE.** Mikro-Button → Speech-Recognition (lokal, iOS native) →
|
||||||
|
editable Preview → Send.
|
||||||
|
|
||||||
|
*Rationale:* Unterwegs schneller als tippen.
|
||||||
|
*Dependencies:* iOS-B-01, Mic-Permission
|
||||||
|
|
||||||
|
### Gruppe D — Session & Navigation
|
||||||
|
|
||||||
|
#### iOS-E-01 — Session-Switcher
|
||||||
|
**SHOULD.** Native iOS-Liste der verfügbaren tmux-Sessions (aus S-09).
|
||||||
|
Wechsel = neuer WS-Stream, vorhergehender wird im Hintergrund weiter
|
||||||
|
gepuffert.
|
||||||
|
|
||||||
|
joe: kann man hier schon auf basis einer bestimmten aktion des users preloaden? wäre nice, wenn es sich so anfühlt als wäre die session "schon immer da gewesen" :D
|
||||||
|
|
||||||
|
claude: Yes! Konkret zwei Strategien kombinierbar:
|
||||||
|
|
||||||
|
**(1) Background-Pre-Connect aller Sessions**
|
||||||
|
App connectet im Hintergrund zu allen bekannten Sessions parallel, hält für jede einen kleinen In-Memory-Stream-Buffer. Switch = Renderer wechselt instant, ist schon synchronisiert. Kosten: paar Sockets + paar MB RAM für Buffer pro Session. Akzeptabel solange du < ~10 aktive Sessions hast.
|
||||||
|
|
||||||
|
**(2) Optimistic-Switch mit Stale-Frame**
|
||||||
|
Swipe zu Session B → sofort den letzten gerenderten Frame von B zeigen (lokal gecached), gleichzeitig live-Stream nachholen. Du siehst niemals einen leeren Screen, nur eine kurze "sync"-Pille.
|
||||||
|
|
||||||
|
**(3) Bonus: Predictive Pre-Render bei Switcher-Open**
|
||||||
|
Wenn User die Switcher-Liste öffnet → App pingt alle Sessions, holt Frame-Snapshots, zeigt Live-Thumbnails (kleine Mini-Terminal-Vorschauen in der Liste). Switch ist dann visuell "ich sehe schon was drin ist".
|
||||||
|
|
||||||
|
Kosten: trivial server-seitig (capture-pane), client-seitig moderate Memory.
|
||||||
|
|
||||||
|
Ich trage (1) + (2) + (3) in v2 als iOS-E-01a/b/c ein. Soll (3) rein? Wäre wirklich "feels-magic"-Material.
|
||||||
|
|
||||||
|
*Rationale:* Pro Projekt eine Pi-Session.
|
||||||
|
*Dependencies:* S-09
|
||||||
|
|
||||||
|
#### iOS-E-02 — Scrollback-Search
|
||||||
|
**SHOULD.** Such-Sheet sucht im lokalen Scrollback-Cache (iOS-A-04)
|
||||||
|
mit Highlight + Jump-to-Match.
|
||||||
|
|
||||||
|
*Rationale:* "Wo war das eine Snippet?" — Standard-Bedarf.
|
||||||
|
*Dependencies:* iOS-A-04
|
||||||
|
|
||||||
|
#### iOS-E-03 — Jump-to-Marker
|
||||||
|
**NICE.** Vertikales Swipe-Gesture springt zwischen Prompt-Grenzen.
|
||||||
|
Visualisiert als kleine Edge-Indicator-Linien.
|
||||||
|
|
||||||
|
*Rationale:* "Vor 3 Antworten gab's diese Idee" — Standard-Bedarf.
|
||||||
|
*Dependencies:* S-12, iOS-A-01
|
||||||
|
|
||||||
|
joe: lassen wir raus. dazu aber ein anderer gedanke. wie viel info können wir reliable von pi über die session und die tree view bekommen? das feature vom pi ist so zentral, dass es nativen ios support verdient
|
||||||
|
|
||||||
|
claude: ✂️ iOS-E-03 raus. Aber der **andere Gedanke** ist groß — wert eines eigenen Threads.
|
||||||
|
|
||||||
|
**Zur Frage: Tree-View nativ in iOS rendern.**
|
||||||
|
|
||||||
|
Das wäre die einzige Stelle, an der wir P-1 ("Stream ist die einzige Wahrheit") aufweichen. Argument dafür: Tree-State ist **Meta-State der Session**, nicht **Inhalt der Session**. Das ist eine andere Kategorie als "Markdown rendern statt ANSI". Konsistent argumentierbar.
|
||||||
|
|
||||||
|
**Was wir bräuchten (Server-seitig):**
|
||||||
|
Ein neuer Side-Channel-Endpoint, der pi's Tree als strukturiertes JSON liefert + Live-Updates bei Branch/Fork/Compact. Format ungefähr:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{"id": "abc", "parent": null, "summary": "main", "messageCount": 42, "createdAt": "..."},
|
||||||
|
{"id": "def", "parent": "abc", "summary": "explored iOS", "messageCount": 18, ...}
|
||||||
|
],
|
||||||
|
"current": "def"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Was wir brauchen (Client-seitig):**
|
||||||
|
Ein natives SwiftUI-Tree-View-Sheet. Tap auf Knoten → entweder direkter Switch via `/checkout <id>` an pi gesendet, oder Preview-Modus mit Stream-Snapshot des Knotens.
|
||||||
|
|
||||||
|
**Großes Aber:**
|
||||||
|
Das hängt komplett davon ab, was die ExtensionAPI hergibt. Das ist genau das Thema von Q-2/Q-3 (was kann die ExtensionAPI?). Wir hatten in der vorherigen Session festgestellt: `pi.sendUserMessage()` umgeht Slash-Command-Processing, `session.prompt()` ist nicht exposed, der Tree wird intern in der Runtime gehalten — Zugriff auf Tree-State ist nicht öffentlich.
|
||||||
|
|
||||||
|
**Vorschlag:** Ich trage in v2 einen neuen Feature-Block **Gruppe T — Tree Navigation** ein, mit dem expliziten Hinweis "depends on ExtensionAPI investigation". Konkret:
|
||||||
|
- **S-14 (NEU)** — Tree-State-Endpoint + Live-Updates (server-seitig, bedingt machbar)
|
||||||
|
- **iOS-T-01 (NEU)** — Native Tree-View mit Checkout-Action
|
||||||
|
- **iOS-T-02 (NEU)** — Branch/Fork-Action aus der App heraus (`/fork`, `/new`, `/compact` via slash-command-injection)
|
||||||
|
|
||||||
|
Ohne ExtensionAPI-Verbesserung sind iOS-T-01/T-02 nur via Slash-Command-Injection halbgar machbar (wir sehen den Tree nicht, können aber Commands schicken). Mit ExtensionAPI-Erweiterung wird's mächtig.
|
||||||
|
|
||||||
|
Soll ich diese Gruppe T in v2 mit aufnehmen + als ersten Spike-Punkt "ExtensionAPI-Audit" definieren?
|
||||||
|
|
||||||
|
#### iOS-E-04 — Bookmarks
|
||||||
|
**NICE.** Long-Press auf Stelle im Scrollback → benannte Marke.
|
||||||
|
Bookmarks-Sheet listet sie, Tap → Jump.
|
||||||
|
|
||||||
|
*Rationale:* Manuelle Marker für wichtige Stellen.
|
||||||
|
*Dependencies:* iOS-A-04
|
||||||
|
|
||||||
|
joe: brauchen wir nicht. raus
|
||||||
|
|
||||||
|
claude: ✂️ raus.
|
||||||
|
|
||||||
|
### Gruppe E — Background & Lifecycle
|
||||||
|
|
||||||
|
#### iOS-F-01 — WS-Keepalive
|
||||||
|
**MUST.** Ping/Pong-Frames im Foreground halten Connection auf
|
||||||
|
zombie-detection-bewussten iOS-Stacks.
|
||||||
|
|
||||||
|
*Rationale:* iOS suspended WS gerne ohne Notification.
|
||||||
|
*Dependencies:* S-02
|
||||||
|
|
||||||
|
#### iOS-F-02 — Wake-up-Sync
|
||||||
|
**MUST.** App-Foreground-Event triggert sofort Reconnect + Delta-Pull.
|
||||||
|
Spinner falls > 200ms.
|
||||||
|
|
||||||
|
*Rationale:* P-3.
|
||||||
|
*Dependencies:* iOS-A-02
|
||||||
|
|
||||||
|
#### iOS-F-03 — Silent-Push-Trigger für Wake
|
||||||
|
**NICE.** Server kann Silent-Push schicken um App zu wecken bevor
|
||||||
|
User selbst auf das Icon tippt → Daten sind schon da.
|
||||||
|
|
||||||
|
*Rationale:* "App fühlt sich an als wär sie nie weg gewesen."
|
||||||
|
*Dependencies:* S-07, APNs
|
||||||
|
|
||||||
|
joe: wann sollte sie das tun? geht das überhaupt mit ios aggresivem app lifecycle management?
|
||||||
|
|
||||||
|
claude: Geht — aber mit Caveats die das Feature ziemlich entwerten.
|
||||||
|
|
||||||
|
**Mechanik:** APNs unterstützt "Silent Pushes" (`content-available: 1`, kein Alert). iOS weckt die App im Background für ~30s Processing-Zeit. App kann WebSocket aufmachen, Delta pullen, schlafen.
|
||||||
|
|
||||||
|
**Caveats:**
|
||||||
|
- iOS coalesced/dropped Silent Pushes nach eigenem Gusto. Apple gibt keine Garantie der Zustellung, nur "best effort".
|
||||||
|
- Budget ist limitiert: ~2-3 erfolgreiche Background-Wakes pro Stunde, danach drosselt iOS aggressiv.
|
||||||
|
- Wenn User die App im App-Switcher force-quit-tet, kommen **gar keine** Background-Wakes mehr — auch Silent Push nicht.
|
||||||
|
- Battery / Low-Power-Mode killt das komplett.
|
||||||
|
|
||||||
|
**Realistischer Wert für uns:** marginal. Das echte Killer-Feature ist iOS-C-02 (sichtbare Push wenn pi `awaiting-input` erreicht) — die ist nicht silent und wird zuverlässig zugestellt.
|
||||||
|
|
||||||
|
**Vorschlag:** iOS-F-03 streichen. Stattdessen verlassen wir uns auf iOS-F-02 (Wake-up-Sync beim manuellen Öffnen), das ist mit Sequence-Cursor + permessage-deflate schnell genug. Silent-Push lohnt den Aufwand nicht.
|
||||||
|
|
||||||
|
Einverstanden mit streichen?
|
||||||
|
joe: ja
|
||||||
|
|
||||||
|
### Gruppe F — Security
|
||||||
|
|
||||||
|
#### iOS-G-01 — Token im Keychain
|
||||||
|
**MUST.** Bearer-Token aus S-10 wird im iOS Keychain gespeichert,
|
||||||
|
nie im Klartext.
|
||||||
|
*Dependencies:* S-10
|
||||||
|
|
||||||
|
#### iOS-G-02 — Face-ID / Touch-ID Gate
|
||||||
|
**SHOULD.** Optionaler Biometrie-Lock vorm Öffnen.
|
||||||
|
*Rationale:* Pi-Sessions können sensitive Daten zeigen.
|
||||||
|
*Dependencies:* —
|
||||||
|
|
||||||
|
joe: ja, will ich haben
|
||||||
|
|
||||||
|
claude: 👍 — soll Face-ID dann verpflichtend (`MUST`) sein oder optional toggle? Bei optional ist's Setting. Bei MUST muss jeder App-Wake erst durch Face-ID. Mein Vorschlag: **opt-in via Setting, default off** — sonst nervt das Re-Auth alle paar Minuten. joe: opt in
|
||||||
|
|
||||||
|
#### iOS-G-03 — Certificate-Pinning
|
||||||
|
**NICE.** TLS-Pinning gegen Sidecar-Host.
|
||||||
|
*Rationale:* MITM-Hardening.
|
||||||
|
*Dependencies:* S-11
|
||||||
|
|
||||||
|
joe: einfaches handling von self signed certificates. pinning via onboarding qr code oder so.
|
||||||
|
kein lust auf pki xD
|
||||||
|
|
||||||
|
claude: Siehe S-11-Reply — gleicher Mechanismus, das ist dasselbe Feature aus Client-Sicht. In v2 fasse ich S-11 + iOS-G-03 zu einem zusammenhängenden "TLS via TOFU+Pinning" Block.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Out of Scope (locked rejections)
|
||||||
|
|
||||||
|
- **Rich Chat-Rendering.** Wir parsen den Stream nicht. Wenn Pi Markdown
|
||||||
|
ausgibt, sieht der Nutzer ANSI-Markdown wie im Terminal.
|
||||||
|
- **Embedded Mini-Terminals / Block-Selection.** Es gibt einen Stream.
|
||||||
|
- **Multi-User-Sharing.** Solo.
|
||||||
|
- **Org/Team-Features, Cloud-Hosted-Service.** Selbstgehostet, ein Mensch.
|
||||||
|
- **Inline-Image-Rendering** (iTerm2 image protocol etc.). Eventuell
|
||||||
|
später, aber nicht in v1.
|
||||||
|
- **Eigenes Mosh.** tmux + WebSocket reicht.
|
||||||
|
- **Cross-platform-Client (Android / Web).** Nur iOS nativ. Bestehende
|
||||||
|
HTML-UI bleibt aber bestehen (separates Artefakt).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Offene Fragen
|
||||||
|
|
||||||
|
- **Q-1** — Wie wird `pi-remote attach` zur Default-Methode? Shell-Alias
|
||||||
|
manuell vs. in pi-Launcher gepatcht?
|
||||||
|
joe: irrelevant. kümmer ic mich selbst drum
|
||||||
|
|
||||||
|
claude: ✂️ Q-1 raus.
|
||||||
|
- **Q-2** — Side-Channel-State: liefert die aktuelle ExtensionAPI alle
|
||||||
|
benötigten Events (`thinking`, `awaiting-input`)? Falls nicht, was
|
||||||
|
brauchen wir upstream?
|
||||||
|
joe: was kann die extension api denn? das sollten wir vorher prüfen
|
||||||
|
|
||||||
|
claude: 👍 — das ist der zentrale Spike vor Phase 0. Ich schlage vor wir definieren das als **Spike-0a — ExtensionAPI-Audit**:
|
||||||
|
- Was liefert sie heute an Events? (`thinking`, `awaiting-input`, `tool-start/end`, Stream-Indicators?)
|
||||||
|
- Welche Slash-Commands kann sie abfragen? (Registry exposed oder nicht?)
|
||||||
|
- Tree-State zugänglich? (Read-only? Subscribe? Mutations via Command-Injection?)
|
||||||
|
- `pi.prompt()` oder Äquivalent — gibt's einen Weg Slash-Commands programmatisch zu dispatchen?
|
||||||
|
|
||||||
|
Output: ein Markdown-Dokument `docs/EXTENSION-API-AUDIT.md` das alle Features dieser Spec zu konkreter ExtensionAPI-Capability mapped. Das blockt nichts am Stream selbst (S-01-S-05) — das funktioniert ohne API-Erweiterung — aber alles unter S-07, S-08, Gruppe C, Gruppe T hängt daran.
|
||||||
|
|
||||||
|
Soll ich diesen Spike als ersten Punkt vor Phase 0 anlegen?
|
||||||
|
- **Q-3** — Slash-Command-Registry: ist die Liste über ExtensionAPI
|
||||||
|
abrufbar oder brauchen wir Hack?
|
||||||
|
joe: siehe Q2
|
||||||
|
|
||||||
|
claude: 👍 — gleicher Spike.
|
||||||
|
- **Q-4** — Marker-Detection (S-12): rein heuristisch reicht für iOS-B-04
|
||||||
|
und iOS-E-03? Oder Extension-Hook nötig?
|
||||||
|
joe: gar nicht rein als feature
|
||||||
|
|
||||||
|
claude: ✂️ — bestätigt. S-12, iOS-E-03, iOS-B-04 fallen alle damit.
|
||||||
|
- **Q-5** — Apple Developer Program ($99/Jahr) vs. AltStore / Sideloadly?
|
||||||
|
Für Solo-Use beides möglich.
|
||||||
|
joe: apple developer
|
||||||
|
|
||||||
|
claude: 👍 — $99/Jahr. Closed.
|
||||||
|
- **Q-6** — APNs: eigener Push-Server vs. simple background-fetch?
|
||||||
|
joe: bietet apple da keine lösung?
|
||||||
|
|
||||||
|
claude: Doch, **APNs** (Apple Push Notification service) ist Apples Lösung. Aber Apple stellt nur den Push-**Endpunkt** bereit (`api.push.apple.com`), nicht den Server der die Pushes auslöst — das musst du selbst betreiben.
|
||||||
|
|
||||||
|
In unserem Setup ist der "Push-Server" einfach der Sidecar selbst:
|
||||||
|
- Sidecar hält ein APNs-Auth-Token (JWT mit deinem Developer-Key, signed via Apple)
|
||||||
|
- Wenn Pi-State zu `awaiting-input` wechselt → Sidecar POSTet an APNs-Endpunkt mit der Device-Token (kommt vom iOS-Client beim Pairing) → Apple liefert die Notification ans iPhone aus.
|
||||||
|
- Keine zusätzliche Infrastruktur, kein Firebase / OneSignal / Push-Gateway dazwischen. Apples APNs ist HTTP/2 mit JWT, ca. 50 Zeilen Node.
|
||||||
|
|
||||||
|
Library: `node-apn` oder `@parse/node-apn`. Setup: APNs-Auth-Key generieren im Developer-Portal (gehört zu deinem Apple-Dev-Account, $99 Q-5), Key in Sidecar-Config, fertig.
|
||||||
|
|
||||||
|
Also: **kein eigener Push-**Server**, nur ein eigener Push-**Client** (= der Sidecar). Closed?
|
||||||
|
|
||||||
|
|
@ -0,0 +1,624 @@
|
||||||
|
# pi-remote — iOS Native App Spec (v3)
|
||||||
|
|
||||||
|
> **Status:** v3 nach ExtensionAPI-Audit.
|
||||||
|
> Audit-Ergebnis: [`EXTENSION-API-AUDIT.md`](./EXTENSION-API-AUDIT.md).
|
||||||
|
> Review-Verlauf v1: [`SPEC-ios-app-review-v1.md`](./SPEC-ios-app-review-v1.md).
|
||||||
|
>
|
||||||
|
> **Changelog v2 → v3:**
|
||||||
|
> - Audit hat S-07, S-08 und Tree-Read als out-of-the-box machbar bestätigt → von PENDING auf firm SHOULD.
|
||||||
|
> - **Tree-Navigation komplett aus iOS entfernt.** Gruppe T gestrichen. Begründung: Slash-Command-Dispatch ist in der ExtensionAPI blockiert, Workarounds (Hack oder Re-Implement) sind nicht den Aufwand wert. Tree-Navigation bleibt nativ in pi.
|
||||||
|
> - Spike-0a abgeschlossen, ist nun referenziertes Audit-Dokument.
|
||||||
|
> - **Q-A** geschlossen: pi-CLI `-p`/`--print` Mode funktioniert mit Haiku 4.5 in ~2s. S-09a hat jetzt konkrete CLI-Flags.
|
||||||
|
> - **Q-C** geschlossen: APNs-Setup-Details unter iOS-C-02. Sidecar routet pro Device-Token nach Sandbox (Xcode-Debug) vs. Production (TestFlight/Release).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Vision
|
||||||
|
|
||||||
|
Eine iOS-App, die laufende `pi`-Sessions **byte-genau spiegelt**, wie sie im
|
||||||
|
SSH-Terminal aussehen — und durch native iOS-Mittel (Touch, Sprache,
|
||||||
|
Notifications, System-Integration) die Bedienung dieser Terminal-Umgebung
|
||||||
|
unterwegs angenehm macht.
|
||||||
|
|
||||||
|
**Kein Hybrid-Rendering.** Was im Terminal ANSI ist, bleibt in der App ANSI.
|
||||||
|
Die App fügt eine *Augmentation-Schicht* hinzu, kein paralleles UI.
|
||||||
|
|
||||||
|
**Kernszenarien:**
|
||||||
|
|
||||||
|
1. **Long-Visit-Sync** — Eine Session läuft tagelang. Nutzer "besucht" sie
|
||||||
|
unregelmäßig vom Mac (SSH) und iPhone (App). Beide zeigen jederzeit
|
||||||
|
denselben State. Reattach ist instant.
|
||||||
|
2. **Session-Lifecycle vom Phone** — Sessions vom iPhone aus spawnen,
|
||||||
|
benennen, switchen und beenden — so easy wie native iOS-Tabs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Principles
|
||||||
|
|
||||||
|
- **P-1** — Der Terminal-Stream ist die einzige Wahrheit für Inhalt.
|
||||||
|
Strukturierte Events dienen ausschließlich Statusanzeigen,
|
||||||
|
Notifications und Meta-State (z.B. Tree-Navigation). Niemals
|
||||||
|
Re-Rendering von Stream-Inhalt.
|
||||||
|
- **P-2** — SSH-Erfahrung bleibt unverändert. Der Mac-Workflow ändert
|
||||||
|
sich durch dieses Projekt nicht spürbar.
|
||||||
|
- **P-3** — Reconnect ist die wichtigste Operation. Sichtbares Ergebnis
|
||||||
|
< 1s nach App-Wake.
|
||||||
|
- **P-4** — Touch-UX wird *zur Terminal-Bedienung* gebaut, nicht
|
||||||
|
*anstelle* von Terminal-Bedienung.
|
||||||
|
- **P-5** — Solo-Use. Multi-User, Sharing, Org-Features sind out of scope.
|
||||||
|
- **P-6** — Server-State wird in tmux gehalten, wo möglich. Kein
|
||||||
|
paralleler State-Store im Sidecar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────┐
|
||||||
|
│ Server │
|
||||||
|
│ │
|
||||||
|
│ pi (Ink TUI) ◄─► tmux session ◄─► SSH-Client (Mac) │
|
||||||
|
│ │ │
|
||||||
|
│ │ pipe-pane (raw bytes) │
|
||||||
|
│ │ send-keys │
|
||||||
|
│ ▼ │
|
||||||
|
│ pi-remote sidecar │
|
||||||
|
│ │ │
|
||||||
|
│ │ WebSocket (wss://) │
|
||||||
|
│ │ ├─ raw ANSI stream (binary) │
|
||||||
|
│ │ ├─ control (send-keys, JSON) │
|
||||||
|
│ │ └─ side-channel (state, JSON) │
|
||||||
|
└─────────────────────────┼────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ iOS App │
|
||||||
|
│ ┌────────────────────┐ │
|
||||||
|
│ │ SwiftTerm renderer │ │
|
||||||
|
│ └────────────────────┘ │
|
||||||
|
│ ┌────────────────────┐ │
|
||||||
|
│ │ Augmentation layer │ │
|
||||||
|
│ └────────────────────┘ │
|
||||||
|
└──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Komponenten:**
|
||||||
|
|
||||||
|
- **tmux** — Session-Persistenz, Multi-Client-Attach, Pane-Pipe, Send-Keys.
|
||||||
|
Sessions als stable IDs, Metadaten via tmux User-Options
|
||||||
|
(`@description`, `@project`, etc.).
|
||||||
|
- **Sidecar** (`pi-remote-control` erweitert) — Node-Prozess. Spawn /
|
||||||
|
Reattach tmux, exponiert WebSocket-API, hält Ringbuffer für Replay,
|
||||||
|
leitet ExtensionAPI-Metadaten als Side-Channel weiter.
|
||||||
|
- **iOS-App** — SwiftUI-Shell, SwiftTerm als Renderer-View, eigener
|
||||||
|
Augmentation-Layer.
|
||||||
|
|
||||||
|
**Datenfluss Stream:** `pi` → tmux pipe-pane → sidecar ringbuffer → WS
|
||||||
|
(binär, `permessage-deflate`) → iOS SwiftTerm.
|
||||||
|
|
||||||
|
**Datenfluss Input:** iOS → WS → sidecar → `tmux send-keys` → tmux pane →
|
||||||
|
pi stdin. Mac-SSH-Client sieht denselben Input.
|
||||||
|
|
||||||
|
**Konventionen:**
|
||||||
|
|
||||||
|
- Terminal-Size: tmux pane fixiert auf **120 × 40**. Client rendert mit
|
||||||
|
Pinch-Zoom auf die physische Display-Größe.
|
||||||
|
- WebSocket-Frames: binär = ANSI-Stream-Chunk; text = JSON-Control
|
||||||
|
(input, side-channel, snapshot-request).
|
||||||
|
- Alternate-Screen-Buffer wird vom Client erkannt und nicht in den
|
||||||
|
Scrollback-Cache aufgenommen.
|
||||||
|
- Terminal-Title-Sequences werden ignoriert.
|
||||||
|
- Mouse-Tracking-Sequenzen werden weitergeleitet, vom Client aber nicht
|
||||||
|
visualisiert (Touch-UX).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Server Features (Sidecar)
|
||||||
|
|
||||||
|
### S-01 — tmux launcher / attach
|
||||||
|
**MUST.** CLI `pi-remote attach [<session>]`. Existiert die Session,
|
||||||
|
reattach; sonst neu spawnen (`tmux new-session -d -s <name> 'pi'`) und
|
||||||
|
attach. Default-Session: `pi-main`. Per Projekt benennbar.
|
||||||
|
|
||||||
|
*Dependencies:* —
|
||||||
|
|
||||||
|
### S-02 — Raw ANSI WebSocket Stream
|
||||||
|
**MUST.** Endpoint `/stream/<session>`.
|
||||||
|
- Binäre Frames für ANSI-Bytes aus `tmux pipe-pane`.
|
||||||
|
- WebSocket-Extension `permessage-deflate` aktiv (3–5× Compression
|
||||||
|
typisch für ANSI).
|
||||||
|
- Eine Verbindung = ein Pane.
|
||||||
|
- Alternate-Screen-Sequenzen werden durchgereicht; Sidecar markiert sie
|
||||||
|
nicht separat (Client tracked).
|
||||||
|
|
||||||
|
*Dependencies:* S-01
|
||||||
|
|
||||||
|
### S-03 — Send-Keys-Endpoint
|
||||||
|
**MUST.** WS-Text-Frames als JSON: `{type:"keys", data:"..."}` für
|
||||||
|
literal-Text, oder `{type:"key", name:"escape"|"tab"|"up"|...}` für
|
||||||
|
Spezialtasten. Sidecar mapped auf `tmux send-keys` oder
|
||||||
|
`tmux send-keys -l`. Bracketed-Paste-Frames
|
||||||
|
(`{type:"paste", data:"..."}`) wrappen automatisch mit `\e[200~ ... \e[201~`.
|
||||||
|
|
||||||
|
*Dependencies:* S-01
|
||||||
|
|
||||||
|
### S-04 — Sequence-Cursor & Delta-Replay
|
||||||
|
**MUST.** Sidecar nummeriert Chunks ausgehend (monotone u64). Client
|
||||||
|
sendet bei Reconnect `{type:"resume", lastSeq:<n>}`. Server liefert ab
|
||||||
|
`lastSeq+1` weiter. Falls Lücke → Snapshot (S-05).
|
||||||
|
|
||||||
|
*Dependencies:* S-02
|
||||||
|
|
||||||
|
### S-05 — Snapshot-Endpoint
|
||||||
|
**MUST.** Wenn `lastSeq` außerhalb des Ringbuffers liegt:
|
||||||
|
`tmux capture-pane -p -e -S -10000` als Snapshot + neuen Start-Seq.
|
||||||
|
Snapshot wird komprimiert über WS geliefert.
|
||||||
|
|
||||||
|
*Dependencies:* S-01, S-04
|
||||||
|
|
||||||
|
### S-06 — Per-Session Disk Buffer
|
||||||
|
**SHOULD.** Sidecar persistiert ANSI-Stream pro Session in eine
|
||||||
|
einzelne Datei `/var/lib/pi-remote/buffer/<session>.log`.
|
||||||
|
|
||||||
|
- **Pro Session**: hartes Cap = **100MB** (configurable). Bei
|
||||||
|
Überlauf wird der Schreibvorgang gestoppt; existierender Inhalt
|
||||||
|
bleibt unangetastet. Neue Bytes ab dem Punkt fehlen im Buffer —
|
||||||
|
Snapshot via S-05 funktioniert weiterhin direkt aus tmux.
|
||||||
|
- **Global**: Hartes Total-Cap = **1GB** über alle Sessions; wenn
|
||||||
|
überschritten, schaltet der Sidecar Buffer-Schreiben global ab und
|
||||||
|
meldet `degraded` über S-13.
|
||||||
|
- **Idle-Cleanup**: Sessions ohne Output UND ohne Client seit > 30
|
||||||
|
Tagen → Buffer-Datei wird gelöscht (Session selbst nur falls
|
||||||
|
ebenfalls beendet).
|
||||||
|
- **Disk-Watchdog**: Bei freiem Platz < 1GB → Buffer-Schreiben global
|
||||||
|
aus, Health-Status `degraded`.
|
||||||
|
- Konfigurierbar via `~/.config/pi-remote/config.toml`.
|
||||||
|
|
||||||
|
*Dependencies:* S-02
|
||||||
|
|
||||||
|
### S-07 — State Side-Channel
|
||||||
|
**SHOULD.** Sidecar abonniert pi-ExtensionAPI-Events (`agent_start`,
|
||||||
|
`agent_end`, `tool_execution_start`, `tool_execution_end`,
|
||||||
|
`session_tree`, `session_compact`) und publiziert als JSON-Control-Frames:
|
||||||
|
```json
|
||||||
|
{"type":"state","value":"thinking"|"tool"|"idle"|"awaiting-input",
|
||||||
|
"tool":"Edit"|"Read"|...,"ts":1234567890}
|
||||||
|
```
|
||||||
|
`awaiting-input` wird abgeleitet aus `agent_end` + `ctx.isIdle()`,
|
||||||
|
da kein explizites Event existiert (siehe Audit §3.1).
|
||||||
|
|
||||||
|
*Dependencies:* S-01
|
||||||
|
|
||||||
|
### S-08 — Slash-Command-Registry
|
||||||
|
**SHOULD.** Endpoint `/sessions/<id>/commands` liefert JSON-Liste der
|
||||||
|
verfügbaren Slash-Commands inkl. Beschreibung und Argument-Schema via
|
||||||
|
`pi.getCommands()` (laut Audit out-of-the-box verfügbar). Dynamisch —
|
||||||
|
Extensions die Commands hinzufügen erscheinen automatisch.
|
||||||
|
|
||||||
|
*Dependencies:* S-01
|
||||||
|
|
||||||
|
### S-09 — Multi-Session-Management
|
||||||
|
**MUST.** Sidecar verwaltet mehrere tmux-Sessions parallel, alle robust
|
||||||
|
gegen Sidecar-Restart (tmux überlebt, Sidecar reattached beim Boot).
|
||||||
|
|
||||||
|
**Endpoints:**
|
||||||
|
- `GET /sessions` — Liste mit Metadaten (Name, Description, Created,
|
||||||
|
letzter Output, Connected-Clients, Pi-State).
|
||||||
|
- `POST /sessions` — neue Session spawnen, Body optional `{name, project}`.
|
||||||
|
- `PATCH /sessions/<id>` — umbenennen / Description ändern.
|
||||||
|
- `DELETE /sessions/<id>` — Session beenden (tmux kill-session + Buffer
|
||||||
|
optional löschen).
|
||||||
|
|
||||||
|
**State:** Ausschließlich in tmux gehalten. Sessions haben Namen
|
||||||
|
(stable ID), Metadaten via tmux User-Options:
|
||||||
|
- `@description` — Auto- oder manuell vergeben (siehe S-09a)
|
||||||
|
- `@project` — optional, vom User gesetzt
|
||||||
|
- `@created` — Timestamp
|
||||||
|
|
||||||
|
Kein eigener JSON-State-Store im Sidecar.
|
||||||
|
|
||||||
|
*Dependencies:* S-01
|
||||||
|
|
||||||
|
### S-09a — Auto-Naming via pi
|
||||||
|
**NICE.** Nach ~3 User-Messages in einer namenlosen Session triggert
|
||||||
|
der Sidecar einen One-Shot-Call via pi-CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pi -p --provider anthropic --model claude-haiku-4-5 \
|
||||||
|
--no-tools --no-context-files --no-extensions --no-session \
|
||||||
|
--thinking off \
|
||||||
|
"Give a 2-4 word title for: '<transcript>'. Reply with title only."
|
||||||
|
```
|
||||||
|
|
||||||
|
~2s Latenz, pi's eigene Anthropic-Auth wird verwendet — keine separaten
|
||||||
|
Credentials nötig. Ergebnis landet als `@description` in tmux.
|
||||||
|
Manuelles Umbenennen aus der App jederzeit möglich und überschreibt.
|
||||||
|
|
||||||
|
*Dependencies:* S-09
|
||||||
|
|
||||||
|
### S-10 — Pairing & Bearer-Token-Auth
|
||||||
|
**MUST.** CLI `pi-remote pair` generiert ein kurzlebiges (5min)
|
||||||
|
Pairing-Token und druckt einen QR-Code im Terminal (Unicode block
|
||||||
|
chars, via `qrcode-terminal`).
|
||||||
|
|
||||||
|
QR-Inhalt: `pi-remote://<host>:<port>?pair=<token>&fp=<sha256-cert-fp>`
|
||||||
|
|
||||||
|
iOS-App scannt → tauscht Pairing-Token gegen permanenten Bearer-Token,
|
||||||
|
pinnt den TLS-Fingerprint. Token im Keychain (iOS-G-01).
|
||||||
|
|
||||||
|
CLI:
|
||||||
|
- `pi-remote auth list` — alle Tokens
|
||||||
|
- `pi-remote auth revoke <name>` — Token widerrufen
|
||||||
|
- `pi-remote auth name <name>` — Token umbenennen (z.B. "jay's iPhone")
|
||||||
|
|
||||||
|
*Dependencies:* S-11
|
||||||
|
|
||||||
|
### S-11 — TLS via Self-Signed + Trust-on-First-Use
|
||||||
|
**MUST.** Sidecar generiert beim ersten Start ein selbstsigniertes
|
||||||
|
Cert (ED25519 oder RSA-2048), 10 Jahre gültig, persistent auf Disk.
|
||||||
|
SHA-256-Fingerprint wird in den QR aus S-10 mit aufgenommen. Client
|
||||||
|
pinnt den Fingerprint beim Pairing. Bei Cert-Rotation (z.B. neuer
|
||||||
|
Host) muss re-paired werden.
|
||||||
|
|
||||||
|
Kein Let's Encrypt, keine CA, kein Reverse-Proxy nötig.
|
||||||
|
|
||||||
|
*Dependencies:* —
|
||||||
|
|
||||||
|
### S-12 — Health & Metrics
|
||||||
|
**NICE.** `/health` Endpoint mit Session-Count, Buffer-Size,
|
||||||
|
Connected-Clients, Disk-Watchdog-Status. Für Monitoring und
|
||||||
|
Selbst-Debugging.
|
||||||
|
|
||||||
|
*Dependencies:* —
|
||||||
|
|
||||||
|
> S-13 (Tree-State Side-Channel) wurde in v3 gestrichen.
|
||||||
|
> Begründung: siehe Out-of-Scope und Changelog.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. iOS Client Features
|
||||||
|
|
||||||
|
### Gruppe A — Rendering & Stream
|
||||||
|
|
||||||
|
#### iOS-A-01 — SwiftTerm-Renderer
|
||||||
|
**MUST.** Vollwertiger ANSI-Terminal-View. Renderer-Setup mit
|
||||||
|
Truecolor, 120×40 fixed grid, configurable Font + Theme.
|
||||||
|
|
||||||
|
*Dependencies:* S-02
|
||||||
|
|
||||||
|
#### iOS-A-02 — Sequence-Cursor & Reconnect
|
||||||
|
**MUST.** Client speichert lokal pro Session den letzten `seq`. Bei
|
||||||
|
WS-Reconnect: schickt `{type:"resume", lastSeq}`, verarbeitet Delta
|
||||||
|
oder Snapshot.
|
||||||
|
|
||||||
|
*Dependencies:* S-04, S-05
|
||||||
|
|
||||||
|
#### iOS-A-03 — Stale-Frame-Display
|
||||||
|
**SHOULD.** Während Sync nach App-Wake: letzten Frame einfrieren,
|
||||||
|
"syncing…" als subtile Overlay-Pill anzeigen. Kein leerer Screen.
|
||||||
|
|
||||||
|
*Dependencies:* iOS-A-02
|
||||||
|
|
||||||
|
#### iOS-A-04 — Local Scrollback-Cache
|
||||||
|
**SHOULD.** App puffert empfangene Bytes lokal pro Session (rolling,
|
||||||
|
default 5MB) für Offline-Scrolling und Suche. Alternate-Screen-Inhalt
|
||||||
|
wird ausgenommen.
|
||||||
|
|
||||||
|
*Dependencies:* iOS-A-01
|
||||||
|
|
||||||
|
#### iOS-A-05 — Themes & Fonts
|
||||||
|
**SHOULD.** Eingebaute Themes:
|
||||||
|
- Default-Dark, Default-Light
|
||||||
|
- Solarized Light, Solarized Dark
|
||||||
|
- Monokai, Dracula
|
||||||
|
- Nord, Gruvbox Dark, Gruvbox Light
|
||||||
|
- Tomorrow Night
|
||||||
|
- GitHub Light, GitHub Dark
|
||||||
|
- System (folgt iOS Light/Dark Mode)
|
||||||
|
|
||||||
|
Eingebaute Fonts (gebundlet):
|
||||||
|
- JetBrains Mono (Default)
|
||||||
|
- Hack
|
||||||
|
- SF Mono
|
||||||
|
- Menlo
|
||||||
|
- Fira Code
|
||||||
|
- Cascadia Code
|
||||||
|
- IBM Plex Mono
|
||||||
|
- Monaspace Neon
|
||||||
|
|
||||||
|
Custom Themes editierbar, iCloud-Sync nur für Custom.
|
||||||
|
|
||||||
|
*Dependencies:* iOS-A-01
|
||||||
|
|
||||||
|
### Gruppe B — Input & Modifier
|
||||||
|
|
||||||
|
#### iOS-B-01 — Software-Keyboard, Direct-Passthrough
|
||||||
|
**MUST.** Standard-iOS-Keyboard. **Mode (b) direct-passthrough**:
|
||||||
|
jeder Tastendruck geht sofort als `send-keys`. Enter sendet `\r` an
|
||||||
|
pi → pi behandelt selbst. Kein App-eigenes Compose-Feld.
|
||||||
|
|
||||||
|
Shift+Enter (newline) via dediziertem `⇧↵`-Button in der Modifier-Bar
|
||||||
|
(siehe iOS-B-02).
|
||||||
|
|
||||||
|
*Dependencies:* S-03
|
||||||
|
|
||||||
|
#### iOS-B-02 — Modifier-Bar
|
||||||
|
**MUST.** Accessory-Bar über der Tastatur:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Ctrl] [Esc] [Tab] [←] [↑] [↓] [→] [⇧↵] [🎙] [📋]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Ctrl** ganz links — Sticky-Toggle: ein Tap → leuchtet → nächste
|
||||||
|
Taste wird als Ctrl+X gesendet. Beliebige Ctrl-Combos möglich
|
||||||
|
(Ctrl-C, Ctrl-D, Ctrl-L, Ctrl-U, ...).
|
||||||
|
- **Esc** — eigener Button, dauerhaft sichtbar.
|
||||||
|
- **Tab** — Autocomplete in pi.
|
||||||
|
- **Pfeiltasten** (Mitte) — History scrollen, in Menüs navigieren,
|
||||||
|
Input-Cursor bewegen.
|
||||||
|
- **`⇧↵`** — Shift+Enter, ein Tap = `\n` (newline in pi multi-line).
|
||||||
|
- **`🎙`** — Voice-Input (iOS-C-06).
|
||||||
|
- **`📋`** — Paste-Button ganz rechts (öffnet Paste-Sheet, iOS-B-08).
|
||||||
|
|
||||||
|
Bei knappem Platz (iPhone-Portrait) ist die Bar horizontal scrollbar.
|
||||||
|
|
||||||
|
*Dependencies:* iOS-B-01
|
||||||
|
|
||||||
|
#### iOS-B-03 — Long-Press-Repeat
|
||||||
|
**SHOULD.** Long-Press auf Pfeiltasten oder Backspace → repeat mit
|
||||||
|
Beschleunigung.
|
||||||
|
|
||||||
|
*Dependencies:* iOS-B-02
|
||||||
|
|
||||||
|
#### iOS-B-04 — Selection & Copy
|
||||||
|
**MUST.** Doppel-Tap → Wort. Tripel-Tap → Zeile. Long-Press + Drag →
|
||||||
|
Range. Native iOS-Copy-Menu.
|
||||||
|
|
||||||
|
*Dependencies:* iOS-A-01
|
||||||
|
|
||||||
|
#### iOS-B-05 — Pinch-Zoom Font
|
||||||
|
**SHOULD.** Pinch in der Terminal-View → Font-Size live.
|
||||||
|
|
||||||
|
*Dependencies:* iOS-A-01
|
||||||
|
|
||||||
|
#### iOS-B-06 — Hardware-Keyboard-Support
|
||||||
|
**SHOULD.** Externe iPad-Keyboards:
|
||||||
|
- Caps→Esc-Remap (optional)
|
||||||
|
- Modifier-Pass-Through (Cmd, Option, Ctrl)
|
||||||
|
- App-Shortcuts:
|
||||||
|
- Cmd-K — Clear
|
||||||
|
- Cmd-T — New session
|
||||||
|
- Cmd-1..9 — Session switch
|
||||||
|
- Cmd-F — Scrollback-Search
|
||||||
|
- Cmd-, — Settings
|
||||||
|
- Cmd-Shift-P — Slash-Command-Palette
|
||||||
|
|
||||||
|
*Dependencies:* iOS-B-01
|
||||||
|
|
||||||
|
#### iOS-B-07 — Reachability / One-Hand-Mode
|
||||||
|
**NICE.** iPhone-Querformat: Modifier-Bar gespiegelt für einhändige
|
||||||
|
Bedienung.
|
||||||
|
|
||||||
|
*Dependencies:* iOS-B-02
|
||||||
|
|
||||||
|
#### iOS-B-08 — Smart Paste (Confirm)
|
||||||
|
**SHOULD.** `📋`-Button in der Modifier-Bar zeigt Clipboard-Vorschau-
|
||||||
|
Chip ("📋 12 lines, 847 chars"). Tap → Sheet mit vollem Preview,
|
||||||
|
Insert / Cancel. Verhindert versehentliches Paste großer Blobs.
|
||||||
|
|
||||||
|
*Dependencies:* iOS-B-01
|
||||||
|
|
||||||
|
#### iOS-B-09 — Bracketed-Paste-Compliance
|
||||||
|
**SHOULD.** Client trackt im Stream `\e[?2004h` / `\e[?2004l`. Wenn
|
||||||
|
aktiv, wird bei Paste die `{type:"paste"}` Variante an S-03 geschickt,
|
||||||
|
sodass pi den ganzen Block als Paste erkennt.
|
||||||
|
|
||||||
|
*Dependencies:* S-03, iOS-B-08
|
||||||
|
|
||||||
|
### Gruppe C — Pi-aware Augmentation
|
||||||
|
|
||||||
|
#### iOS-C-01 — Status-Bar
|
||||||
|
**MUST.** Top-Bar zeigt: Connection-Status, Session-Name, Pi-State
|
||||||
|
(`● thinking` / `⏵ tool: Edit` / `▶ awaiting` / `⏸ idle`) basierend auf
|
||||||
|
S-07.
|
||||||
|
|
||||||
|
*Dependencies:* S-07
|
||||||
|
|
||||||
|
#### iOS-C-02 — Push-Notification bei Awaiting-Input
|
||||||
|
**MUST.** Wenn App im Background und Pi wechselt zu `awaiting-input`:
|
||||||
|
Push-Notification "Pi ist fertig · <session-name>". Tap → App öffnet in
|
||||||
|
der richtigen Session.
|
||||||
|
|
||||||
|
**APNs-Setup:**
|
||||||
|
- Apple Developer Portal: App-ID mit Push-Capability +
|
||||||
|
APNs Auth-Key (`.p8`, einmaliger Download). Notiere Team-ID und Key-ID.
|
||||||
|
- Dieselbe `.p8`-Datei funktioniert für Sandbox und Production.
|
||||||
|
- Sidecar-Config (neue Sektion in `~/.config/pi-remote/config.toml`):
|
||||||
|
```toml
|
||||||
|
[apns]
|
||||||
|
team_id = "..."
|
||||||
|
key_id = "..."
|
||||||
|
key_path = "/etc/pi-remote/AuthKey_<key-id>.p8"
|
||||||
|
bundle_id = "de.vpsj.pi-remote"
|
||||||
|
```
|
||||||
|
- **Environment-Routing pro Device:**
|
||||||
|
Xcode-Debug-Builds (lokales Testen via WiFi-Pair) registrieren ihre
|
||||||
|
Device-Tokens bei der Sandbox; TestFlight- und Release-Builds gehen
|
||||||
|
gegen Production. Beide gleichzeitig betreibbar, da derselbe Key
|
||||||
|
funktioniert. Sidecar speichert pro Device-Token ein `environment:
|
||||||
|
"sandbox" | "production"` Feld und routet beim Push entsprechend zu
|
||||||
|
`api.sandbox.push.apple.com` bzw. `api.push.apple.com`.
|
||||||
|
- **Pairing-Erweiterung (S-10):** iOS-App schickt beim Pairing
|
||||||
|
zusätzlich `{deviceToken, environment, deviceName}`. `environment`
|
||||||
|
bestimmt sich zur Compile-Time aus der Build-Config.
|
||||||
|
- **HTTP-Headers**:
|
||||||
|
- `apns-topic: <bundle-id>`
|
||||||
|
- `apns-push-type: alert` (required ab iOS 13)
|
||||||
|
- `apns-priority: 10`
|
||||||
|
- `apns-collapse-id: session-<id>` — verhindert Notification-Spam, neuere
|
||||||
|
State-Wechsel überschreiben ältere im Lock-Screen.
|
||||||
|
- **JWT:** ES256-signed mit `.p8`-Key, 1h gültig, im Sidecar für ~55min
|
||||||
|
cachen.
|
||||||
|
- **Auto-Cleanup:** APNs-Response `410 Gone` → Device-Token tot, Sidecar
|
||||||
|
löscht Mapping. App registriert beim nächsten Launch neu.
|
||||||
|
- **Library:** `@parse/node-apn` oder `node-apn-http2`.
|
||||||
|
|
||||||
|
*Dependencies:* S-07, S-10 (Pairing trägt Device-Token), APNs Auth-Key
|
||||||
|
|
||||||
|
#### iOS-C-03 — Haptic Feedback bei State-Wechseln
|
||||||
|
**NICE.** Subtile Vibration bei `thinking → idle` oder
|
||||||
|
`thinking → awaiting-input`.
|
||||||
|
|
||||||
|
*Dependencies:* S-07
|
||||||
|
|
||||||
|
#### iOS-C-04 — Slash-Command-Palette
|
||||||
|
**SHOULD.** **Long-Press auf die Modifier-Bar** → öffnet Sheet mit
|
||||||
|
nativer Liste aller Slash-Commands (aus S-08), Fuzzy-Search. Tap →
|
||||||
|
injiziert via `send-keys`. Commands mit Argumenten → Formular-View.
|
||||||
|
|
||||||
|
*Dependencies:* S-03, S-08
|
||||||
|
|
||||||
|
#### iOS-C-05 — Voice-to-Prompt
|
||||||
|
**NICE.** `🎙`-Button → Speech-Recognition (lokal, iOS native) →
|
||||||
|
editable Preview → Send.
|
||||||
|
|
||||||
|
*Dependencies:* iOS-B-01, Mic-Permission
|
||||||
|
|
||||||
|
### Gruppe D — Session & Navigation
|
||||||
|
|
||||||
|
#### iOS-D-01 — Session-Switcher (MUST)
|
||||||
|
**MUST.** Native iOS-Liste der verfügbaren Sessions (via S-09). Spawn /
|
||||||
|
Rename / Kill direkt aus dem Switcher.
|
||||||
|
|
||||||
|
*Dependencies:* S-09
|
||||||
|
|
||||||
|
#### iOS-D-01a — Background Pre-Connect
|
||||||
|
**SHOULD.** App connectet im Hintergrund zu allen bekannten Sessions
|
||||||
|
parallel, hält pro Session einen kleinen In-Memory-Stream-Buffer +
|
||||||
|
letzten Frame. Switch = Renderer wechselt instant.
|
||||||
|
|
||||||
|
Kosten: ein Socket pro Session, ein paar MB RAM. Akzeptabel solange
|
||||||
|
< ~10 aktive Sessions.
|
||||||
|
|
||||||
|
*Dependencies:* iOS-D-01
|
||||||
|
|
||||||
|
#### iOS-D-01b — Optimistic-Switch mit Stale-Frame
|
||||||
|
**SHOULD.** Swipe zu Session B → sofort gecachten Frame zeigen,
|
||||||
|
parallel live-Sync. Niemals leerer Screen, nur kurze "sync"-Pille.
|
||||||
|
|
||||||
|
*Dependencies:* iOS-D-01, iOS-D-01a
|
||||||
|
|
||||||
|
#### iOS-D-01c — Predictive Thumbnails
|
||||||
|
**SHOULD.** Im Session-Switcher zeigt jede Session eine kleine Live-
|
||||||
|
Mini-Vorschau (z.B. 40×12 Zeichen Snapshot via `tmux capture-pane`).
|
||||||
|
Beim Öffnen des Switchers werden die Thumbnails aktualisiert.
|
||||||
|
|
||||||
|
*Dependencies:* iOS-D-01
|
||||||
|
|
||||||
|
#### iOS-D-02 — Scrollback-Search
|
||||||
|
**SHOULD.** Such-Sheet sucht im lokalen Scrollback-Cache (iOS-A-04)
|
||||||
|
mit Highlight + Jump-to-Match. Auch offline nutzbar.
|
||||||
|
|
||||||
|
*Dependencies:* iOS-A-04
|
||||||
|
|
||||||
|
### Gruppe E — Background & Lifecycle
|
||||||
|
|
||||||
|
#### iOS-E-01 — WS-Keepalive
|
||||||
|
**MUST.** Ping/Pong-Frames im Foreground halten Connection. iOS
|
||||||
|
suspended WS gerne ohne Notification.
|
||||||
|
|
||||||
|
*Dependencies:* S-02
|
||||||
|
|
||||||
|
#### iOS-E-02 — Wake-up-Sync
|
||||||
|
**MUST.** App-Foreground-Event triggert sofort Reconnect + Delta-Pull.
|
||||||
|
Spinner falls > 200ms. Ziel < 1s sichtbarer Sync (P-3).
|
||||||
|
|
||||||
|
*Dependencies:* iOS-A-02
|
||||||
|
|
||||||
|
### Gruppe F — Security
|
||||||
|
|
||||||
|
#### iOS-F-01 — Token im Keychain
|
||||||
|
**MUST.** Bearer-Token aus S-10 wird im iOS Keychain gespeichert.
|
||||||
|
|
||||||
|
*Dependencies:* S-10
|
||||||
|
|
||||||
|
#### iOS-F-02 — Face-ID / Touch-ID Gate
|
||||||
|
**SHOULD.** Biometrie-Lock vorm Öffnen der App. **Opt-in via Setting,
|
||||||
|
default off.**
|
||||||
|
|
||||||
|
*Dependencies:* —
|
||||||
|
|
||||||
|
#### iOS-F-03 — TLS-Pinning via QR-Pairing
|
||||||
|
**MUST.** Beim Pairing (S-10) wird der Cert-Fingerprint gepinnt. Jede
|
||||||
|
künftige Connection verifiziert dagegen. Re-Pairing bei Cert-Rotation.
|
||||||
|
|
||||||
|
*Dependencies:* S-10, S-11
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Out of Scope (locked)
|
||||||
|
|
||||||
|
- **Rich Chat-Rendering.** Wir parsen den Stream nicht. ANSI bleibt ANSI.
|
||||||
|
- **Embedded Mini-Terminals / Block-Selection.** Es gibt einen Stream.
|
||||||
|
- **Multi-User-Sharing.** Solo.
|
||||||
|
- **Org/Team-Features, Cloud-Hosted-Service.** Selbstgehostet.
|
||||||
|
- **Inline-Image-Rendering** (iTerm2 protocol etc.). Eventuell später.
|
||||||
|
- **Eigenes Mosh.** tmux + WebSocket reicht.
|
||||||
|
- **Cross-platform-Clients (Android / Web).** Nur iOS nativ. Die
|
||||||
|
bestehende HTML-UI bleibt als separates Artefakt.
|
||||||
|
- **Silent-Push-Background-Wake.** iOS coalesced unzuverlässig; manueller
|
||||||
|
Wake-up-Sync (iOS-E-02) reicht.
|
||||||
|
- **Marker-Detection / Jump-to-Marker.** Pi liefert keine zuverlässigen
|
||||||
|
Marker.
|
||||||
|
- **Tap-to-Cursor.** Ohne Marker-Detection nicht robust machbar.
|
||||||
|
- **Eigener Sidecar-State-Store.** State lebt in tmux (P-6).
|
||||||
|
- **Disk-Buffer-Rotation.** Ein Cap, drüber hinaus = Daten verloren.
|
||||||
|
Snapshot funktioniert weiter.
|
||||||
|
- **Bookmarks / manuelle Marker.** Pi-interne Tools sollen das übernehmen.
|
||||||
|
- **Snippet-Library** (Prompts mit Variablen). Pi-interne Tools.
|
||||||
|
- **Tree-View / Tree-Navigation in iOS** (jeglicher Form, read-only
|
||||||
|
oder interaktiv). Tree wird ausschließlich nativ in pi bedient. Audit
|
||||||
|
hat gezeigt: Slash-Command-Dispatch ist in der ExtensionAPI nicht sauber
|
||||||
|
zugänglich; Workarounds sind die Komplexität nicht wert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. ExtensionAPI-Audit (abgeschlossen)
|
||||||
|
|
||||||
|
Das Audit wurde durchgeführt; Ergebnis liegt in
|
||||||
|
[`EXTENSION-API-AUDIT.md`](./EXTENSION-API-AUDIT.md).
|
||||||
|
|
||||||
|
**Kernergebnisse:**
|
||||||
|
- S-07, S-08, sowie Tree-**Read** sind out-of-the-box machbar.
|
||||||
|
- Tree-**Write** (Slash-Command-Dispatch wie `/fork`, `/checkout`,
|
||||||
|
`/new`) ist nicht sauber zugänglich → Gruppe T gestrichen (siehe
|
||||||
|
Out-of-Scope).
|
||||||
|
- Tool-Call-Daten sind vollständig verfügbar (Name, Args, Result).
|
||||||
|
|
||||||
|
Kein weiterer Spike vor Phase 1 nötig.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Phasen
|
||||||
|
|
||||||
|
> Phase-Aufwände bewusst weggelassen (sind in v1 gestrichen worden).
|
||||||
|
|
||||||
|
- **Spike-0 — Stream-PoC** — `pi-remote-control` um tmux pipe-pane +
|
||||||
|
WS-Stream erweitern. Verifizieren dass pi sauber in tmux läuft (kein
|
||||||
|
Crash, Alternate-Screen sauber, kein Latency-Problem).
|
||||||
|
- **Phase 1 — Sidecar production-ready** — S-01 bis S-12 (alle), S-09a
|
||||||
|
optional.
|
||||||
|
- **Phase 2 — iOS-App MVP** — Renderer (Gruppe A), Input (Gruppe B
|
||||||
|
ohne Hardware-KB), Status-Bar (iOS-C-01), Session-Switcher
|
||||||
|
(iOS-D-01 + a/b), Reconnect-Lifecycle (Gruppe E), Auth (Gruppe F).
|
||||||
|
- **Phase 3 — iOS-App Augmentation** — Slash-Palette (iOS-C-04),
|
||||||
|
Voice (iOS-C-05), Thumbnails (iOS-D-01c), Scrollback-Search
|
||||||
|
(iOS-D-02), Hardware-KB (iOS-B-06).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Offene Punkte
|
||||||
|
|
||||||
|
Keine offenen Punkte mehr in v3. Q-A und Q-C wurden in dieser Version
|
||||||
|
geschlossen (siehe S-09a bzw. iOS-C-02).
|
||||||
|
|
||||||
Loading…
Reference in New Issue