pi-remote-control 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/docs/adr/0001-package-extension-as-control-shim.md +19 -0
- package/docs/adr/0002-use-sqlite-for-daemon-state.md +19 -0
- package/docs/adr/0003-use-lock-file-as-process-state.md +19 -0
- package/docs/adr/0004-allow-loopback-pair-code-without-token.md +19 -0
- package/docs/adr/0005-defer-os-service-installation.md +19 -0
- package/docs/adr/0006-use-tui-activated-remote-control-sessions.md +24 -0
- package/docs/adr/0007-require-tui-originated-pairing.md +19 -0
- package/docs/adr/0008-use-qr-pairing-links.md +21 -0
- package/docs/adr/0009-rename-package-to-remote-control.md +19 -0
- package/docs/adr/0010-clean-stale-lock-on-status.md +19 -0
- package/docs/adr/0011-use-loopback-tui-control.md +19 -0
- package/docs/adr/0012-use-paginated-session-transcript-loading.md +37 -0
- package/docs/adr/0013-require-manual-reactivation-after-tui-entry.md +31 -0
- package/docs/adr/0014-read-transcripts-from-session-files.md +33 -0
- package/docs/adr/0015-normalize-transcript-messages-and-stream-events.md +35 -0
- package/docs/adr/0016-expose-turn-lifecycle-events.md +31 -0
- package/docs/adr/0017-bound-initial-websocket-session-state.md +31 -0
- package/docs/adr/0018-reregister-active-tui-session-on-heartbeat-miss.md +33 -0
- package/docs/adr/0019-display-only-pairing-qr-and-expiry.md +25 -0
- package/docs/adr/0020-expose-session-status-snapshots.md +31 -0
- package/docs/adr/0021-support-remote-compact-action.md +31 -0
- package/docs/adr/0022-rename-session-status-to-runtime-status.md +27 -0
- package/docs/adr/0023-return-remote-compact-results.md +29 -0
- package/docs/architecture.md +96 -0
- package/docs/data-model.md +284 -0
- package/docs/interfaces.md +470 -0
- package/package.json +37 -0
- package/scripts/http-smoke-test.sh +100 -0
- package/src/active-session-registry.ts +205 -0
- package/src/auth/pairing.ts +30 -0
- package/src/auth/tokens.ts +30 -0
- package/src/cli-runner.cjs +15 -0
- package/src/cli.ts +254 -0
- package/src/config.ts +26 -0
- package/src/extension/index.ts +422 -0
- package/src/index.ts +16 -0
- package/src/lock.ts +26 -0
- package/src/pairing-link.ts +15 -0
- package/src/paths.ts +21 -0
- package/src/persistence/daemon-store.ts +56 -0
- package/src/persistence/schema.ts +21 -0
- package/src/qr.ts +23 -0
- package/src/runtime-status.ts +116 -0
- package/src/server/http.ts +529 -0
- package/src/session-index.ts +9 -0
- package/src/session-transcript.ts +34 -0
- package/src/transcript-message.ts +76 -0
- package/src/transcript-pagination.ts +68 -0
- package/src/transcript-preview.ts +102 -0
- package/src/transcript-stream.ts +89 -0
- package/src/types.ts +116 -0
- package/tests/active-session-registry.test.ts +170 -0
- package/tests/auth.test.ts +18 -0
- package/tests/cli.test.ts +361 -0
- package/tests/config.test.ts +35 -0
- package/tests/daemon-store.test.ts +54 -0
- package/tests/extension.test.ts +617 -0
- package/tests/lock.test.ts +36 -0
- package/tests/pairing-link.test.ts +26 -0
- package/tests/pairing.test.ts +26 -0
- package/tests/paths.test.ts +29 -0
- package/tests/qr.test.ts +25 -0
- package/tests/schema.test.ts +18 -0
- package/tests/server-http.test.ts +932 -0
- package/tests/session-index.test.ts +10 -0
- package/tests/session-transcript.test.ts +75 -0
- package/tests/transcript-pagination.test.ts +54 -0
- package/tests/transcript-preview.test.ts +64 -0
- package/tests/transcript-stream.test.ts +103 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Return remote compact results
|
|
2
|
+
|
|
3
|
+
## Title
|
|
4
|
+
|
|
5
|
+
Return remote compact results
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Accepted
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
ADR 0021 added `/compact` as an explicit allowlisted remote action. The current request path only tells iOS that the daemon accepted the command for delivery to the owning TUI extension. It does not tell iOS whether Pi compaction eventually succeeded, what summary was produced, or why compaction failed.
|
|
14
|
+
|
|
15
|
+
Pi's extension API triggers compaction asynchronously through the live TUI context and supports completion and error callbacks.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
Return a `requestId` from `POST /v1/sessions/{sessionId}/compact` and report the asynchronous outcome on the session WebSocket as `remote_compact_result`.
|
|
20
|
+
|
|
21
|
+
The TUI extension handles `remote_compact` by calling `ctx.compact()` with completion and error callbacks. On success, it posts `{ "type": "remote_compact_result", "requestId": "...", "ok": true, "summary": "...", "firstKeptEntryId": "...", "tokensBefore": 12345 }` to the daemon. On failure, it posts `{ "type": "remote_compact_result", "requestId": "...", "ok": false, "message": "..." }`.
|
|
22
|
+
|
|
23
|
+
The daemon forwards this result to iOS WebSocket subscribers for the same session. The result is live stream state, not durable daemon state and not part of HTTP session snapshots.
|
|
24
|
+
|
|
25
|
+
## Consequences
|
|
26
|
+
|
|
27
|
+
The iOS app can correlate compact completion with the original request and show success summaries or failure messages.
|
|
28
|
+
|
|
29
|
+
Clients that need compact results must keep the session WebSocket open after the HTTP compact request is accepted.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`pi-remote-control` is a long-lived local relay service that exposes authenticated, Tailscale-reachable remote control for Pi TUI sessions that the user explicitly enables. It is distributed as a Pi package, but the daemon process itself is not a Pi extension runtime.
|
|
6
|
+
|
|
7
|
+
## Pi package shape
|
|
8
|
+
|
|
9
|
+
The package contains two runtime surfaces:
|
|
10
|
+
|
|
11
|
+
- Daemon binary: the long-lived HTTP/WebSocket service used by the iOS app and the Pi TUI extension.
|
|
12
|
+
- Pi extension: the TUI-facing control surface that registers remote-control commands, owns the active Pi session integration, and forwards session events to the daemon.
|
|
13
|
+
|
|
14
|
+
The extension must not host the daemon server in-process. Pi extensions are loaded per Pi process and are rebound during session replacement flows, so a server started directly from extension load would be tied to the wrong lifecycle and could be attempted once per Pi session/process.
|
|
15
|
+
|
|
16
|
+
## Process lifecycle
|
|
17
|
+
|
|
18
|
+
The daemon is started by one of these explicit singleton mechanisms:
|
|
19
|
+
|
|
20
|
+
1. Manual CLI:
|
|
21
|
+
- `pi-remote-control start`.
|
|
22
|
+
2. Pi extension commands:
|
|
23
|
+
- `/remote-control` starts the daemon if needed before enabling the current TUI session.
|
|
24
|
+
- `/remote-control-pair` starts the daemon if needed before creating a pair code.
|
|
25
|
+
|
|
26
|
+
OS service installation is intentionally deferred for the MVP.
|
|
27
|
+
|
|
28
|
+
The extension may perform a cheap health check when a command runs, but it must not auto-start the daemon during extension load. Daemon startup goes through singleton lock acquisition and returns immediately when an existing daemon is healthy.
|
|
29
|
+
|
|
30
|
+
## Singleton ownership
|
|
31
|
+
|
|
32
|
+
Only the daemon process owns the network listener, pairing state, device tokens, active TUI session registry, and iOS WebSocket fanout. Singleton enforcement uses `daemon.lock` under the daemon state directory. The lock is created atomically during startup and contains the daemon PID for `status` and `stop`.
|
|
33
|
+
|
|
34
|
+
If another Pi process loads the extension, it sees the existing daemon and may register its own TUI session only after the user runs `/remote-control` in that process.
|
|
35
|
+
|
|
36
|
+
## Pi extension responsibilities
|
|
37
|
+
|
|
38
|
+
The extension responsibilities are session-aware:
|
|
39
|
+
|
|
40
|
+
- Register `/remote-control` as a no-argument toggle for the current TUI session.
|
|
41
|
+
- Register `/remote-control-pair` as the only pair-code creation command and render its pairing link as a QR code with expiration time.
|
|
42
|
+
- Start the daemon on demand when either command needs it.
|
|
43
|
+
- When `/remote-control` enables a session, open a control channel to the daemon, register current session metadata, and keep the registration fresh with heartbeats.
|
|
44
|
+
- When a locally active session heartbeat finds that the daemon no longer has the registration, re-register the current TUI session; if re-registration fails, clear local active state and notify the user.
|
|
45
|
+
- When `/remote-control` disables a session or the TUI session shuts down, unregister it; if shutdown cleanup is missed, the daemon expires the registration after the TUI PID exits or heartbeats stop.
|
|
46
|
+
- Forward Pi turn, message, assistant streaming, tool execution, queue, status, and lifecycle events to the daemon while remote control is active. These TUI-to-daemon events are package-internal inputs for daemon normalization.
|
|
47
|
+
- Compute structured runtime-status snapshots from the live TUI context and send them to the daemon when status inputs change.
|
|
48
|
+
- Receive daemon-forwarded prompt, abort, and compact commands and apply them to the current live TUI runtime through Pi extension APIs.
|
|
49
|
+
- Report asynchronous compact success or failure results back to the daemon for iOS WebSocket subscribers.
|
|
50
|
+
|
|
51
|
+
The extension owns live Pi session control. It does not expose a network listener to iOS.
|
|
52
|
+
|
|
53
|
+
## Daemon responsibilities
|
|
54
|
+
|
|
55
|
+
The daemon responsibilities are independent of Pi SDK session ownership:
|
|
56
|
+
|
|
57
|
+
- Serve the HTTP/WebSocket API documented in `docs/interfaces.md`.
|
|
58
|
+
- Enforce device token authentication for iOS requests and non-loopback TUI control requests.
|
|
59
|
+
- Persist pairing codes, paired device token hashes, and daemon metadata.
|
|
60
|
+
- Track currently activated TUI sessions and group them into projects for iOS display.
|
|
61
|
+
- Relay prompt, abort, and compact requests from iOS to the TUI extension that owns the target session.
|
|
62
|
+
- Forward TUI-reported compact results to subscribed iOS clients.
|
|
63
|
+
- Store the latest TUI-reported runtime-status snapshot for each active session and include it in session state sent to iOS.
|
|
64
|
+
- Serve bounded recent transcript snapshots and older transcript pages for active sessions by reading Pi session JSONL files and normalizing entries into public `TranscriptMessage` values.
|
|
65
|
+
- Normalize live TUI Pi events into public transcript stream events for subscribed iOS WebSocket clients without using the stream for full historical transcript payloads.
|
|
66
|
+
- Broadcast session updates to subscribed iOS WebSocket clients.
|
|
67
|
+
|
|
68
|
+
The daemon does not use Pi SDK or RPC to discover, open, prompt, stream, or abort sessions in the MVP.
|
|
69
|
+
|
|
70
|
+
## Session runtime model
|
|
71
|
+
|
|
72
|
+
A live session controller is represented by a TUI extension control channel, not a daemon-created Pi runtime. The control channel is the authority for one remote-control-enabled TUI session. It also provides runtime-status snapshots for model, usage, cost, and context information. If the channel closes, the owning TUI PID exits, or heartbeats stop, the daemon marks the session inactive, removes it from project/session listings, and notifies iOS subscribers. If the same TUI process still has local remote control active and later observes the missing registration through heartbeat polling, it re-registers the session.
|
|
73
|
+
|
|
74
|
+
Multiple TUI processes may enable remote control at the same time. Each active session has one owning TUI control channel. The daemon rejects prompt or abort requests for sessions without an active owner.
|
|
75
|
+
|
|
76
|
+
## Persistence model
|
|
77
|
+
|
|
78
|
+
The daemon binds the configured remote-facing address and, for specific non-loopback bind addresses, an additional `127.0.0.1` listener on the same port for local TUI control. The extension uses the loopback listener by default; iOS uses the configured advertised URL.
|
|
79
|
+
|
|
80
|
+
Session detail reads are bounded and derive transcript history from the active session's Pi JSONL session file: the daemon returns a recent transcript window first, then serves older transcript pages on request. Session detail reads and initial WebSocket state also include the latest TUI-reported `RuntimeStatus` snapshot when available. HTTP transcript reads and WebSocket live updates expose the same public `TranscriptMessage` shape. WebSocket streams are for normalized live updates, status updates, and asynchronous remote-action results; they must not carry unbounded session history. The initial WebSocket `session_state` is further bounded to a small recent window with oversized transcript payloads truncated to previews.
|
|
81
|
+
|
|
82
|
+
The daemon stores its own durable state in a daemon state directory, defaulting to `~/.pi/remote-control` and overridable with `PI_REMOTE_CONTROL_DIR`.
|
|
83
|
+
|
|
84
|
+
Durable daemon-owned files:
|
|
85
|
+
|
|
86
|
+
- `config.json`: daemon configuration such as bind address and advertised base URL.
|
|
87
|
+
- `daemon.sqlite`: SQLite database for paired devices, token hashes, pairing codes, and metadata.
|
|
88
|
+
- `daemon.lock`: singleton lock file containing the daemon PID.
|
|
89
|
+
|
|
90
|
+
Active TUI sessions are process state, not durable daemon state. Pi session transcripts remain in Pi's own JSONL session files under Pi's session directory. The daemon reads those files for HTTP transcript snapshots and pages instead of maintaining a duplicate completed-transcript snapshot in memory.
|
|
91
|
+
|
|
92
|
+
## Installation model
|
|
93
|
+
|
|
94
|
+
The package can be installed with Pi package installation, for example from a local path, git source, or npm source. The package manifest exposes the extension through the `pi.extensions` field and the daemon binary through the normal package binary entry.
|
|
95
|
+
|
|
96
|
+
Installing the Pi package makes Pi aware of `/remote-control` and `/remote-control-pair`; it does not by itself imply that the daemon process is running or that any TUI session is remotely visible. Pairing QR codes require an advertised base URL that is reachable from iOS, such as a Tailscale HTTPS or HTTP URL.
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Data model
|
|
2
|
+
|
|
3
|
+
## Persistent storage
|
|
4
|
+
|
|
5
|
+
The daemon state directory defaults to `~/.pi/remote-control` and can be overridden with `PI_REMOTE_CONTROL_DIR`.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
~/.pi/remote-control/
|
|
9
|
+
├── config.json
|
|
10
|
+
├── daemon.sqlite
|
|
11
|
+
└── daemon.lock
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The directory must be created with owner-only permissions. Database and token-bearing files must not be world-readable. `daemon.lock` is created atomically and contains the daemon PID for singleton enforcement, `status`, and `stop`.
|
|
15
|
+
|
|
16
|
+
## SQLite schema
|
|
17
|
+
|
|
18
|
+
`daemon.sqlite` is the daemon-owned source of truth for remote access state. Active TUI sessions are process state and Pi session JSONL files remain the source of truth for transcripts.
|
|
19
|
+
|
|
20
|
+
```sql
|
|
21
|
+
create table meta (
|
|
22
|
+
key text primary key,
|
|
23
|
+
value text not null
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
create table devices (
|
|
27
|
+
id text primary key,
|
|
28
|
+
name text not null,
|
|
29
|
+
token_hash text not null unique,
|
|
30
|
+
created_at text not null,
|
|
31
|
+
last_seen_at text,
|
|
32
|
+
revoked_at text
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
create table pairing_codes (
|
|
36
|
+
id text primary key,
|
|
37
|
+
code_hash text not null unique,
|
|
38
|
+
created_at text not null,
|
|
39
|
+
expires_at text not null,
|
|
40
|
+
consumed_at text
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Pairing codes and device token hashes are durable. Active session registry entries are rebuilt by currently running Pi TUI extensions after the user enables `/remote-control`. Completed transcript history is read from Pi session JSONL files, not stored in daemon SQLite or active registry memory.
|
|
45
|
+
|
|
46
|
+
## Config file
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
type DaemonConfig = {
|
|
50
|
+
bindAddress: string;
|
|
51
|
+
advertisedBaseUrl?: string;
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`config.json` is human-editable daemon configuration. It does not contain allowed project roots for the MVP because project visibility is derived from active remote-control TUI sessions. `bindAddress` is the remote-facing listener. When it is a specific non-loopback address, the daemon also listens on `127.0.0.1` on the same port for local TUI control. `advertisedBaseUrl` is the URL encoded into pairing QR codes and used by iOS for future daemon calls; it must not be a loopback or wildcard address when pairing a separate device.
|
|
56
|
+
|
|
57
|
+
## Daemon process state
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
type DaemonState = {
|
|
61
|
+
pid: number;
|
|
62
|
+
startedAt: string;
|
|
63
|
+
version: string;
|
|
64
|
+
bindAddress: string;
|
|
65
|
+
stateDir: string;
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Stored process state is used for health checks and singleton detection. It is not session history.
|
|
70
|
+
|
|
71
|
+
## Pairing code
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
type PairingCode = {
|
|
75
|
+
codeHash: string;
|
|
76
|
+
expiresAt: string;
|
|
77
|
+
createdAt: string;
|
|
78
|
+
consumedAt?: string;
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Pairing codes are short-lived. The daemon stores code hashes, not raw codes. Raw pair codes are created only for `/remote-control-pair` and are encoded into the Pi TUI QR code.
|
|
83
|
+
|
|
84
|
+
## Pairing link
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
type PairingLink = {
|
|
88
|
+
scheme: "pi-remote";
|
|
89
|
+
action: "pair";
|
|
90
|
+
baseUrl: string;
|
|
91
|
+
code: string;
|
|
92
|
+
expiresAt: string;
|
|
93
|
+
};
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The raw link is encoded as a `pi-remote://pair?...` URL and rendered as a QR code by `/remote-control-pair`. The raw link is not printed as a separate TUI text line.
|
|
97
|
+
|
|
98
|
+
## Paired device
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
type PairedDevice = {
|
|
102
|
+
id: string;
|
|
103
|
+
name: string;
|
|
104
|
+
tokenHash: string;
|
|
105
|
+
createdAt: string;
|
|
106
|
+
lastSeenAt?: string;
|
|
107
|
+
revokedAt?: string;
|
|
108
|
+
};
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The iOS app stores the bearer token in Keychain. The daemon stores only token hashes.
|
|
112
|
+
|
|
113
|
+
## Active project
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
type ActiveProject = {
|
|
117
|
+
id: string;
|
|
118
|
+
name: string;
|
|
119
|
+
path: string;
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
An active project exists while at least one registered TUI session for that project has remote control enabled.
|
|
124
|
+
|
|
125
|
+
## Active TUI session
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
type ActiveTuiSession = {
|
|
129
|
+
id: string;
|
|
130
|
+
piSessionId: string;
|
|
131
|
+
projectId: string;
|
|
132
|
+
sessionFile: string;
|
|
133
|
+
name?: string;
|
|
134
|
+
pid: number;
|
|
135
|
+
messageCount: number;
|
|
136
|
+
isStreaming: boolean;
|
|
137
|
+
runtimeStatus?: RuntimeStatus;
|
|
138
|
+
registeredAt: string;
|
|
139
|
+
lastSeenAt: string;
|
|
140
|
+
};
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
An active TUI session is owned by one Pi extension control channel. It is removed when `/remote-control` disables it, the TUI session shuts down, or the control channel closes. If the daemon removes it because heartbeats stopped but the same TUI process still has local remote-control state active, the TUI extension can recreate the active session by re-registering on the next heartbeat miss. Its `sessionFile` points to the Pi JSONL transcript used for HTTP transcript reads. Its `runtimeStatus` is the latest structured runtime-status snapshot reported by the owning TUI extension.
|
|
144
|
+
|
|
145
|
+
## Runtime status
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
type RuntimeStatus = {
|
|
149
|
+
model: null | {
|
|
150
|
+
provider: string;
|
|
151
|
+
id: string;
|
|
152
|
+
name?: string;
|
|
153
|
+
contextWindow?: number;
|
|
154
|
+
maxTokens?: number;
|
|
155
|
+
reasoning?: boolean;
|
|
156
|
+
};
|
|
157
|
+
thinkingLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | null;
|
|
158
|
+
usage: {
|
|
159
|
+
input: number;
|
|
160
|
+
output: number;
|
|
161
|
+
cacheRead: number;
|
|
162
|
+
cacheWrite: number;
|
|
163
|
+
cost: {
|
|
164
|
+
input: number;
|
|
165
|
+
output: number;
|
|
166
|
+
cacheRead: number;
|
|
167
|
+
cacheWrite: number;
|
|
168
|
+
total: number;
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
context: null | {
|
|
172
|
+
tokens: number | null;
|
|
173
|
+
contextWindow: number;
|
|
174
|
+
percent: number | null;
|
|
175
|
+
};
|
|
176
|
+
updatedAt: string;
|
|
177
|
+
};
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`RuntimeStatus` is computed by the live TUI extension from Pi extension context. It is process state in the daemon, not durable storage. `context.tokens` and `context.percent` may be `null` when Pi reports current context usage as unknown.
|
|
181
|
+
|
|
182
|
+
## Session snapshot
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
type SessionSnapshot = {
|
|
186
|
+
session: ActiveTuiSession;
|
|
187
|
+
messages: TranscriptMessage[];
|
|
188
|
+
olderMessagesCursor?: string;
|
|
189
|
+
hasOlderMessages: boolean;
|
|
190
|
+
tools: ToolCallStatus[];
|
|
191
|
+
isStreaming: boolean;
|
|
192
|
+
pendingMessageCount: number;
|
|
193
|
+
runtimeStatus: RuntimeStatus | null;
|
|
194
|
+
};
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The daemon returns a bounded recent-message snapshot for active sessions so newly connected iOS clients can render persisted state before incremental events arrive. Snapshot messages are derived from the session's Pi JSONL `sessionFile` at request time and normalized into `TranscriptMessage` values. The snapshot includes the latest TUI-reported `RuntimeStatus` when available. Older transcript history is loaded through explicit transcript page requests.
|
|
198
|
+
|
|
199
|
+
## Transcript message
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
type TranscriptMessage = {
|
|
203
|
+
id: string;
|
|
204
|
+
role: "user" | "assistant" | "toolResult" | "system";
|
|
205
|
+
content: TranscriptContentBlock[];
|
|
206
|
+
text: string;
|
|
207
|
+
textTruncated?: boolean;
|
|
208
|
+
textOriginalBytes?: number;
|
|
209
|
+
createdAt: string;
|
|
210
|
+
toolCallId?: string;
|
|
211
|
+
toolName?: string;
|
|
212
|
+
isError?: boolean;
|
|
213
|
+
isStreaming: boolean;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
type TranscriptContentBlock =
|
|
217
|
+
| { type: "text"; text: string; truncated?: boolean; originalBytes?: number }
|
|
218
|
+
| { type: "thinking"; thinking: string; truncated?: boolean; originalBytes?: number }
|
|
219
|
+
| { type: "toolCall"; id: string; name: string; arguments: unknown; argumentsTruncated?: boolean; argumentsOriginalBytes?: number }
|
|
220
|
+
| { type: "image"; data: string; mimeType: string; truncated?: boolean; originalBytes?: number };
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`TranscriptMessage` is the public transcript shape used by both HTTP transcript reads and WebSocket live updates. `content` preserves structured Pi message blocks. `text` is a display summary derived from text-like blocks and kept for clients that need a simple preview. Tool-result messages carry `toolCallId`, `toolName`, and `isError` when present. Truncation metadata is used when the daemon intentionally sends a preview instead of the full string payload.
|
|
224
|
+
|
|
225
|
+
## Transcript page
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
type TranscriptPage = {
|
|
229
|
+
messages: TranscriptMessage[];
|
|
230
|
+
olderMessagesCursor?: string;
|
|
231
|
+
hasOlderMessages: boolean;
|
|
232
|
+
};
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Transcript pages contain bounded message windows ordered oldest-to-newest by `createdAt` and are derived from the session's Pi JSONL `sessionFile` at request time. Cursor values are generated from the oldest loaded message's `createdAt` timestamp, are daemon-encoded, and are opaque to clients. Clients merge pages and live updates by de-duplicating `TranscriptMessage.id`.
|
|
236
|
+
|
|
237
|
+
## Transcript stream event
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
type TranscriptStreamEvent =
|
|
241
|
+
| { type: "session_state"; state: SessionSnapshot }
|
|
242
|
+
| { type: "turn_start"; turnIndex: number; createdAt?: string }
|
|
243
|
+
| { type: "turn_end"; turnIndex: number }
|
|
244
|
+
| { type: "transcript_message_start"; message: TranscriptMessage }
|
|
245
|
+
| { type: "transcript_message_patch"; messageId: string; contentIndex?: number; patch: TranscriptMessagePatch }
|
|
246
|
+
| { type: "transcript_message_end"; message: TranscriptMessage }
|
|
247
|
+
| { type: "tool_execution_start"; toolCallId: string; toolName: string; args: unknown }
|
|
248
|
+
| { type: "tool_execution_update"; toolCallId: string; toolName: string; partialResult: unknown }
|
|
249
|
+
| { type: "tool_execution_end"; toolCallId: string; toolName: string; result?: unknown; isError: boolean }
|
|
250
|
+
| { type: "runtime_status"; status: RuntimeStatus }
|
|
251
|
+
| RemoteCompactResultEvent
|
|
252
|
+
| { type: "session_closed" }
|
|
253
|
+
| { type: "error"; error: string };
|
|
254
|
+
|
|
255
|
+
type TranscriptMessagePatch =
|
|
256
|
+
| { type: "text_delta"; delta: string }
|
|
257
|
+
| { type: "thinking_delta"; delta: string }
|
|
258
|
+
| { type: "toolCall"; toolCall: { type: "toolCall"; id: string; name: string; arguments: unknown } }
|
|
259
|
+
| { type: "replace"; message: TranscriptMessage };
|
|
260
|
+
|
|
261
|
+
type RemoteCompactResultEvent =
|
|
262
|
+
| { type: "remote_compact_result"; requestId: string; ok: true; summary: string; firstKeptEntryId: string; tokensBefore: number }
|
|
263
|
+
| { type: "remote_compact_result"; requestId: string; ok: false; message: string };
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Stream events are daemon-normalized and public to iOS. They are derived from package-internal TUI Pi events and TUI-computed runtime-status snapshots but do not expose raw Pi event payloads. `turn_start` and `turn_end` are lifecycle signals; transcript content remains represented by `TranscriptMessage` events. `runtime_status` replaces the previous runtime-status snapshot for the session. `remote_compact_result` reports the asynchronous outcome of a remote compact request and is correlated by `requestId`; it is not stored in daemon durable state. The initial `session_state` stream event is limited to at most 20 recent messages; oversized string payloads in those messages are truncated to their first 10 KiB and marked with truncation metadata.
|
|
267
|
+
|
|
268
|
+
## TUI control channel
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
type TuiControlChannel = {
|
|
272
|
+
sessionId: string;
|
|
273
|
+
pid: number;
|
|
274
|
+
status: "active" | "closing";
|
|
275
|
+
lastHeartbeatAt: string;
|
|
276
|
+
latestRuntimeStatus?: RuntimeStatus;
|
|
277
|
+
};
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The control channel is the daemon's route for sending remote prompt, abort, and compact commands to the owning TUI extension and for receiving compact results. Loopback TUI control requests do not require a bearer token; non-loopback TUI control requests do.
|
|
281
|
+
|
|
282
|
+
## Tool state
|
|
283
|
+
|
|
284
|
+
`tools` is a compact snapshot of active or recent tool execution state associated by `toolCallId`. Tool-call declarations are preserved inside assistant `TranscriptMessage.content`; tool execution progress and completion are delivered on the WebSocket stream as normalized tool events.
|