pi2web 0.2.0 → 0.2.6

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.
Files changed (55) hide show
  1. package/README.md +209 -18
  2. package/dist/activity.js +178 -0
  3. package/dist/agent-feed.js +133 -0
  4. package/dist/agent-templates.js +144 -0
  5. package/dist/agents.js +575 -97
  6. package/dist/auth.js +85 -20
  7. package/dist/collab/baseline.js +84 -0
  8. package/dist/collab/dispatcher.js +221 -0
  9. package/dist/collab/hub.js +2888 -0
  10. package/dist/collab/pi-extension.js +494 -0
  11. package/dist/collab/review-flow.js +459 -0
  12. package/dist/collab/routes.js +285 -0
  13. package/dist/collab/schemas.js +281 -0
  14. package/dist/collab/scoring-flow.js +336 -0
  15. package/dist/collab/similarity.js +15 -0
  16. package/dist/collab/store.js +918 -0
  17. package/dist/collab/types.js +137 -0
  18. package/dist/collab/validate.js +185 -0
  19. package/dist/drafts.js +50 -0
  20. package/dist/http/agent-routes.js +196 -0
  21. package/dist/http/login-limiter.js +53 -0
  22. package/dist/http/mail-settings-routes.js +53 -0
  23. package/dist/http/system-prompt-routes.js +52 -0
  24. package/dist/http/utils.js +43 -0
  25. package/dist/http/web-assets.js +63 -0
  26. package/dist/http/workspace-routes.js +58 -0
  27. package/dist/mail-notifier.js +95 -21
  28. package/dist/main.js +141 -17
  29. package/dist/mcp.js +132 -0
  30. package/dist/metadata-store.js +199 -56
  31. package/dist/paths.js +10 -0
  32. package/dist/sdk-backend.js +288 -49
  33. package/dist/server.js +558 -358
  34. package/dist/session-namer.js +163 -0
  35. package/dist/terminals.js +94 -27
  36. package/dist/timeline.js +185 -0
  37. package/dist/voice.js +196 -40
  38. package/dist/workspaces.js +91 -26
  39. package/dist/ws/agent-socket.js +256 -0
  40. package/dist/ws/terminal-socket.js +46 -0
  41. package/npm-shrinkwrap.json +3685 -0
  42. package/package.json +18 -6
  43. package/packages/edge-tts-server/src/remote_pi_edge_tts/__pycache__/__init__.cpython-312.pyc +0 -0
  44. package/packages/edge-tts-server/src/remote_pi_edge_tts/__pycache__/__main__.cpython-312.pyc +0 -0
  45. package/packages/protocol/openapi.yaml +8 -2
  46. package/packages/protocol/websocket.schema.json +94 -10
  47. package/web/activity-view.js +121 -0
  48. package/web/app.js +3770 -404
  49. package/web/backend-state.js +269 -0
  50. package/web/collab.html +95 -0
  51. package/web/collab.js +1080 -0
  52. package/web/hid-keyboard.js +453 -0
  53. package/web/index.html +14 -9
  54. package/web/styles.css +73 -0
  55. package/ANDROID_APP_FEATURES.md +0 -256
