pi2web 0.2.1 → 0.2.8
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 +53 -6
- package/dist/activity.js +178 -0
- package/dist/agent-feed.js +133 -0
- package/dist/agent-templates.js +144 -0
- package/dist/agents.js +671 -152
- package/dist/auth.js +3 -1
- package/dist/collab/baseline.js +84 -0
- package/dist/collab/dispatcher.js +45 -10
- package/dist/collab/hub.js +1392 -384
- package/dist/collab/pi-extension.js +322 -65
- package/dist/collab/review-flow.js +197 -48
- package/dist/collab/routes.js +94 -23
- package/dist/collab/schemas.js +69 -13
- package/dist/collab/scoring-flow.js +145 -39
- package/dist/collab/similarity.js +15 -0
- package/dist/collab/store.js +467 -112
- package/dist/collab/types.js +94 -10
- package/dist/collab/validate.js +62 -25
- package/dist/drafts.js +26 -6
- package/dist/http/agent-routes.js +222 -0
- package/dist/http/login-limiter.js +53 -0
- package/dist/http/mail-settings-routes.js +53 -0
- package/dist/http/system-prompt-routes.js +52 -0
- package/dist/http/utils.js +47 -0
- package/dist/http/web-assets.js +64 -0
- package/dist/http/workspace-routes.js +58 -0
- package/dist/mail-notifier.js +63 -17
- package/dist/main.js +61 -16
- package/dist/mcp.js +24 -5
- package/dist/metadata-store.js +203 -56
- package/dist/paths.js +10 -0
- package/dist/sdk-backend.js +249 -45
- package/dist/server.js +488 -614
- package/dist/session-namer.js +66 -17
- package/dist/session-performance.js +137 -0
- package/dist/sotto-stt.js +217 -0
- package/dist/terminals.js +94 -27
- package/dist/timeline.js +185 -0
- package/dist/voice.js +153 -35
- package/dist/workspaces.js +91 -26
- package/dist/ws/agent-socket.js +256 -0
- package/dist/ws/terminal-socket.js +46 -0
- package/npm-shrinkwrap.json +3685 -0
- package/package.json +11 -1
- package/packages/protocol/openapi.yaml +24 -2
- package/packages/protocol/websocket.schema.json +58 -15
- package/web/activity-view.js +121 -0
- package/web/app.js +3647 -756
- package/web/backend-state.js +59 -27
- package/web/collab.js +684 -257
- package/web/hid-keyboard.js +173 -140
- package/web/index.html +7 -2
- package/web/styles.css +21 -2
- package/web/voice-pcm.js +36 -0
package/dist/sdk-backend.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { PERFORMANCE_ENTRY, SessionPerformanceTracker } from './session-performance.js';
|
|
4
5
|
import { createCollabExtension } from './collab/pi-extension.js';
|
|
5
6
|
/** Older Pi builds resolved steer()/followUp() with undefined; treat anything but an explicit `handled` as queued. */
|
|
6
7
|
const normalizeDisposition = (value) => value === 'handled' ? 'handled' : 'queued';
|
|
@@ -16,12 +17,38 @@ export class SdkBackend {
|
|
|
16
17
|
this.session = session;
|
|
17
18
|
this.agentId = agentId;
|
|
18
19
|
this.cwd = cwd;
|
|
20
|
+
const performance = new SessionPerformanceTracker(run => this.session.sessionManager.appendCustomEntry(PERFORMANCE_ENTRY, run));
|
|
21
|
+
this.session.subscribe(event => performance.apply(event));
|
|
19
22
|
}
|
|
20
|
-
static async create(agentId, cwd, sessionFile, profile = 'default', collabBridge) {
|
|
23
|
+
static async create(agentId, cwd, sessionFile, profile = 'default', collabBridge, options = {}) {
|
|
21
24
|
const sessionManager = sessionFile ? SessionManager.open(sessionFile) : SessionManager.create(cwd);
|
|
25
|
+
const { template, piDefaultPrompt } = options;
|
|
22
26
|
// This factory is deliberately inline instead of auto-discovered: normal Pi and normal pi2web agents
|
|
23
27
|
// never load the collaboration tools or their collaboration-specific system guidance.
|
|
24
|
-
const
|
|
28
|
+
const extensionFactories = profile === 'collab' && collabBridge
|
|
29
|
+
? [{ name: 'pi2web-collab', factory: createCollabExtension(agentId, collabBridge) }]
|
|
30
|
+
: [];
|
|
31
|
+
// A template behaves like Pi's own prompt files: `replace` stands in for SYSTEM.md, `append` follows
|
|
32
|
+
// APPEND_SYSTEM.md. `piDefaultPrompt` ignores both files to show what Pi itself would send.
|
|
33
|
+
const systemPromptOverride = piDefaultPrompt
|
|
34
|
+
? () => undefined
|
|
35
|
+
: template?.mode === 'replace'
|
|
36
|
+
? () => template.systemPrompt
|
|
37
|
+
: undefined;
|
|
38
|
+
const appendSystemPromptOverride = piDefaultPrompt
|
|
39
|
+
? () => []
|
|
40
|
+
: template?.mode === 'append'
|
|
41
|
+
? (base) => [...base, template.systemPrompt]
|
|
42
|
+
: undefined;
|
|
43
|
+
const resourceLoader = extensionFactories.length || systemPromptOverride || appendSystemPromptOverride
|
|
44
|
+
? new DefaultResourceLoader({
|
|
45
|
+
cwd,
|
|
46
|
+
agentDir: getAgentDir(),
|
|
47
|
+
extensionFactories,
|
|
48
|
+
systemPromptOverride,
|
|
49
|
+
appendSystemPromptOverride
|
|
50
|
+
})
|
|
51
|
+
: undefined;
|
|
25
52
|
if (resourceLoader)
|
|
26
53
|
await resourceLoader.reload();
|
|
27
54
|
const { session, modelFallbackMessage } = await createAgentSession({ cwd, sessionManager, resourceLoader });
|
|
@@ -35,29 +62,113 @@ export class SdkBackend {
|
|
|
35
62
|
}
|
|
36
63
|
// Slash commands, skills and prompt templates are expanded by prompt() itself (expandPromptTemplates defaults to true),
|
|
37
64
|
// so `/name args` typed in the web composer behaves exactly like the Pi TUI.
|
|
38
|
-
prompt(message) {
|
|
65
|
+
prompt(message) {
|
|
66
|
+
return this.session.prompt(message);
|
|
67
|
+
}
|
|
39
68
|
// Since pi 0.99 steer()/followUp() report whether the message reached the running turn (`handled`)
|
|
40
69
|
// or waits for it (`queued`); the web composer shows that difference instead of guessing.
|
|
41
|
-
async steer(message) {
|
|
42
|
-
|
|
43
|
-
|
|
70
|
+
async steer(message) {
|
|
71
|
+
return normalizeDisposition(await this.session.steer(message));
|
|
72
|
+
}
|
|
73
|
+
async followUp(message) {
|
|
74
|
+
return normalizeDisposition(await this.session.followUp(message));
|
|
75
|
+
}
|
|
76
|
+
async getQueue() {
|
|
77
|
+
return {
|
|
78
|
+
steering: [...this.session.getSteeringMessages()],
|
|
79
|
+
followUp: [...this.session.getFollowUpMessages()]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
44
82
|
/** Drop queued steering and follow-up messages and hand them back so a client can restore them to its composer. */
|
|
45
|
-
async clearQueue() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
83
|
+
async clearQueue() {
|
|
84
|
+
const cleared = this.session.clearQueue();
|
|
85
|
+
return { steering: [...cleared.steering], followUp: [...cleared.followUp] };
|
|
86
|
+
}
|
|
87
|
+
async moveQueue(from, index, expected) {
|
|
88
|
+
const before = {
|
|
89
|
+
steering: [...this.session.getSteeringMessages()],
|
|
90
|
+
followUp: [...this.session.getFollowUpMessages()]
|
|
91
|
+
};
|
|
92
|
+
if (!this.session.isStreaming || before[from][index] !== expected)
|
|
93
|
+
throw Object.assign(new Error('Queued message has changed or already been consumed'), {
|
|
94
|
+
code: 'QUEUE_CHANGED'
|
|
95
|
+
});
|
|
96
|
+
const to = from === 'steering' ? 'followUp' : 'steering';
|
|
97
|
+
before[from].splice(index, 1);
|
|
98
|
+
before[to].push(expected);
|
|
99
|
+
if ([...before.steering, ...before.followUp].some(text => text.startsWith('/')))
|
|
100
|
+
throw Object.assign(new Error('Slash commands cannot be moved safely'), { code: 'QUEUE_CHANGED' });
|
|
101
|
+
// Pi exposes clearQueue(), but no per-message move. Clear and requeue in the same JS turn:
|
|
102
|
+
// steer()/followUp() enqueue synchronously before their promises settle. No model callback can
|
|
103
|
+
// drain the half-updated queue between these calls. Never replace a stale queue by index alone.
|
|
104
|
+
this.session.clearQueue();
|
|
105
|
+
const pending = [
|
|
106
|
+
...before.steering.map(text => this.session.steer(text)),
|
|
107
|
+
...before.followUp.map(text => this.session.followUp(text))
|
|
108
|
+
];
|
|
109
|
+
await Promise.all(pending);
|
|
110
|
+
return this.getQueue();
|
|
111
|
+
}
|
|
112
|
+
abort() {
|
|
113
|
+
return this.session.abort();
|
|
114
|
+
}
|
|
115
|
+
async getState() {
|
|
116
|
+
return {
|
|
117
|
+
agentId: this.agentId,
|
|
118
|
+
sessionId: this.session.sessionId,
|
|
119
|
+
sessionFile: this.session.sessionFile,
|
|
120
|
+
sessionName: this.session.sessionName,
|
|
121
|
+
cwd: this.cwd,
|
|
122
|
+
status: this.disposed ? 'stopped' : this.session.isStreaming ? 'streaming' : 'idle'
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
timeline() {
|
|
126
|
+
const manager = this.session.sessionManager;
|
|
127
|
+
return {
|
|
128
|
+
sessionId: this.session.sessionId,
|
|
129
|
+
leafId: manager.getLeafId(),
|
|
130
|
+
entries: () => manager.getBranch()
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** The effective system prompt Pi will send next, plus the prompt files that contributed to it. */
|
|
134
|
+
async getSystemPrompt() {
|
|
135
|
+
const loader = this.session.resourceLoader;
|
|
136
|
+
return {
|
|
137
|
+
systemPrompt: this.session.systemPrompt,
|
|
138
|
+
systemFile: loader.getSystemPromptSource()?.path,
|
|
139
|
+
appendFiles: loader.getAppendSystemPromptSources().map(source => source.path),
|
|
140
|
+
contextFiles: loader.getAgentsFiles().agentsFiles.map(file => file.path)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
49
143
|
async getCapabilities() {
|
|
50
144
|
const current = this.session.model;
|
|
51
145
|
// ModelRuntime.getAvailable() is async since 0.80.8 and may consult provider auth; the snapshot keeps
|
|
52
146
|
// an unauthenticated or offline call from blocking the capabilities endpoint.
|
|
53
|
-
const available = await this.session.modelRuntime
|
|
54
|
-
|
|
147
|
+
const available = await this.session.modelRuntime
|
|
148
|
+
.getAvailable()
|
|
149
|
+
.catch(() => this.session.modelRuntime.getAvailableSnapshot());
|
|
150
|
+
return {
|
|
151
|
+
model: current ? this.modelInfo(current) : null,
|
|
152
|
+
models: available.map((model) => this.modelInfo(model)),
|
|
153
|
+
thinkingLevel: this.session.thinkingLevel,
|
|
154
|
+
thinkingLevels: this.session.getAvailableThinkingLevels(),
|
|
155
|
+
supportsThinking: this.session.supportsThinking()
|
|
156
|
+
};
|
|
55
157
|
}
|
|
56
158
|
/** Slash commands the composer can offer: file-based prompt templates and skills discovered for this cwd. */
|
|
57
159
|
async listCommands() {
|
|
58
160
|
const loader = this.session.resourceLoader;
|
|
59
|
-
const prompts = loader.getPrompts().prompts.map(prompt => ({
|
|
60
|
-
|
|
161
|
+
const prompts = loader.getPrompts().prompts.map(prompt => ({
|
|
162
|
+
name: prompt.name,
|
|
163
|
+
description: prompt.description,
|
|
164
|
+
argumentHint: prompt.argumentHint,
|
|
165
|
+
source: 'prompt'
|
|
166
|
+
}));
|
|
167
|
+
const skills = loader.getSkills().skills.map(skill => ({
|
|
168
|
+
name: `skill:${skill.name}`,
|
|
169
|
+
description: skill.description,
|
|
170
|
+
source: 'skill'
|
|
171
|
+
}));
|
|
61
172
|
return [...prompts, ...skills].sort((a, b) => a.name.localeCompare(b.name));
|
|
62
173
|
}
|
|
63
174
|
/** Run a command inside the Agent session so its output is recorded in the transcript (`!!` keeps it out of the model context). */
|
|
@@ -70,36 +181,100 @@ export class SdkBackend {
|
|
|
70
181
|
return { id, ...result };
|
|
71
182
|
}
|
|
72
183
|
catch (error) {
|
|
73
|
-
this.events.emit('event', {
|
|
184
|
+
this.events.emit('event', {
|
|
185
|
+
type: 'bash_execution_end',
|
|
186
|
+
id,
|
|
187
|
+
command,
|
|
188
|
+
isError: true,
|
|
189
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
190
|
+
});
|
|
74
191
|
throw error;
|
|
75
192
|
}
|
|
76
193
|
}
|
|
77
|
-
abortBash() {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
194
|
+
abortBash() {
|
|
195
|
+
this.session.abortBash();
|
|
196
|
+
}
|
|
197
|
+
async getSession() {
|
|
198
|
+
return {
|
|
199
|
+
...(await this.getSessionInfo()),
|
|
200
|
+
leafId: this.session.sessionManager.getLeafId(),
|
|
201
|
+
entries: this.session.sessionManager.getEntries(),
|
|
202
|
+
tree: this.session.sessionManager.getTree(),
|
|
203
|
+
userMessages: this.session.getUserMessagesForForking()
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
async getSessionInfo() {
|
|
207
|
+
return {
|
|
208
|
+
sessionId: this.session.sessionId,
|
|
209
|
+
sessionFile: this.session.sessionFile,
|
|
210
|
+
sessionName: this.session.sessionName,
|
|
211
|
+
stats: this.session.getSessionStats(),
|
|
212
|
+
contextUsage: this.session.getContextUsage()
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
compact(instructions) {
|
|
216
|
+
return this.session.compact(instructions);
|
|
217
|
+
}
|
|
218
|
+
async setModel(provider, modelId) {
|
|
219
|
+
const model = this.session.modelRuntime.getModel(provider, modelId);
|
|
220
|
+
if (!model)
|
|
221
|
+
throw Object.assign(new Error('Model not found'), { code: 'MODEL_NOT_FOUND' });
|
|
222
|
+
await this.session.setModel(model);
|
|
223
|
+
}
|
|
224
|
+
async setThinkingLevel(level) {
|
|
225
|
+
if (level !== 'off' && !this.session.getAvailableThinkingLevels().includes(level))
|
|
226
|
+
throw Object.assign(new Error('Invalid thinking level'), { code: 'INVALID_THINKING_LEVEL' });
|
|
227
|
+
this.session.setThinkingLevel(level);
|
|
228
|
+
}
|
|
229
|
+
async setSessionName(name) {
|
|
230
|
+
const value = name.trim();
|
|
231
|
+
if (!value || value.length > 200)
|
|
232
|
+
throw Object.assign(new Error('Session name must be 1-200 characters'), { code: 'INVALID_SESSION_NAME' });
|
|
233
|
+
this.session.setSessionName(value);
|
|
234
|
+
}
|
|
235
|
+
navigate(entryId) {
|
|
236
|
+
return this.session.navigateTree(entryId);
|
|
237
|
+
}
|
|
238
|
+
async fork(entryId) {
|
|
239
|
+
const entry = this.session.sessionManager.getEntry(entryId);
|
|
240
|
+
if (!entry || entry.type !== 'message' || entry.message?.role !== 'user')
|
|
241
|
+
throw Object.assign(new Error('A user message entry is required'), { code: 'SESSION_ENTRY_NOT_FOUND' });
|
|
242
|
+
const content = entry.message.content;
|
|
243
|
+
const selectedText = typeof content === 'string'
|
|
244
|
+
? content
|
|
245
|
+
: Array.isArray(content)
|
|
246
|
+
? content
|
|
247
|
+
.filter((part) => part.type === 'text')
|
|
248
|
+
.map((part) => part.text)
|
|
249
|
+
.join('\n')
|
|
250
|
+
: '';
|
|
251
|
+
let sessionFile;
|
|
252
|
+
if (entry.parentId) {
|
|
253
|
+
sessionFile = this.session.sessionManager.createBranchedSession(entry.parentId);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
const current = this.session.sessionFile;
|
|
257
|
+
if (!current)
|
|
258
|
+
throw Object.assign(new Error('Session persistence is disabled'), { code: 'SESSION_NOT_PERSISTED' });
|
|
259
|
+
sessionFile = SessionManager.create(this.cwd, this.session.sessionManager.getSessionDir(), {
|
|
260
|
+
parentSession: current
|
|
261
|
+
}).getSessionFile();
|
|
262
|
+
}
|
|
263
|
+
return { sessionFile, selectedText };
|
|
264
|
+
}
|
|
98
265
|
async extensionResponse(requestId, value) {
|
|
99
266
|
if (!this.resolveUiRequest(requestId, value))
|
|
100
267
|
throw Object.assign(new Error('Extension UI request not found'), { code: 'EXTENSION_REQUEST_NOT_FOUND' });
|
|
101
268
|
}
|
|
102
|
-
subscribe(listener) {
|
|
269
|
+
subscribe(listener) {
|
|
270
|
+
const fromSession = (event) => listener(event);
|
|
271
|
+
const unsubscribe = this.session.subscribe(fromSession);
|
|
272
|
+
this.events.on('event', listener);
|
|
273
|
+
return () => {
|
|
274
|
+
unsubscribe();
|
|
275
|
+
this.events.off('event', listener);
|
|
276
|
+
};
|
|
277
|
+
}
|
|
103
278
|
async dispose() {
|
|
104
279
|
if (this.disposed)
|
|
105
280
|
return;
|
|
@@ -109,7 +284,16 @@ export class SdkBackend {
|
|
|
109
284
|
this.session.dispose();
|
|
110
285
|
this.disposed = true;
|
|
111
286
|
}
|
|
112
|
-
modelInfo(model) {
|
|
287
|
+
modelInfo(model) {
|
|
288
|
+
return {
|
|
289
|
+
provider: model.provider,
|
|
290
|
+
id: model.id,
|
|
291
|
+
name: model.name,
|
|
292
|
+
reasoning: !!model.reasoning,
|
|
293
|
+
contextWindow: model.contextWindow,
|
|
294
|
+
maxTokens: model.maxTokens
|
|
295
|
+
};
|
|
296
|
+
}
|
|
113
297
|
resolveUiRequest(requestId, value) {
|
|
114
298
|
const pending = this.pendingUi.get(requestId);
|
|
115
299
|
if (!pending)
|
|
@@ -132,17 +316,37 @@ export class SdkBackend {
|
|
|
132
316
|
}
|
|
133
317
|
extensionUi() {
|
|
134
318
|
return {
|
|
135
|
-
select: async (title, options, opts) => await this.request('select', { title, options, opts }),
|
|
319
|
+
select: async (title, options, opts) => (await this.request('select', { title, options, opts })),
|
|
136
320
|
confirm: async (title, message, opts) => Boolean(await this.request('confirm', { title, message, opts })),
|
|
137
|
-
input: async (title, placeholder, opts) => await this.request('input', { title, placeholder, opts }),
|
|
138
|
-
editor: async (title, prefill) => await this.request('editor', { title, prefill }),
|
|
321
|
+
input: async (title, placeholder, opts) => (await this.request('input', { title, placeholder, opts })),
|
|
322
|
+
editor: async (title, prefill) => (await this.request('editor', { title, prefill })),
|
|
139
323
|
notify: (message, type) => this.events.emit('event', { type: 'extension_ui_notify', message, notificationType: type ?? 'info' }),
|
|
140
324
|
setStatus: (key, text) => this.events.emit('event', { type: 'extension_ui_status', key, text }),
|
|
141
|
-
setWidget: (key, content, options) => this.events.emit('event', {
|
|
325
|
+
setWidget: (key, content, options) => this.events.emit('event', {
|
|
326
|
+
type: 'extension_ui_widget',
|
|
327
|
+
key,
|
|
328
|
+
content: Array.isArray(content) ? content : undefined,
|
|
329
|
+
options
|
|
330
|
+
}),
|
|
142
331
|
setTitle: (title) => this.events.emit('event', { type: 'extension_ui_title', title }),
|
|
143
|
-
setWorkingMessage: (message) => this.events.emit('event', { type: 'extension_ui_working_message', message }),
|
|
144
|
-
|
|
332
|
+
setWorkingMessage: (message) => this.events.emit('event', { type: 'extension_ui_working_message', message }),
|
|
333
|
+
setWorkingVisible: () => { },
|
|
334
|
+
setWorkingIndicator: () => { },
|
|
335
|
+
setHiddenThinkingLabel: () => { },
|
|
336
|
+
onTerminalInput: () => () => { },
|
|
337
|
+
setFooter: () => { },
|
|
338
|
+
setHeader: () => { },
|
|
339
|
+
pasteToEditor: () => { },
|
|
340
|
+
setEditorText: () => { },
|
|
341
|
+
getEditorText: () => '',
|
|
342
|
+
addAutocompleteProvider: () => { },
|
|
343
|
+
setEditorComponent: () => { },
|
|
344
|
+
custom: async () => {
|
|
345
|
+
throw Object.assign(new Error('Custom extension components are unsupported'), {
|
|
346
|
+
code: 'EXTENSION_CUSTOM_UI_UNSUPPORTED'
|
|
347
|
+
});
|
|
348
|
+
}
|
|
145
349
|
};
|
|
146
350
|
}
|
|
147
351
|
}
|
|
148
|
-
export const createSdkBackend = (agentId, cwd, sessionFile, profile = 'default', collabBridge) => SdkBackend.create(agentId, cwd, sessionFile, profile, collabBridge);
|
|
352
|
+
export const createSdkBackend = (agentId, cwd, sessionFile, profile = 'default', collabBridge, options = {}) => SdkBackend.create(agentId, cwd, sessionFile, profile, collabBridge, options);
|