@tt-a1i/openpi 0.3.1 → 0.5.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 +184 -59
- package/SETUP.md +23 -7
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +14 -6
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +30 -42
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +146 -87
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +75 -59
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +328 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +222 -140
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +127 -131
- package/extensions/shared/activity-status.ts +36 -5
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +444 -22
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +97 -38
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +113 -0
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +42 -8
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +461 -186
- package/extensions/subagents/navigation.ts +86 -28
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +651 -536
- package/extensions/subagents/src/prompt.ts +185 -42
- package/extensions/subagents/src/result-artifact.ts +146 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +23 -6
- package/extensions/subagents/src/ui/takeover.ts +128 -337
- package/extensions/subagents/src/ui/transcript.ts +38 -501
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -44
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +147 -22
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +62 -0
- package/extensions/workflows/dashboard.ts +458 -339
- package/extensions/workflows/handoff.ts +121 -25
- package/extensions/workflows/index.ts +1042 -492
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +131 -19
- package/extensions/workflows/navigation.ts +61 -18
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +166 -10
- package/extensions/workflows/replay-safety.ts +58 -27
- package/extensions/workflows/result-delivery.ts +253 -0
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +36 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +10 -5
- package/skills/workflows/SKILL.md +53 -10
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -296
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import { readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { webCapabilitySnapshot } from "../../extensions/shared/web-observer-registry.ts";
|
|
5
|
+
import {
|
|
6
|
+
boundedText,
|
|
7
|
+
jsonByteLength,
|
|
8
|
+
projectEntries,
|
|
9
|
+
projectEntry,
|
|
10
|
+
WEB_MAX_MODELS,
|
|
11
|
+
WEB_MAX_SESSIONS,
|
|
12
|
+
WEB_MAX_SESSION_PREVIEW,
|
|
13
|
+
WEB_MAX_SNAPSHOT_BYTES,
|
|
14
|
+
WEB_MAX_WORKSPACES,
|
|
15
|
+
type WebSessionProjection,
|
|
16
|
+
type WebSessionSummary,
|
|
17
|
+
type WebSnapshotTruncation,
|
|
18
|
+
type WebWorkspaceSummary,
|
|
19
|
+
} from "../protocol/types.ts";
|
|
20
|
+
import type { WebRuntimeController } from "../runtime/types.ts";
|
|
21
|
+
|
|
22
|
+
type WorkspaceStateSnapshot = {
|
|
23
|
+
importedWorkspaces: Set<string>;
|
|
24
|
+
hiddenWorkspaces: Set<string>;
|
|
25
|
+
workspaceNames: Map<string, string>;
|
|
26
|
+
ungroupedSessions: Set<string>;
|
|
27
|
+
restoreInitialWorkspace: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export class PiWebAdapter {
|
|
31
|
+
private readonly runtime: WebRuntimeController;
|
|
32
|
+
private readonly importedWorkspaces = new Set<string>();
|
|
33
|
+
private readonly hiddenWorkspaces = new Set<string>();
|
|
34
|
+
private readonly workspaceNames = new Map<string, string>();
|
|
35
|
+
private readonly workspaceStateFile: string;
|
|
36
|
+
private readonly ungroupedSessions = new Set<string>();
|
|
37
|
+
private readonly archiveFile: string;
|
|
38
|
+
private readonly archivedSessions = new Set<string>();
|
|
39
|
+
private archivesLoaded = false;
|
|
40
|
+
private archivesLoading?: Promise<void>;
|
|
41
|
+
private archivesWrite: Promise<void> = Promise.resolve();
|
|
42
|
+
private workspaceStateLoaded = false;
|
|
43
|
+
private workspaceStateLoading?: Promise<void>;
|
|
44
|
+
private workspaceStateWrite: Promise<void> = Promise.resolve();
|
|
45
|
+
private initialization?: Promise<void>;
|
|
46
|
+
private restoreInitialWorkspace = false;
|
|
47
|
+
private writeSequence = 0;
|
|
48
|
+
|
|
49
|
+
constructor(runtime: WebRuntimeController) {
|
|
50
|
+
this.runtime = runtime;
|
|
51
|
+
this.workspaceStateFile = join(
|
|
52
|
+
runtime.sessionDirectory,
|
|
53
|
+
"workspace-state.json",
|
|
54
|
+
);
|
|
55
|
+
this.archiveFile = join(runtime.sessionDirectory, "archived-sessions.json");
|
|
56
|
+
if (runtime.workspaceSelected === true) {
|
|
57
|
+
this.importedWorkspaces.add(resolve(runtime.cwd));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async ensureWorkspaceStateLoaded() {
|
|
62
|
+
if (this.workspaceStateLoaded) return;
|
|
63
|
+
if (!this.workspaceStateLoading) {
|
|
64
|
+
this.workspaceStateLoading = (async () => {
|
|
65
|
+
try {
|
|
66
|
+
const parsed: unknown = JSON.parse(
|
|
67
|
+
await readFile(this.workspaceStateFile, "utf8"),
|
|
68
|
+
);
|
|
69
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
70
|
+
throw new Error("Workspace metadata must be an object");
|
|
71
|
+
}
|
|
72
|
+
const state = parsed as Record<string, unknown>;
|
|
73
|
+
if (
|
|
74
|
+
!Array.isArray(state.hiddenWorkspaces) ||
|
|
75
|
+
!state.hiddenWorkspaces.every((path) => typeof path === "string") ||
|
|
76
|
+
!Array.isArray(state.ungroupedSessions) ||
|
|
77
|
+
!state.ungroupedSessions.every((path) => typeof path === "string") ||
|
|
78
|
+
!state.workspaceNames ||
|
|
79
|
+
typeof state.workspaceNames !== "object" ||
|
|
80
|
+
Array.isArray(state.workspaceNames) ||
|
|
81
|
+
!Object.entries(state.workspaceNames).every(
|
|
82
|
+
([path, name]) => path.length > 0 && typeof name === "string",
|
|
83
|
+
)
|
|
84
|
+
) {
|
|
85
|
+
throw new Error("Workspace metadata has an invalid shape");
|
|
86
|
+
}
|
|
87
|
+
for (const path of state.hiddenWorkspaces) {
|
|
88
|
+
this.hiddenWorkspaces.add(resolve(path));
|
|
89
|
+
}
|
|
90
|
+
for (const path of state.ungroupedSessions) {
|
|
91
|
+
this.ungroupedSessions.add(resolve(path));
|
|
92
|
+
}
|
|
93
|
+
for (const [path, name] of Object.entries(state.workspaceNames)) {
|
|
94
|
+
this.workspaceNames.set(resolve(path), name as string);
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
98
|
+
}
|
|
99
|
+
this.restoreInitialWorkspace =
|
|
100
|
+
this.runtime.workspaceSelected === true &&
|
|
101
|
+
this.hiddenWorkspaces.has(resolve(this.runtime.cwd));
|
|
102
|
+
this.workspaceStateLoaded = true;
|
|
103
|
+
})();
|
|
104
|
+
}
|
|
105
|
+
await this.workspaceStateLoading;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private enqueueWorkspaceMutation<T>(
|
|
109
|
+
mutate: (draft: WorkspaceStateSnapshot) => T | Promise<T>,
|
|
110
|
+
) {
|
|
111
|
+
const operation = this.workspaceStateWrite.then(async () => {
|
|
112
|
+
const draft = this.captureWorkspaceState();
|
|
113
|
+
const result = await mutate(draft);
|
|
114
|
+
await this.writeAtomically(
|
|
115
|
+
this.workspaceStateFile,
|
|
116
|
+
`${JSON.stringify({
|
|
117
|
+
version: 1,
|
|
118
|
+
hiddenWorkspaces: [...draft.hiddenWorkspaces],
|
|
119
|
+
ungroupedSessions: [...draft.ungroupedSessions],
|
|
120
|
+
workspaceNames: Object.fromEntries(draft.workspaceNames),
|
|
121
|
+
})}\n`,
|
|
122
|
+
);
|
|
123
|
+
this.restoreWorkspaceState(draft);
|
|
124
|
+
return result;
|
|
125
|
+
});
|
|
126
|
+
this.workspaceStateWrite = operation.then(
|
|
127
|
+
() => undefined,
|
|
128
|
+
() => undefined,
|
|
129
|
+
);
|
|
130
|
+
return operation;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async ensureArchivesLoaded() {
|
|
134
|
+
if (this.archivesLoaded) return;
|
|
135
|
+
if (!this.archivesLoading) {
|
|
136
|
+
this.archivesLoading = (async () => {
|
|
137
|
+
try {
|
|
138
|
+
const parsed: unknown = JSON.parse(
|
|
139
|
+
await readFile(this.archiveFile, "utf8"),
|
|
140
|
+
);
|
|
141
|
+
if (
|
|
142
|
+
!Array.isArray(parsed) ||
|
|
143
|
+
!parsed.every((path) => typeof path === "string")
|
|
144
|
+
) {
|
|
145
|
+
throw new Error("Archive metadata must be an array of paths");
|
|
146
|
+
}
|
|
147
|
+
for (const path of parsed) this.archivedSessions.add(resolve(path));
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
150
|
+
}
|
|
151
|
+
this.archivesLoaded = true;
|
|
152
|
+
})();
|
|
153
|
+
}
|
|
154
|
+
await this.archivesLoading;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private enqueueArchiveMutation<T>(
|
|
158
|
+
mutate: (draft: Set<string>) => T | Promise<T>,
|
|
159
|
+
) {
|
|
160
|
+
const operation = this.archivesWrite.then(async () => {
|
|
161
|
+
const draft = new Set(this.archivedSessions);
|
|
162
|
+
const result = await mutate(draft);
|
|
163
|
+
await this.writeAtomically(
|
|
164
|
+
this.archiveFile,
|
|
165
|
+
`${JSON.stringify([...draft])}\n`,
|
|
166
|
+
);
|
|
167
|
+
this.archivedSessions.clear();
|
|
168
|
+
for (const archivedPath of draft) {
|
|
169
|
+
this.archivedSessions.add(archivedPath);
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
});
|
|
173
|
+
this.archivesWrite = operation.then(
|
|
174
|
+
() => undefined,
|
|
175
|
+
() => undefined,
|
|
176
|
+
);
|
|
177
|
+
return operation;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private async writeAtomically(filePath: string, content: string) {
|
|
181
|
+
const temporary = `${filePath}.tmp-${process.pid}-${++this.writeSequence}`;
|
|
182
|
+
try {
|
|
183
|
+
await writeFile(temporary, content, "utf8");
|
|
184
|
+
await rename(temporary, filePath);
|
|
185
|
+
} finally {
|
|
186
|
+
await rm(temporary, { force: true });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
initialize() {
|
|
191
|
+
this.initialization ??= (async () => {
|
|
192
|
+
await this.ensureWorkspaceStateLoaded();
|
|
193
|
+
if (!this.restoreInitialWorkspace) return;
|
|
194
|
+
await this.enqueueWorkspaceMutation((draft) => {
|
|
195
|
+
draft.restoreInitialWorkspace = false;
|
|
196
|
+
draft.hiddenWorkspaces.delete(resolve(this.runtime.cwd));
|
|
197
|
+
draft.importedWorkspaces.add(resolve(this.runtime.cwd));
|
|
198
|
+
});
|
|
199
|
+
})();
|
|
200
|
+
return this.initialization;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async importWorkspace(path: string) {
|
|
204
|
+
await this.ensureWorkspaceStateLoaded();
|
|
205
|
+
const canonical = await realpath(resolve(path));
|
|
206
|
+
const info = await stat(canonical);
|
|
207
|
+
if (!info.isDirectory())
|
|
208
|
+
throw new Error("Workspace path is not a directory");
|
|
209
|
+
return this.enqueueWorkspaceMutation((draft) => {
|
|
210
|
+
draft.importedWorkspaces.add(canonical);
|
|
211
|
+
draft.hiddenWorkspaces.delete(canonical);
|
|
212
|
+
return canonical;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async requireWorkspace(path: string) {
|
|
217
|
+
await this.ensureWorkspaceStateLoaded();
|
|
218
|
+
const canonical = resolve(path);
|
|
219
|
+
const workspaces = await this.workspacePaths();
|
|
220
|
+
if (!workspaces.has(canonical) || this.hiddenWorkspaces.has(canonical)) {
|
|
221
|
+
throw new Error("Workspace is not available");
|
|
222
|
+
}
|
|
223
|
+
return canonical;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async requireSession(path: string) {
|
|
227
|
+
await this.ensureWorkspaceStateLoaded();
|
|
228
|
+
const sessions = await this.listSessions(path);
|
|
229
|
+
const session = sessions.find((item) => item.path === path);
|
|
230
|
+
if (!session) throw new Error("Session is not available");
|
|
231
|
+
if (!session.ungrouped) {
|
|
232
|
+
const workspace = resolve(session.cwd);
|
|
233
|
+
if (this.hiddenWorkspaces.has(workspace)) {
|
|
234
|
+
throw new Error("Workspace is not available");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return session;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async renameWorkspace(path: string, name: string) {
|
|
241
|
+
await this.ensureWorkspaceStateLoaded();
|
|
242
|
+
const canonical = resolve(path);
|
|
243
|
+
const normalized = name.trim();
|
|
244
|
+
if (
|
|
245
|
+
!normalized ||
|
|
246
|
+
normalized.length > 80 ||
|
|
247
|
+
/[\u0000-\u001f\u007f]/.test(normalized)
|
|
248
|
+
) {
|
|
249
|
+
throw new Error("Workspace name must be 1-80 visible characters");
|
|
250
|
+
}
|
|
251
|
+
return this.enqueueWorkspaceMutation(async (draft) => {
|
|
252
|
+
const workspaces = await this.workspacePaths(undefined, draft);
|
|
253
|
+
if (!workspaces.has(canonical)) {
|
|
254
|
+
throw new Error("Workspace is not available");
|
|
255
|
+
}
|
|
256
|
+
draft.workspaceNames.set(canonical, normalized);
|
|
257
|
+
return normalized;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async renameSession(path: string, name: string) {
|
|
262
|
+
const session = await this.requireSession(path);
|
|
263
|
+
const normalized = name.trim();
|
|
264
|
+
if (
|
|
265
|
+
!normalized ||
|
|
266
|
+
normalized.length > 80 ||
|
|
267
|
+
/[\u0000-\u001f\u007f]/.test(normalized)
|
|
268
|
+
) {
|
|
269
|
+
throw new Error("Conversation name must be 1-80 visible characters");
|
|
270
|
+
}
|
|
271
|
+
const manager =
|
|
272
|
+
session.id === this.runtime.sessionManager.getSessionId()
|
|
273
|
+
? this.runtime.sessionManager
|
|
274
|
+
: SessionManager.open(session.path, this.runtime.sessionDirectory);
|
|
275
|
+
manager.appendSessionInfo(normalized);
|
|
276
|
+
return normalized;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async archiveSession(path: string) {
|
|
280
|
+
await this.ensureArchivesLoaded();
|
|
281
|
+
await this.enqueueArchiveMutation(async (draft) => {
|
|
282
|
+
const session = await this.requireSession(path);
|
|
283
|
+
draft.add(resolve(session.path));
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async removeWorkspace(path: string) {
|
|
288
|
+
await this.ensureWorkspaceStateLoaded();
|
|
289
|
+
const canonical = resolve(path);
|
|
290
|
+
await this.enqueueWorkspaceMutation(async (draft) => {
|
|
291
|
+
const workspaces = await this.workspacePaths(undefined, draft);
|
|
292
|
+
if (!workspaces.has(canonical)) {
|
|
293
|
+
throw new Error("Workspace is not available");
|
|
294
|
+
}
|
|
295
|
+
const sessions = await SessionManager.listAll(this.runtime.sessionDirectory);
|
|
296
|
+
for (const session of sessions) {
|
|
297
|
+
if (resolve(session.cwd) === canonical) {
|
|
298
|
+
draft.ungroupedSessions.add(resolve(session.path));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (resolve(this.runtime.cwd) === canonical) {
|
|
302
|
+
const currentPath =
|
|
303
|
+
this.runtime.sessionManager.getSessionFile() ??
|
|
304
|
+
`current:${this.runtime.sessionManager.getSessionId()}`;
|
|
305
|
+
draft.ungroupedSessions.add(resolve(currentPath));
|
|
306
|
+
}
|
|
307
|
+
draft.importedWorkspaces.delete(canonical);
|
|
308
|
+
draft.workspaceNames.delete(canonical);
|
|
309
|
+
draft.hiddenWorkspaces.add(canonical);
|
|
310
|
+
draft.restoreInitialWorkspace = false;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async listSessionProjection(pinnedPath?: string) {
|
|
315
|
+
await this.ensureWorkspaceStateLoaded();
|
|
316
|
+
await this.ensureArchivesLoaded();
|
|
317
|
+
const allSessions = await SessionManager.listAll(
|
|
318
|
+
this.runtime.sessionDirectory,
|
|
319
|
+
);
|
|
320
|
+
// Pi already returns this list newest-first. The scan remains Pi-owned;
|
|
321
|
+
// only the Web projection below is retained and bounded.
|
|
322
|
+
const sorted = allSessions;
|
|
323
|
+
const currentId = this.runtime.sessionManager.getSessionId();
|
|
324
|
+
const pinned = new Set(
|
|
325
|
+
sorted
|
|
326
|
+
.filter(
|
|
327
|
+
(session) =>
|
|
328
|
+
session.id === currentId ||
|
|
329
|
+
(pinnedPath !== undefined && session.path === pinnedPath),
|
|
330
|
+
)
|
|
331
|
+
.map((session) => session.path),
|
|
332
|
+
);
|
|
333
|
+
const retainedPaths = new Set(pinned);
|
|
334
|
+
for (const session of sorted) {
|
|
335
|
+
if (retainedPaths.size >= WEB_MAX_SESSIONS) break;
|
|
336
|
+
retainedPaths.add(session.path);
|
|
337
|
+
}
|
|
338
|
+
const projected = sorted
|
|
339
|
+
.filter((session) => retainedPaths.has(session.path))
|
|
340
|
+
.map((session) => ({
|
|
341
|
+
id: session.id,
|
|
342
|
+
path: session.path,
|
|
343
|
+
cwd: resolve(session.cwd),
|
|
344
|
+
...(session.name
|
|
345
|
+
? { name: boundedText(session.name, WEB_MAX_SESSION_PREVIEW) }
|
|
346
|
+
: {}),
|
|
347
|
+
modified: session.modified.toISOString(),
|
|
348
|
+
created: session.created.toISOString(),
|
|
349
|
+
messageCount: session.messageCount,
|
|
350
|
+
firstMessage: boundedText(
|
|
351
|
+
session.firstMessage,
|
|
352
|
+
WEB_MAX_SESSION_PREVIEW,
|
|
353
|
+
),
|
|
354
|
+
...(this.archivedSessions.has(resolve(session.path)) ? { archived: true } : {}),
|
|
355
|
+
...(this.ungroupedSessions.has(resolve(session.path)) ? { ungrouped: true } : {}),
|
|
356
|
+
}));
|
|
357
|
+
if (
|
|
358
|
+
this.runtime.workspaceSelected === true &&
|
|
359
|
+
!projected.some((session) => session.id === currentId)
|
|
360
|
+
) {
|
|
361
|
+
const entries = this.runtime.sessionManager.getBranch();
|
|
362
|
+
let firstUser = "";
|
|
363
|
+
let messageCount = 0;
|
|
364
|
+
for (const entry of entries) {
|
|
365
|
+
if (entry.type !== "message") continue;
|
|
366
|
+
messageCount++;
|
|
367
|
+
if (!firstUser) {
|
|
368
|
+
const projectedEntry = projectEntry(entry);
|
|
369
|
+
if (projectedEntry.message?.role === "user") {
|
|
370
|
+
firstUser = projectedEntry.message.content;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const now = new Date().toISOString();
|
|
375
|
+
const currentPath =
|
|
376
|
+
this.runtime.sessionManager.getSessionFile() ?? `current:${currentId}`;
|
|
377
|
+
projected.unshift({
|
|
378
|
+
id: currentId,
|
|
379
|
+
path: currentPath,
|
|
380
|
+
cwd: resolve(this.runtime.cwd),
|
|
381
|
+
...(this.runtime.sessionManager.getSessionName()
|
|
382
|
+
? {
|
|
383
|
+
name: boundedText(
|
|
384
|
+
this.runtime.sessionManager.getSessionName()!,
|
|
385
|
+
WEB_MAX_SESSION_PREVIEW,
|
|
386
|
+
),
|
|
387
|
+
}
|
|
388
|
+
: {}),
|
|
389
|
+
modified: now,
|
|
390
|
+
created: now,
|
|
391
|
+
messageCount,
|
|
392
|
+
firstMessage: boundedText(firstUser, WEB_MAX_SESSION_PREVIEW),
|
|
393
|
+
...(this.archivedSessions.has(resolve(currentPath))
|
|
394
|
+
? { archived: true }
|
|
395
|
+
: {}),
|
|
396
|
+
...(this.ungroupedSessions.has(resolve(currentPath))
|
|
397
|
+
? { ungrouped: true }
|
|
398
|
+
: {}),
|
|
399
|
+
});
|
|
400
|
+
if (projected.length > WEB_MAX_SESSIONS) {
|
|
401
|
+
let removeAt = projected.length - 1;
|
|
402
|
+
while (
|
|
403
|
+
removeAt > 0 &&
|
|
404
|
+
pinnedPath !== undefined &&
|
|
405
|
+
projected[removeAt]?.path === pinnedPath
|
|
406
|
+
) {
|
|
407
|
+
removeAt--;
|
|
408
|
+
}
|
|
409
|
+
projected.splice(removeAt, 1);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
sessions: projected,
|
|
414
|
+
omitted: Math.max(
|
|
415
|
+
0,
|
|
416
|
+
allSessions.length +
|
|
417
|
+
(allSessions.some((session) => session.id === currentId) ||
|
|
418
|
+
this.runtime.workspaceSelected !== true
|
|
419
|
+
? 0
|
|
420
|
+
: 1) -
|
|
421
|
+
projected.length,
|
|
422
|
+
),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async listSessions(pinnedPath?: string): Promise<WebSessionSummary[]> {
|
|
427
|
+
return (await this.listSessionProjection(pinnedPath)).sessions;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async getSnapshot(selectedPath?: string) {
|
|
431
|
+
await this.ensureWorkspaceStateLoaded();
|
|
432
|
+
const sessionProjection = await this.listSessionProjection(selectedPath);
|
|
433
|
+
const sessions = sessionProjection.sessions;
|
|
434
|
+
const currentCwd = resolve(this.runtime.cwd);
|
|
435
|
+
const workspacePaths = await this.workspacePaths(sessions);
|
|
436
|
+
const retainedWorkspacePaths: string[] = [];
|
|
437
|
+
let visibleWorkspaceCount = 0;
|
|
438
|
+
for (const workspacePath of workspacePaths) {
|
|
439
|
+
if (this.hiddenWorkspaces.has(workspacePath)) continue;
|
|
440
|
+
visibleWorkspaceCount++;
|
|
441
|
+
if (retainedWorkspacePaths.length < WEB_MAX_WORKSPACES) {
|
|
442
|
+
retainedWorkspacePaths.push(workspacePath);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const workspaces: WebWorkspaceSummary[] = retainedWorkspacePaths
|
|
446
|
+
.map((path) => ({
|
|
447
|
+
path,
|
|
448
|
+
name: (this.workspaceNames.get(path) ?? basename(path)) || path,
|
|
449
|
+
current:
|
|
450
|
+
this.runtime.workspaceSelected === true && path === currentCwd,
|
|
451
|
+
}))
|
|
452
|
+
.sort((left, right) =>
|
|
453
|
+
left.current === right.current
|
|
454
|
+
? left.name.localeCompare(right.name)
|
|
455
|
+
: left.current
|
|
456
|
+
? -1
|
|
457
|
+
: 1,
|
|
458
|
+
);
|
|
459
|
+
const path =
|
|
460
|
+
selectedPath ??
|
|
461
|
+
sessions.find(
|
|
462
|
+
(session) => session.id === this.runtime.sessionManager.getSessionId(),
|
|
463
|
+
)?.path;
|
|
464
|
+
const selectedSession = path
|
|
465
|
+
? await this.getSession(path, sessions)
|
|
466
|
+
: undefined;
|
|
467
|
+
const allModels = this.runtime.listModels();
|
|
468
|
+
const currentModel = allModels.find((model) => model.current);
|
|
469
|
+
const retainedModels = currentModel ? [currentModel] : [];
|
|
470
|
+
for (const model of allModels) {
|
|
471
|
+
if (retainedModels.length >= WEB_MAX_MODELS) break;
|
|
472
|
+
if (model !== currentModel) retainedModels.push(model);
|
|
473
|
+
}
|
|
474
|
+
const models = retainedModels.map((model) => ({
|
|
475
|
+
...model,
|
|
476
|
+
provider: boundedText(model.provider, WEB_MAX_SESSION_PREVIEW),
|
|
477
|
+
id: boundedText(model.id, WEB_MAX_SESSION_PREVIEW),
|
|
478
|
+
name: boundedText(model.name, WEB_MAX_SESSION_PREVIEW),
|
|
479
|
+
label: boundedText(model.label, WEB_MAX_SESSION_PREVIEW),
|
|
480
|
+
}));
|
|
481
|
+
const snapshot = {
|
|
482
|
+
...(this.runtime.workspaceSelected === true
|
|
483
|
+
? { currentSessionId: this.runtime.sessionManager.getSessionId() }
|
|
484
|
+
: {}),
|
|
485
|
+
workspaces,
|
|
486
|
+
sessions,
|
|
487
|
+
models,
|
|
488
|
+
...(selectedSession ? { selectedSession } : {}),
|
|
489
|
+
runtime: {
|
|
490
|
+
status: this.runtime.isIdle()
|
|
491
|
+
? ("idle" as const)
|
|
492
|
+
: ("running" as const),
|
|
493
|
+
capabilities: webCapabilitySnapshot(this.runtime.sessionManager),
|
|
494
|
+
},
|
|
495
|
+
truncation: {
|
|
496
|
+
truncated:
|
|
497
|
+
sessionProjection.omitted > 0 ||
|
|
498
|
+
visibleWorkspaceCount > workspaces.length ||
|
|
499
|
+
allModels.length > models.length ||
|
|
500
|
+
selectedSession?.truncation.truncated === true,
|
|
501
|
+
sessionsOmitted: sessionProjection.omitted,
|
|
502
|
+
workspacesOmitted: visibleWorkspaceCount - workspaces.length,
|
|
503
|
+
modelsOmitted: allModels.length - models.length,
|
|
504
|
+
maxBytes: WEB_MAX_SNAPSHOT_BYTES,
|
|
505
|
+
bytes: 0,
|
|
506
|
+
} satisfies WebSnapshotTruncation,
|
|
507
|
+
};
|
|
508
|
+
this.fitSnapshot(snapshot);
|
|
509
|
+
snapshot.truncation.bytes = jsonByteLength(snapshot);
|
|
510
|
+
return snapshot;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private fitSnapshot(snapshot: {
|
|
514
|
+
currentSessionId?: string;
|
|
515
|
+
workspaces: WebWorkspaceSummary[];
|
|
516
|
+
sessions: WebSessionSummary[];
|
|
517
|
+
selectedSession?: WebSessionProjection;
|
|
518
|
+
models: ReturnType<WebRuntimeController["listModels"]>;
|
|
519
|
+
truncation: WebSnapshotTruncation;
|
|
520
|
+
}) {
|
|
521
|
+
const targetBytes = WEB_MAX_SNAPSHOT_BYTES - 1_024;
|
|
522
|
+
let bytes = jsonByteLength(snapshot);
|
|
523
|
+
if (bytes <= targetBytes) return;
|
|
524
|
+
|
|
525
|
+
const selected = snapshot.selectedSession;
|
|
526
|
+
if (selected && selected.entries.length > 0) {
|
|
527
|
+
let removed = 0;
|
|
528
|
+
while (selected.entries.length > 0 && bytes > targetBytes) {
|
|
529
|
+
const entry = selected.entries.shift();
|
|
530
|
+
if (!entry) break;
|
|
531
|
+
removed++;
|
|
532
|
+
bytes -= jsonByteLength(entry) + 1;
|
|
533
|
+
}
|
|
534
|
+
let messagePartsOmitted = 0;
|
|
535
|
+
let messagesTruncated = 0;
|
|
536
|
+
for (const entry of selected.entries) {
|
|
537
|
+
if (entry.type !== "message" || !entry.message?.truncation) continue;
|
|
538
|
+
messagesTruncated++;
|
|
539
|
+
messagePartsOmitted += entry.message.truncation.partsOmitted ?? 0;
|
|
540
|
+
}
|
|
541
|
+
selected.bytes = jsonByteLength(selected.entries);
|
|
542
|
+
selected.truncation = {
|
|
543
|
+
...selected.truncation,
|
|
544
|
+
truncated: true,
|
|
545
|
+
entriesOmitted: selected.truncation.entriesOmitted + removed,
|
|
546
|
+
messagePartsOmitted,
|
|
547
|
+
messagesTruncated,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const selectedPath = selected?.path;
|
|
552
|
+
bytes = jsonByteLength(snapshot);
|
|
553
|
+
while (bytes > targetBytes && snapshot.sessions.length > 1) {
|
|
554
|
+
let index = -1;
|
|
555
|
+
for (let candidate = snapshot.sessions.length - 1; candidate >= 0; candidate--) {
|
|
556
|
+
const session = snapshot.sessions[candidate]!;
|
|
557
|
+
if (
|
|
558
|
+
session.id !== snapshot.currentSessionId &&
|
|
559
|
+
session.path !== selectedPath
|
|
560
|
+
) {
|
|
561
|
+
index = candidate;
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (index < 0) break;
|
|
566
|
+
const [removed] = snapshot.sessions.splice(index, 1);
|
|
567
|
+
if (removed) bytes -= jsonByteLength(removed) + 1;
|
|
568
|
+
snapshot.truncation.sessionsOmitted++;
|
|
569
|
+
}
|
|
570
|
+
bytes = jsonByteLength(snapshot);
|
|
571
|
+
while (bytes > targetBytes && snapshot.workspaces.length > 1) {
|
|
572
|
+
let index = -1;
|
|
573
|
+
for (let candidate = snapshot.workspaces.length - 1; candidate >= 0; candidate--) {
|
|
574
|
+
if (!snapshot.workspaces[candidate]!.current) {
|
|
575
|
+
index = candidate;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (index < 0) break;
|
|
580
|
+
const [removed] = snapshot.workspaces.splice(index, 1);
|
|
581
|
+
if (removed) bytes -= jsonByteLength(removed) + 1;
|
|
582
|
+
snapshot.truncation.workspacesOmitted++;
|
|
583
|
+
}
|
|
584
|
+
bytes = jsonByteLength(snapshot);
|
|
585
|
+
while (bytes > targetBytes && snapshot.models.length > 1) {
|
|
586
|
+
let index = -1;
|
|
587
|
+
for (let candidate = snapshot.models.length - 1; candidate >= 0; candidate--) {
|
|
588
|
+
if (!snapshot.models[candidate]!.current) {
|
|
589
|
+
index = candidate;
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (index < 0) break;
|
|
594
|
+
const [removed] = snapshot.models.splice(index, 1);
|
|
595
|
+
if (removed) bytes -= jsonByteLength(removed) + 1;
|
|
596
|
+
snapshot.truncation.modelsOmitted++;
|
|
597
|
+
}
|
|
598
|
+
snapshot.truncation.truncated = true;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private captureWorkspaceState(): WorkspaceStateSnapshot {
|
|
602
|
+
return {
|
|
603
|
+
importedWorkspaces: new Set(this.importedWorkspaces),
|
|
604
|
+
hiddenWorkspaces: new Set(this.hiddenWorkspaces),
|
|
605
|
+
workspaceNames: new Map(this.workspaceNames),
|
|
606
|
+
ungroupedSessions: new Set(this.ungroupedSessions),
|
|
607
|
+
restoreInitialWorkspace: this.restoreInitialWorkspace,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private restoreWorkspaceState(state: WorkspaceStateSnapshot) {
|
|
612
|
+
this.importedWorkspaces.clear();
|
|
613
|
+
for (const path of state.importedWorkspaces) this.importedWorkspaces.add(path);
|
|
614
|
+
this.hiddenWorkspaces.clear();
|
|
615
|
+
for (const path of state.hiddenWorkspaces) this.hiddenWorkspaces.add(path);
|
|
616
|
+
this.workspaceNames.clear();
|
|
617
|
+
for (const [path, name] of state.workspaceNames) {
|
|
618
|
+
this.workspaceNames.set(path, name);
|
|
619
|
+
}
|
|
620
|
+
this.ungroupedSessions.clear();
|
|
621
|
+
for (const path of state.ungroupedSessions) {
|
|
622
|
+
this.ungroupedSessions.add(path);
|
|
623
|
+
}
|
|
624
|
+
this.restoreInitialWorkspace = state.restoreInitialWorkspace;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private async workspacePaths(
|
|
628
|
+
knownSessions?: WebSessionSummary[],
|
|
629
|
+
workspaceState?: WorkspaceStateSnapshot,
|
|
630
|
+
) {
|
|
631
|
+
const sessions = knownSessions ?? (await this.listSessions());
|
|
632
|
+
return new Set([
|
|
633
|
+
...(this.runtime.workspaceSelected === true
|
|
634
|
+
? [resolve(this.runtime.cwd)]
|
|
635
|
+
: []),
|
|
636
|
+
...(workspaceState?.importedWorkspaces ?? this.importedWorkspaces),
|
|
637
|
+
...sessions.map((session) => session.cwd),
|
|
638
|
+
]);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async getSession(
|
|
642
|
+
path: string,
|
|
643
|
+
knownSessions?: WebSessionSummary[],
|
|
644
|
+
): Promise<WebSessionProjection | undefined> {
|
|
645
|
+
const sessions = knownSessions ?? (await this.listSessions(path));
|
|
646
|
+
const summary = sessions.find((session) => session.path === path);
|
|
647
|
+
if (!summary) return undefined;
|
|
648
|
+
|
|
649
|
+
const manager =
|
|
650
|
+
summary.id === this.runtime.sessionManager.getSessionId()
|
|
651
|
+
? this.runtime.sessionManager
|
|
652
|
+
: SessionManager.open(path);
|
|
653
|
+
const projected = projectEntries(manager.getBranch());
|
|
654
|
+
return {
|
|
655
|
+
id: summary.id,
|
|
656
|
+
path: summary.path,
|
|
657
|
+
cwd: summary.cwd,
|
|
658
|
+
...projected,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
export async function openBrowser(url: string) {
|
|
7
|
+
const command =
|
|
8
|
+
process.platform === "darwin"
|
|
9
|
+
? "open"
|
|
10
|
+
: process.platform === "win32"
|
|
11
|
+
? "cmd"
|
|
12
|
+
: "xdg-open";
|
|
13
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
14
|
+
try {
|
|
15
|
+
await execFileAsync(command, args);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|