@@ -0,0 +1,256 @@
1
+ import { WebSocket } from 'ws';
2
+ export const PROTOCOL_VERSION = 2;
3
+ /**
4
+ * Events every client sees for every Agent: what it is doing, not what it produced.
5
+ * Transcript content reaches only clients viewing the Agent (`focus`), as timeline items.
6
+ */
7
+ const OVERVIEW_EVENTS = new Set([
8
+ 'agent_start',
9
+ 'agent_end',
10
+ 'agent_settled',
11
+ 'activity_changed',
12
+ 'queue_update',
13
+ 'session_info_changed',
14
+ 'agent_unloaded',
15
+ 'agent_loaded',
16
+ 'auto_retry_start',
17
+ 'auto_retry_end',
18
+ 'summarization_retry_scheduled',
19
+ 'summarization_retry_attempt_start',
20
+ 'summarization_retry_finished',
21
+ 'extension_ui_request',
22
+ 'extension_ui_response',
23
+ 'extension_ui_notify',
24
+ 'extension_ui_status',
25
+ 'extension_ui_widget',
26
+ 'extension_ui_title',
27
+ 'extension_ui_working_message'
28
+ ]);
29
+ /** Delay before sending streamed text or tool output, so one frame carries many updates. */
30
+ const BATCH_WINDOW_MS = 100;
31
+ /**
32
+ * Serves `/api/v1/ws`.
33
+ * - `subscribe_all`: the state of every Agent, then their overview events.
34
+ * - `focus {agentId, limit}`: a snapshot of the Agent being viewed (newest timeline page, streamed
35
+ * text, dialogs), then in order on this socket: streamed text, appended items, user shell output.
36
+ * - `watch`/`unwatch {toolCallId}`: output of a running tool the client has expanded.
37
+ * Nothing is replayed: on (re)connect a client gets current state again. HTTP serves only what
38
+ * never changes: older timeline pages and item bodies.
39
+ */
40
+ export function handleAgentSocket(ctx, ws) {
41
+ ctx.clients.add(ws);
42
+ const subscriptions = new Map();
43
+ let focused;
44
+ /** Running tools whose card this client has expanded. */
45
+ const watched = new Set();
46
+ // Providers emit one delta per token, and tools report their whole output so far on every update.
47
+ // Both wait for a short window: adjacent deltas merge, and only a tool's latest output is sent.
48
+ let pendingDelta, batchTimer;
49
+ const pendingOutputs = new Map();
50
+ const sendNow = (message) => {
51
+ if (ws.readyState !== WebSocket.OPEN)
52
+ return;
53
+ if (ws.bufferedAmount > 2 * 1024 * 1024) {
54
+ ws.close(1013, 'Agent client is too slow');
55
+ return;
56
+ }
57
+ ws.send(JSON.stringify(message));
58
+ };
59
+ const flushBatch = () => {
60
+ if (batchTimer)
61
+ clearTimeout(batchTimer);
62
+ batchTimer = undefined;
63
+ if (pendingDelta) {
64
+ const message = pendingDelta;
65
+ pendingDelta = undefined;
66
+ sendNow(message);
67
+ }
68
+ for (const [toolCallId, { agentId, output }] of pendingOutputs)
69
+ if (watched.has(toolCallId))
70
+ sendNow({ type: 'tool_output', agentId, toolCallId, output });
71
+ pendingOutputs.clear();
72
+ };
73
+ const scheduleBatch = () => {
74
+ if (!batchTimer)
75
+ batchTimer = setTimeout(flushBatch, BATCH_WINDOW_MS);
76
+ };
77
+ /** Everything goes through here so a held delta or output is never overtaken by a later message. */
78
+ const send = (message) => {
79
+ if (message?.type !== 'delta') {
80
+ flushBatch();
81
+ sendNow(message);
82
+ return;
83
+ }
84
+ if (pendingDelta && pendingDelta.agentId === message.agentId && pendingDelta.kind === message.kind)
85
+ pendingDelta.delta += message.delta;
86
+ else {
87
+ flushBatch();
88
+ pendingDelta = { ...message };
89
+ }
90
+ scheduleBatch();
91
+ };
92
+ const forwardDetail = (agentId, message) => {
93
+ if (message.type === 'tool_output') {
94
+ if (!watched.has(message.toolCallId))
95
+ return;
96
+ pendingOutputs.set(message.toolCallId, { agentId, output: message.output });
97
+ scheduleBatch();
98
+ }
99
+ else if (message.type === 'event')
100
+ send({
101
+ type: 'agent_event',
102
+ agentId,
103
+ timestamp: Date.now(),
104
+ status: ctx.agents.get(agentId).record.status,
105
+ event: message.event
106
+ });
107
+ else
108
+ send({ ...message, agentId });
109
+ };
110
+ let focusRequest = 0;
111
+ const unfocus = () => {
112
+ focusRequest++;
113
+ subscriptions.get('focus')?.();
114
+ subscriptions.delete('focus');
115
+ focused = undefined;
116
+ watched.clear();
117
+ pendingOutputs.clear();
118
+ };
119
+ const unsubscribeVoice = ctx.voice?.subscribe((agentId, event) => {
120
+ if (subscriptions.has('*'))
121
+ send({ type: 'voice_event', agentId, event });
122
+ });
123
+ const unsubscribeNamer = ctx.sessionNamer?.subscribe((agentId, event) => {
124
+ if (subscriptions.has('*'))
125
+ send({ type: 'session_namer_event', agentId, event });
126
+ });
127
+ ws.on('close', () => {
128
+ focusRequest++; // A focus still loading is for a socket that no longer exists.
129
+ if (batchTimer)
130
+ clearTimeout(batchTimer);
131
+ pendingDelta = undefined;
132
+ pendingOutputs.clear();
133
+ ctx.clients.delete(ws);
134
+ unsubscribeVoice?.();
135
+ unsubscribeNamer?.();
136
+ for (const unsubscribe of subscriptions.values())
137
+ unsubscribe();
138
+ });
139
+ ws.on('message', raw => {
140
+ void (async () => {
141
+ let m;
142
+ try {
143
+ m = JSON.parse(raw.toString());
144
+ if (m.type === 'subscribe_drafts') {
145
+ subscriptions.get('drafts')?.();
146
+ send({ type: 'subscribed_drafts', protocolVersion: 1 });
147
+ subscriptions.set('drafts', ctx.drafts.subscribe((draft, replay) => send({ type: 'draft_event', draft, replay })));
148
+ return;
149
+ }
150
+ if (m.type === 'draft_ack') {
151
+ ctx.drafts.ack(String(m.id ?? ''));
152
+ return;
153
+ }
154
+ if (m.type === 'subscribe_collab') {
155
+ subscriptions.get('collab')?.();
156
+ subscriptions.set('collab', ctx.collab.subscribe(event => send({ type: 'collab_event', event })));
157
+ send({ type: 'subscribed_collab', protocolVersion: 1 });
158
+ return;
159
+ }
160
+ if (m.type === 'subscribe_all') {
161
+ subscriptions.get('*')?.();
162
+ // State first: events that follow are then always newer than what the client holds.
163
+ for (const agent of ctx.agents.list())
164
+ send({
165
+ type: 'agent_state',
166
+ agentId: agent.agentId,
167
+ state: ctx.agents.snapshotState(agent.agentId)
168
+ });
169
+ subscriptions.set('*', ctx.agents.subscribeAll((agentId, e) => {
170
+ if (OVERVIEW_EVENTS.has(e.event.type))
171
+ send({
172
+ type: 'agent_event',
173
+ agentId,
174
+ timestamp: e.timestamp,
175
+ status: e.status,
176
+ event: e.event
177
+ });
178
+ }));
179
+ // serverTime lets clients correct clock skew when showing how long an activity has been running.
180
+ send({ type: 'subscribed_all', protocolVersion: PROTOCOL_VERSION, serverTime: Date.now() });
181
+ return;
182
+ }
183
+ if (m.type === 'focus') {
184
+ unfocus();
185
+ if (m.agentId === null || m.agentId === undefined)
186
+ return;
187
+ const agentId = String(m.agentId), request = focusRequest;
188
+ // Messages emitted while the snapshot is being sent wait for it; they are all newer.
189
+ let held = [];
190
+ const focus = await ctx.agents
191
+ .focus(agentId, message => (held ? held.push(message) : forwardDetail(agentId, message)), m.limit)
192
+ .catch(error => {
193
+ if (request === focusRequest)
194
+ send({ type: 'focus_error', agentId, error: { code: error.code, message: error.message } });
195
+ });
196
+ if (!focus)
197
+ return;
198
+ if (request !== focusRequest)
199
+ return focus.unsubscribe(); // Focus moved on while loading.
200
+ focused = agentId;
201
+ subscriptions.set('focus', focus.unsubscribe);
202
+ send({ type: 'focus_state', agentId, ...focus.snapshot });
203
+ const pending = held;
204
+ held = undefined;
205
+ for (const message of pending)
206
+ forwardDetail(agentId, message);
207
+ return;
208
+ }
209
+ if (m.type === 'watch' || m.type === 'unwatch') {
210
+ const toolCallId = String(m.toolCallId ?? '');
211
+ if (m.type === 'unwatch') {
212
+ watched.delete(toolCallId);
213
+ pendingOutputs.delete(toolCallId);
214
+ }
215
+ else if (focused && toolCallId) {
216
+ watched.add(toolCallId);
217
+ const output = ctx.agents.toolOutput(focused, toolCallId);
218
+ if (output !== undefined)
219
+ send({ type: 'tool_output', agentId: focused, toolCallId, output });
220
+ }
221
+ return;
222
+ }
223
+ if (['prompt', 'steer', 'follow-up', 'abort'].includes(m.type)) {
224
+ if (m.type !== 'abort') {
225
+ ctx.voice?.recordUserPrompt(m.agentId, String(m.message ?? ''));
226
+ ctx.sessionNamer?.recordUserPrompt(m.agentId, String(m.message ?? ''));
227
+ }
228
+ const disposition = await ctx.agents.command(m.agentId, m.type, m.message);
229
+ send({ type: 'command_result', requestId: m.requestId, success: true, disposition });
230
+ return;
231
+ }
232
+ if (m.type === 'clear-queue') {
233
+ send({
234
+ type: 'command_result',
235
+ requestId: m.requestId,
236
+ success: true,
237
+ queue: await ctx.agents.clearQueue(m.agentId)
238
+ });
239
+ return;
240
+ }
241
+ throw new Error('Unknown command');
242
+ }
243
+ catch (e) {
244
+ send({
245
+ type: 'command_result',
246
+ requestId: m?.requestId,
247
+ success: false,
248
+ error: {
249
+ code: e.code ?? 'BAD_REQUEST',
250
+ message: e instanceof SyntaxError ? 'Invalid JSON' : e.message
251
+ }
252
+ });
253
+ }
254
+ })();
255
+ });
256
+ }
@@ -0,0 +1,46 @@
1
+ import { WebSocket } from 'ws';
2
+ /** Serves `/api/v1/terminals/:id/ws`: terminal output streaming plus input and resize commands. */
3
+ export function handleTerminalSocket(ctx, ws, terminalId) {
4
+ ctx.clients.add(ws);
5
+ let unsubscribe = () => { };
6
+ ws.on('close', () => {
7
+ ctx.clients.delete(ws);
8
+ unsubscribe();
9
+ });
10
+ try {
11
+ const send = (message) => {
12
+ if (ws.readyState !== WebSocket.OPEN)
13
+ return;
14
+ if (ws.bufferedAmount > 1024 * 1024) {
15
+ ws.close(1013, 'Terminal client is too slow');
16
+ return;
17
+ }
18
+ ws.send(JSON.stringify(message));
19
+ };
20
+ unsubscribe = ctx.terminals.subscribe(terminalId, event => {
21
+ send(event);
22
+ if (event.type === 'exit')
23
+ setTimeout(() => ws.close(), 50);
24
+ });
25
+ const snapshot = ctx.terminals.snapshot(terminalId);
26
+ send({ type: 'snapshot', data: snapshot.data, record: snapshot.record });
27
+ ws.on('message', raw => {
28
+ try {
29
+ const message = JSON.parse(raw.toString());
30
+ if (message.type === 'input')
31
+ ctx.terminals.write(terminalId, message.data);
32
+ else if (message.type === 'resize')
33
+ ctx.terminals.resize(terminalId, Number(message.cols), Number(message.rows));
34
+ else
35
+ throw new Error('Unknown terminal command');
36
+ }
37
+ catch (error) {
38
+ send({ type: 'error', message: error.message });
39
+ }
40
+ });
41
+ }
42
+ catch (error) {
43
+ ws.send(JSON.stringify({ type: 'error', message: error.message }));
44
+ ws.close();
45
+ }
46
+ }