local-operator-ui 0.20.1 → 0.21.1
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/out/main/index.js +462 -343
- package/out/preload/index.js +10 -0
- package/out/renderer/assets/{_basePickBy-C7yOlBwG.js → _basePickBy-laWkgx91.js} +1 -1
- package/out/renderer/assets/{_baseUniq-BxWDXUxR.js → _baseUniq-BqC22eo2.js} +1 -1
- package/out/renderer/assets/{agent-details-page-DGusXpiv.js → agent-details-page-DGagheZ3.js} +1 -1
- package/out/renderer/assets/{agent-hub-page-BEy-xjst.js → agent-hub-page-D32dOI0k.js} +1 -1
- package/out/renderer/assets/{agents-page-VfOUQ-yP.js → agents-page-Svf2IDW4.js} +2 -2
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-358ZDLa0.js → architectureDiagram-IEHRJDOE-DnZHTBwm.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-tzEHVS4j.js → blockDiagram-JOT3LUYC-DYgjchjt.js} +1 -1
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-D3tP-CCo.js → c4Diagram-VJAJSXHY-BcWXeTv-.js} +1 -1
- package/out/renderer/assets/channel-D3VO50ky.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-BKUSTR_S.js → chunk-4BMEZGHF-BEHPBqRs.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-DvXPJSTp.js → chunk-A2AXSNBT-B9_c_zdY.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-iJyV8fDW.js → chunk-AEK57VVT-CRDJdW1n.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-B3DDJ3MU.js → chunk-D6G4REZN-ttF3lcfY.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-Dbn_07ZJ.js → chunk-RZ5BOZE2-DrXSLYPB.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-ChdMxZnm.js → chunk-XZIHB7SX-C21vkWhM.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-DvvsZs_g.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-DvvsZs_g.js +1 -0
- package/out/renderer/assets/clone-CxJL_Jp5.js +1 -0
- package/out/renderer/assets/{compact-pagination-duvMYksr.js → compact-pagination-D6D-kfpz.js} +1 -1
- package/out/renderer/assets/{dagre-OKDRZEBW-BuSM1V0w.js → dagre-OKDRZEBW-OI9InyrW.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-39xt9Mcn.js → diagram-SSKATNLV-DjLE_tpn.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-Bgyt_DWD.js → diagram-VNBRO52H-QNILArDh.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-DNfsSD46.js → erDiagram-Q7BY3M3F-B5zUJ0BX.js} +1 -1
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-DbS8UHwN.js → flowDiagram-4HSFHLVR-CT_emQp8.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-Dxdp2V-6.js → ganttDiagram-APWFNJXF-BIkl41-h.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-BLrXJPe3.js → gitGraphDiagram-7IBYFJ6S-o1q3a_aW.js} +1 -1
- package/out/renderer/assets/{graph-Cgkq5xQ7.js → graph-5IfVp3-D.js} +1 -1
- package/out/renderer/assets/{icon-RIkwUxL5.js → icon-DfC9xz6s.js} +1 -1
- package/out/renderer/assets/icon-DurcHu6h.css +1 -0
- package/out/renderer/assets/{index-lEoaSpWQ.js → index-3JYAvVIP.js} +1 -1
- package/out/renderer/assets/{index-CLwRuOtJ.js → index-B2sOYNB-.js} +309 -307
- package/out/renderer/assets/{index-C4RcIL0i.js → index-DyCsy2F5.js} +1 -1
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-DR6So_J8.js → infoDiagram-PH2N3AL5-DlM0nQ0T.js} +1 -1
- package/out/renderer/assets/{installer-Ck14_4Oh.js → installer-BVVQxXae.js} +1 -1
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-DubpieGX.js → journeyDiagram-U35MCT3I-DNEgTjMB.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-BtwsU2Rg.js → kanban-definition-NDS4AKOZ-CEYF5q_9.js} +1 -1
- package/out/renderer/assets/{layout-BDeRPl4h.js → layout-DE06iqUP.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-ZyBMetf1.js → legacy-agents-page-eOxYX-8u.js} +1 -1
- package/out/renderer/assets/{mermaid.core-d-O-_a6F.js → mermaid.core-BQYlUB6i.js} +5 -5
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-BCZdiKEB.js → mindmap-definition-ALO5MXBD-CRJFFljH.js} +1 -1
- package/out/renderer/assets/{page-header-D-uc8qqA.js → page-header-CzSDxSlW.js} +1 -1
- package/out/renderer/assets/{parseISO-PdZfrqAM.js → parseISO-CuttNio1.js} +1 -1
- package/out/renderer/assets/{pieDiagram-IB7DONF6-Bd2e7gHe.js → pieDiagram-IB7DONF6-Djn6bGuC.js} +1 -1
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-oypZsih9.js → quadrantDiagram-7GDLP6J5-DLPeaSj9.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-c4YagPOX.js → radar-MK3ICKWK-D1c7Dfxy.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-BUlY7XDi.js → radient-auth-buttons-C4t-Tn4o.js} +1 -1
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-7UoAwEus.js → requirementDiagram-KVF5MWMF-Bh1Vsk-Y.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-JStPMuRd.js → sankeyDiagram-QLVOVGJD-CFE_4lHF.js} +1 -1
- package/out/renderer/assets/{schedules-page-IjBtVuIo.js → schedules-page-BMho-_qy.js} +1 -1
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-Lene9gQ-.js → sequenceDiagram-X6HHIX6F-DN47XoiL.js} +1 -1
- package/out/renderer/assets/{settings-page-BqJRfztd.js → settings-page-UQGdkspM.js} +1 -1
- package/out/renderer/assets/{square-pen-CQEKS3JT.js → square-pen-DPhq73Ln.js} +1 -1
- package/out/renderer/assets/{stateDiagram-DGXRK772-umC2tGUK.js → stateDiagram-DGXRK772-CkRlRCxN.js} +1 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-C4bVPoTE.js +1 -0
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-h2KfXqek.js → timeline-definition-BDJGKUSR-D_FilQ9b.js} +1 -1
- package/out/renderer/assets/{use-agent-like-mutation-BanZ02f7.js → use-agent-like-mutation-9LC-lCBi.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-BOH5ZQcH.js → xychartDiagram-VJFVF3MP-sgYxwCpl.js} +1 -1
- package/out/renderer/index.html +3 -3
- package/out/renderer/installer.html +3 -3
- package/package.json +2 -2
- package/out/renderer/assets/channel-D37fF6my.js +0 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-CcnNtrPC.js +0 -1
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-CcnNtrPC.js +0 -1
- package/out/renderer/assets/clone-DPqLvbSr.js +0 -1
- package/out/renderer/assets/icon-HSrrv8O-.css +0 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-Cp1tcztr.js +0 -1
package/out/main/index.js
CHANGED
|
@@ -6,11 +6,11 @@ const utils = require("@electron-toolkit/utils");
|
|
|
6
6
|
const electron = require("electron");
|
|
7
7
|
const posthogNode = require("posthog-node");
|
|
8
8
|
const path = require("path");
|
|
9
|
+
const zod = require("zod");
|
|
9
10
|
const node_child_process = require("node:child_process");
|
|
10
11
|
const node_crypto = require("node:crypto");
|
|
11
12
|
const os = require("node:os");
|
|
12
13
|
const node_util = require("node:util");
|
|
13
|
-
const zod = require("zod");
|
|
14
14
|
const dotenv = require("dotenv");
|
|
15
15
|
const electronLog = require("electron-log");
|
|
16
16
|
const https = require("node:https");
|
|
@@ -35,291 +35,6 @@ const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path$1);
|
|
|
35
35
|
const https__namespace = /* @__PURE__ */ _interopNamespaceDefault(https);
|
|
36
36
|
const icon = path.join(__dirname, "../../resources/icon.png");
|
|
37
37
|
const id$1 = zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/);
|
|
38
|
-
const MAX_UPLOAD_BYTES = 16 * 1024 * 1024;
|
|
39
|
-
const MAX_DOWNLOAD_BYTES = 32 * 1024 * 1024;
|
|
40
|
-
const mediaRequestSchema = zod.z.discriminatedUnion("op", [
|
|
41
|
-
zod.z.object({
|
|
42
|
-
op: zod.z.literal("speech.create"),
|
|
43
|
-
request: zod.z.record(zod.z.unknown())
|
|
44
|
-
}).strict(),
|
|
45
|
-
zod.z.object({
|
|
46
|
-
op: zod.z.literal("speech.agent"),
|
|
47
|
-
agentId: id$1,
|
|
48
|
-
request: zod.z.record(zod.z.unknown())
|
|
49
|
-
}).strict(),
|
|
50
|
-
zod.z.object({
|
|
51
|
-
op: zod.z.literal("transcription.create"),
|
|
52
|
-
fileName: zod.z.string().min(1).max(255),
|
|
53
|
-
mimeType: zod.z.string().min(1).max(128),
|
|
54
|
-
fields: zod.z.record(zod.z.string().max(4096))
|
|
55
|
-
}).strict(),
|
|
56
|
-
zod.z.object({
|
|
57
|
-
op: zod.z.literal("agent.import"),
|
|
58
|
-
fileName: zod.z.string().min(1).max(255)
|
|
59
|
-
}).strict(),
|
|
60
|
-
// Export is a gated GET that answers with a ZIP, so it belongs on this relay
|
|
61
|
-
// rather than the JSON transport: `desktopEndpoint`'s envelope has nowhere to
|
|
62
|
-
// put binary. It is the only read here, which is why `endpoint()` below
|
|
63
|
-
// returns a method instead of assuming POST.
|
|
64
|
-
zod.z.object({ op: zod.z.literal("agent.export"), agentId: id$1 }).strict(),
|
|
65
|
-
// A durable transcript row references an image by content digest with the
|
|
66
|
-
// payload stripped, so rendering a screenshot after a reload means fetching
|
|
67
|
-
// bytes. That cannot go through the JSON transport (its envelope has nowhere
|
|
68
|
-
// to put them), which is what puts it on this relay.
|
|
69
|
-
//
|
|
70
|
-
// Both identifiers are shape-constrained here as well as by the backend
|
|
71
|
-
// route: this file's whole premise is that renderer code cannot pick a URL,
|
|
72
|
-
// and a digest that reaches `endpoint()` unvalidated is renderer-controlled
|
|
73
|
-
// path text.
|
|
74
|
-
zod.z.object({
|
|
75
|
-
op: zod.z.literal("sessions.attachment"),
|
|
76
|
-
sessionId: zod.z.string().regex(/^[a-f0-9]{12}$/),
|
|
77
|
-
digest: zod.z.string().regex(/^[a-f0-9]{32}$/)
|
|
78
|
-
}).strict()
|
|
79
|
-
]);
|
|
80
|
-
function endpoint(request) {
|
|
81
|
-
switch (request.op) {
|
|
82
|
-
case "speech.create":
|
|
83
|
-
return { path: "/v1/tools/speech", method: "POST" };
|
|
84
|
-
case "speech.agent":
|
|
85
|
-
return { path: `/v1/agents/${request.agentId}/speech`, method: "POST" };
|
|
86
|
-
case "transcription.create":
|
|
87
|
-
return { path: "/v1/transcriptions", method: "POST" };
|
|
88
|
-
case "agent.import":
|
|
89
|
-
return { path: "/v1/agents/import", method: "POST" };
|
|
90
|
-
case "agent.export":
|
|
91
|
-
return { path: `/v1/agents/${request.agentId}/export`, method: "GET" };
|
|
92
|
-
case "sessions.attachment":
|
|
93
|
-
return {
|
|
94
|
-
path: `/v1/desktop/sessions/${request.sessionId}/attachments/${request.digest}`,
|
|
95
|
-
method: "GET"
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
async function requestDesktopMedia(input, bytes, backendUrl, token) {
|
|
100
|
-
const parsed = mediaRequestSchema.safeParse(input);
|
|
101
|
-
if (!parsed.success) {
|
|
102
|
-
return { status: 422, kind: "error", detail: "Invalid media operation." };
|
|
103
|
-
}
|
|
104
|
-
if (!token) {
|
|
105
|
-
return {
|
|
106
|
-
status: 503,
|
|
107
|
-
kind: "error",
|
|
108
|
-
detail: "Restart with a desktop-managed backend to use these controls."
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
const request = parsed.data;
|
|
112
|
-
if (bytes && bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
113
|
-
return { status: 413, kind: "error", detail: "This file is too large." };
|
|
114
|
-
}
|
|
115
|
-
const target2 = endpoint(request);
|
|
116
|
-
let body;
|
|
117
|
-
let contentType;
|
|
118
|
-
if (request.op === "agent.export" || request.op === "sessions.attachment") {
|
|
119
|
-
body = void 0;
|
|
120
|
-
} else if (request.op === "speech.create" || request.op === "speech.agent") {
|
|
121
|
-
body = JSON.stringify(request.request);
|
|
122
|
-
contentType = "application/json";
|
|
123
|
-
} else {
|
|
124
|
-
if (!bytes) {
|
|
125
|
-
return { status: 422, kind: "error", detail: "A file is required." };
|
|
126
|
-
}
|
|
127
|
-
const form = new FormData();
|
|
128
|
-
const blob = new Blob([bytes], {
|
|
129
|
-
type: request.op === "transcription.create" ? request.mimeType : "application/zip"
|
|
130
|
-
});
|
|
131
|
-
form.append("file", blob, request.fileName);
|
|
132
|
-
if (request.op === "transcription.create") {
|
|
133
|
-
for (const [key, value] of Object.entries(request.fields)) {
|
|
134
|
-
form.append(key, value);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
body = form;
|
|
138
|
-
}
|
|
139
|
-
try {
|
|
140
|
-
const response = await fetch(new URL(target2.path, backendUrl), {
|
|
141
|
-
method: target2.method,
|
|
142
|
-
headers: {
|
|
143
|
-
Accept: "application/json, audio/*, image/*, application/octet-stream",
|
|
144
|
-
...contentType ? { "Content-Type": contentType } : {},
|
|
145
|
-
Authorization: `Bearer ${token}`
|
|
146
|
-
},
|
|
147
|
-
body,
|
|
148
|
-
redirect: "error",
|
|
149
|
-
signal: AbortSignal.timeout(12e4)
|
|
150
|
-
});
|
|
151
|
-
const responseType = response.headers.get("content-type") ?? "";
|
|
152
|
-
if (!response.ok) {
|
|
153
|
-
let detail = "The media request failed.";
|
|
154
|
-
if (responseType.includes("application/json")) {
|
|
155
|
-
try {
|
|
156
|
-
const parsedError = await response.json();
|
|
157
|
-
const text = parsedError.detail ?? parsedError.message;
|
|
158
|
-
if (typeof text === "string") detail = text.slice(0, 512);
|
|
159
|
-
} catch {
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return { status: response.status, kind: "error", detail };
|
|
163
|
-
}
|
|
164
|
-
if (responseType.includes("application/json")) {
|
|
165
|
-
return {
|
|
166
|
-
status: response.status,
|
|
167
|
-
kind: "json",
|
|
168
|
-
body: await response.json()
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
172
|
-
if (buffer.byteLength > MAX_DOWNLOAD_BYTES) {
|
|
173
|
-
return {
|
|
174
|
-
status: 502,
|
|
175
|
-
kind: "error",
|
|
176
|
-
detail: "The backend returned too much data."
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
return {
|
|
180
|
-
status: response.status,
|
|
181
|
-
kind: "bytes",
|
|
182
|
-
mimeType: responseType || "application/octet-stream",
|
|
183
|
-
data: buffer
|
|
184
|
-
};
|
|
185
|
-
} catch {
|
|
186
|
-
return {
|
|
187
|
-
status: 503,
|
|
188
|
-
kind: "error",
|
|
189
|
-
detail: "The backend could not complete this request. Check its connection and try again."
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
const SESSION_ID = /^[a-f0-9]{12}$/;
|
|
194
|
-
const SAFE_ID = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
195
|
-
const MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
|
196
|
-
class DesktopStreamRelay {
|
|
197
|
-
constructor(backendUrl, token) {
|
|
198
|
-
this.backendUrl = backendUrl;
|
|
199
|
-
this.token = token;
|
|
200
|
-
}
|
|
201
|
-
streams = /* @__PURE__ */ new Map();
|
|
202
|
-
/** Frame observer for main-owned side effects (notifications). It sees
|
|
203
|
-
* every parsed frame before the renderer does, on the same subscription. */
|
|
204
|
-
observer = null;
|
|
205
|
-
observe(observer) {
|
|
206
|
-
this.observer = observer;
|
|
207
|
-
}
|
|
208
|
-
/** True when this relay can authenticate; a false answer is why the UI
|
|
209
|
-
* shows the pairing/setup state rather than a dead transcript. */
|
|
210
|
-
get available() {
|
|
211
|
-
return Boolean(this.token);
|
|
212
|
-
}
|
|
213
|
-
subscribe(args, emit) {
|
|
214
|
-
if (!SESSION_ID.test(args.sessionId)) {
|
|
215
|
-
throw new Error("Invalid session for streaming.");
|
|
216
|
-
}
|
|
217
|
-
if (args.epoch !== void 0 && !SAFE_ID.test(args.epoch)) {
|
|
218
|
-
throw new Error("Invalid stream epoch.");
|
|
219
|
-
}
|
|
220
|
-
if (args.afterSeq !== void 0 && (!Number.isInteger(args.afterSeq) || args.afterSeq < 0)) {
|
|
221
|
-
throw new Error("Invalid stream cursor.");
|
|
222
|
-
}
|
|
223
|
-
if (!this.token) {
|
|
224
|
-
throw new Error("This backend was not started with desktop controls.");
|
|
225
|
-
}
|
|
226
|
-
const streamId = node_crypto.randomBytes(16).toString("hex");
|
|
227
|
-
const controller = new AbortController();
|
|
228
|
-
this.streams.set(streamId, controller);
|
|
229
|
-
const query = new URLSearchParams();
|
|
230
|
-
if (args.epoch) query.set("epoch", args.epoch);
|
|
231
|
-
if (args.afterSeq !== void 0)
|
|
232
|
-
query.set("after_seq", String(args.afterSeq));
|
|
233
|
-
const suffix = query.size > 0 ? `?${query}` : "";
|
|
234
|
-
const url = new URL(
|
|
235
|
-
`/v1/desktop/sessions/${args.sessionId}/events${suffix}`,
|
|
236
|
-
this.backendUrl
|
|
237
|
-
);
|
|
238
|
-
void this.pump(
|
|
239
|
-
streamId,
|
|
240
|
-
args.sessionId,
|
|
241
|
-
url,
|
|
242
|
-
controller.signal,
|
|
243
|
-
emit
|
|
244
|
-
).finally(() => {
|
|
245
|
-
this.streams.delete(streamId);
|
|
246
|
-
});
|
|
247
|
-
return { streamId };
|
|
248
|
-
}
|
|
249
|
-
async pump(streamId, sessionId2, url, signal, emit) {
|
|
250
|
-
try {
|
|
251
|
-
const response = await fetch(url, {
|
|
252
|
-
headers: {
|
|
253
|
-
Accept: "text/event-stream",
|
|
254
|
-
Authorization: `Bearer ${this.token}`
|
|
255
|
-
},
|
|
256
|
-
redirect: "error",
|
|
257
|
-
signal
|
|
258
|
-
});
|
|
259
|
-
if (!response.ok || !response.body) {
|
|
260
|
-
emit({
|
|
261
|
-
streamId,
|
|
262
|
-
kind: "error",
|
|
263
|
-
detail: `The event stream was refused (${response.status}).`
|
|
264
|
-
});
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
const reader = response.body.getReader();
|
|
268
|
-
const decoder = new TextDecoder();
|
|
269
|
-
let buffer = "";
|
|
270
|
-
for (; ; ) {
|
|
271
|
-
const { done, value } = await reader.read();
|
|
272
|
-
if (done) break;
|
|
273
|
-
buffer += decoder.decode(value, { stream: true });
|
|
274
|
-
for (; ; ) {
|
|
275
|
-
const boundary = buffer.indexOf("\n\n");
|
|
276
|
-
if (boundary < 0) {
|
|
277
|
-
if (buffer.length > MAX_FRAME_BYTES) {
|
|
278
|
-
emit({
|
|
279
|
-
streamId,
|
|
280
|
-
kind: "error",
|
|
281
|
-
detail: "The event stream exceeded its frame budget."
|
|
282
|
-
});
|
|
283
|
-
reader.cancel().catch(() => void 0);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
const record = buffer.slice(0, boundary);
|
|
289
|
-
buffer = buffer.slice(boundary + 2);
|
|
290
|
-
for (const line of record.split("\n")) {
|
|
291
|
-
if (line.startsWith("data:")) {
|
|
292
|
-
const data = line.slice(5).replace(/^ /, "");
|
|
293
|
-
this.observer?.(sessionId2, data);
|
|
294
|
-
emit({ streamId, kind: "data", data });
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
emit({ streamId, kind: "end" });
|
|
300
|
-
} catch (error) {
|
|
301
|
-
if (signal.aborted) {
|
|
302
|
-
emit({ streamId, kind: "end" });
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
emit({
|
|
306
|
-
streamId,
|
|
307
|
-
kind: "error",
|
|
308
|
-
detail: error instanceof Error && error.name === "AbortError" ? "The event stream ended." : "The event stream connection failed."
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
unsubscribe(streamId) {
|
|
313
|
-
if (!/^[a-f0-9]{32}$/.test(streamId)) return;
|
|
314
|
-
this.streams.get(streamId)?.abort();
|
|
315
|
-
this.streams.delete(streamId);
|
|
316
|
-
}
|
|
317
|
-
dispose() {
|
|
318
|
-
for (const controller of this.streams.values()) controller.abort();
|
|
319
|
-
this.streams.clear();
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
const id = zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/);
|
|
323
38
|
const settingKey = zod.z.string().min(1).max(256).regex(/^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/);
|
|
324
39
|
const secret = zod.z.string().min(1).max(32768);
|
|
325
40
|
const sessionId = zod.z.string().regex(/^[a-f0-9]{12}$/);
|
|
@@ -418,11 +133,37 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
418
133
|
cwd: zod.z.string().min(1).max(4096),
|
|
419
134
|
target: target.optional()
|
|
420
135
|
}).strict(),
|
|
136
|
+
/*
|
|
137
|
+
* What a NEW conversation's readings would be, without creating anything.
|
|
138
|
+
*
|
|
139
|
+
* The composer's status strip needs a model, an effort ladder and a window
|
|
140
|
+
* BEFORE a session exists, and every honest way to get them was rejected: a
|
|
141
|
+
* `sessions.create` on pane open writes a directory and a marker, so every
|
|
142
|
+
* abandoned new-chat pane would leave a visible empty row in the sidebar,
|
|
143
|
+
* and composing it in the renderer from `config.get` + `models.catalogue`
|
|
144
|
+
* moves model RESOLUTION (Python policy) into the app and reports nothing
|
|
145
|
+
* when the pair is absent from the catalogue. This op runs the backend's own
|
|
146
|
+
* cold resolution and returns the same canonical projection a cold session
|
|
147
|
+
* publishes, so the strip keeps one arithmetic path and the draft cannot
|
|
148
|
+
* disagree with the session the first send creates.
|
|
149
|
+
*
|
|
150
|
+
* Body and response mirror `sessions.create` deliberately: same `cwd`, same
|
|
151
|
+
* optional `target`, same 422 for an unresolvable profile. The response is a
|
|
152
|
+
* `CanonicalFrontendSync` — the wire shape `sessions.watch` streams — whose
|
|
153
|
+
* `snapshot.session_id` is EMPTY, because there is no session. The renderer
|
|
154
|
+
* passes it to the strip and never into the canonical sessions store.
|
|
155
|
+
*/
|
|
156
|
+
zod.z.object({
|
|
157
|
+
op: zod.z.literal("sessions.preview"),
|
|
158
|
+
requestId,
|
|
159
|
+
cwd: zod.z.string().min(1).max(4096),
|
|
160
|
+
target: target.optional()
|
|
161
|
+
}).strict(),
|
|
421
162
|
zod.z.object({ op: zod.z.literal("sessions.get"), sessionId }).strict(),
|
|
422
163
|
zod.z.object({
|
|
423
164
|
op: zod.z.literal("sessions.history"),
|
|
424
165
|
sessionId,
|
|
425
|
-
beforeId: id.optional(),
|
|
166
|
+
beforeId: id$1.optional(),
|
|
426
167
|
limit: zod.z.number().int().min(1).max(500).optional()
|
|
427
168
|
}).strict(),
|
|
428
169
|
zod.z.object({
|
|
@@ -444,8 +185,8 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
444
185
|
zod.z.object({
|
|
445
186
|
op: zod.z.literal("sessions.answer"),
|
|
446
187
|
sessionId,
|
|
447
|
-
epoch: id,
|
|
448
|
-
requestId: id,
|
|
188
|
+
epoch: id$1,
|
|
189
|
+
requestId: id$1,
|
|
449
190
|
value: zod.z.string().max(32768).optional(),
|
|
450
191
|
approved: zod.z.boolean().optional(),
|
|
451
192
|
questionIndex: zod.z.number().int().min(0).optional()
|
|
@@ -494,8 +235,8 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
494
235
|
zod.z.object({ op: zod.z.literal("legacy.models.providers") }).strict(),
|
|
495
236
|
// Reachability is checked only when the user asks for it: a probe is a real
|
|
496
237
|
// network round trip, and nothing behind first paint may make one.
|
|
497
|
-
zod.z.object({ op: zod.z.literal("auth.probe"), provider: id }).strict(),
|
|
498
|
-
zod.z.object({ op: zod.z.literal("legacy.agent.upload"), agentId: id }).strict(),
|
|
238
|
+
zod.z.object({ op: zod.z.literal("auth.probe"), provider: id$1 }).strict(),
|
|
239
|
+
zod.z.object({ op: zod.z.literal("legacy.agent.upload"), agentId: id$1 }).strict(),
|
|
499
240
|
zod.z.object({
|
|
500
241
|
op: zod.z.literal("legacy.agents.list"),
|
|
501
242
|
page: zod.z.number().int().min(1).optional(),
|
|
@@ -504,19 +245,19 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
504
245
|
sort: zod.z.string().max(64).optional(),
|
|
505
246
|
direction: zod.z.enum(["asc", "desc"]).optional()
|
|
506
247
|
}).strict(),
|
|
507
|
-
zod.z.object({ op: zod.z.literal("legacy.agent.get"), agentId: id }).strict(),
|
|
248
|
+
zod.z.object({ op: zod.z.literal("legacy.agent.get"), agentId: id$1 }).strict(),
|
|
508
249
|
zod.z.object({
|
|
509
250
|
op: zod.z.literal("legacy.agent.history"),
|
|
510
|
-
agentId: id,
|
|
251
|
+
agentId: id$1,
|
|
511
252
|
page: zod.z.number().int().min(1).optional(),
|
|
512
253
|
perPage: zod.z.number().int().min(1).max(500).optional()
|
|
513
254
|
}).strict(),
|
|
514
255
|
zod.z.object({
|
|
515
256
|
op: zod.z.literal("legacy.jobs.list"),
|
|
516
|
-
agentId: id.optional(),
|
|
257
|
+
agentId: id$1.optional(),
|
|
517
258
|
status: zod.z.string().max(64).optional()
|
|
518
259
|
}).strict(),
|
|
519
|
-
zod.z.object({ op: zod.z.literal("legacy.job.get"), jobId: id }).strict(),
|
|
260
|
+
zod.z.object({ op: zod.z.literal("legacy.job.get"), jobId: id$1 }).strict(),
|
|
520
261
|
// The schedules surface. Gated in managed mode not because a schedule is
|
|
521
262
|
// sensitive to READ but because both writes hand their `prompt` to the
|
|
522
263
|
// scheduler, which later runs it as the user's own agent -- so a bare
|
|
@@ -528,13 +269,13 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
528
269
|
}).strict(),
|
|
529
270
|
zod.z.object({
|
|
530
271
|
op: zod.z.literal("legacy.agent.schedules.list"),
|
|
531
|
-
agentId: id,
|
|
272
|
+
agentId: id$1,
|
|
532
273
|
page: zod.z.number().int().min(1).optional(),
|
|
533
274
|
perPage: zod.z.number().int().min(1).max(100).optional()
|
|
534
275
|
}).strict(),
|
|
535
276
|
zod.z.object({
|
|
536
277
|
op: zod.z.literal("legacy.agent.schedule.create"),
|
|
537
|
-
agentId: id,
|
|
278
|
+
agentId: id$1,
|
|
538
279
|
// Passed through as the request body. Declared field-by-field rather
|
|
539
280
|
// than as a passthrough object so the renderer cannot smuggle keys the
|
|
540
281
|
// backend model would silently accept.
|
|
@@ -544,10 +285,10 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
544
285
|
unit: scheduleUnit
|
|
545
286
|
})
|
|
546
287
|
}).strict(),
|
|
547
|
-
zod.z.object({ op: zod.z.literal("legacy.schedule.get"), scheduleId: id }).strict(),
|
|
288
|
+
zod.z.object({ op: zod.z.literal("legacy.schedule.get"), scheduleId: id$1 }).strict(),
|
|
548
289
|
zod.z.object({
|
|
549
290
|
op: zod.z.literal("legacy.schedule.edit"),
|
|
550
|
-
scheduleId: id,
|
|
291
|
+
scheduleId: id$1,
|
|
551
292
|
// Every field optional: the backend applies `exclude_unset`, so sending
|
|
552
293
|
// a key the user did not touch would overwrite it with a default.
|
|
553
294
|
schedule: scheduleWrite.extend({
|
|
@@ -556,7 +297,7 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
556
297
|
unit: scheduleUnit.nullish()
|
|
557
298
|
})
|
|
558
299
|
}).strict(),
|
|
559
|
-
zod.z.object({ op: zod.z.literal("legacy.schedule.remove"), scheduleId: id }).strict(),
|
|
300
|
+
zod.z.object({ op: zod.z.literal("legacy.schedule.remove"), scheduleId: id$1 }).strict(),
|
|
560
301
|
// The remaining gated legacy writes and reads the renderer still issued as
|
|
561
302
|
// bare same-origin `fetch`. `apiConfig.baseUrl` points AT THE BACKEND, not
|
|
562
303
|
// through the main-process relay, so in managed mode these went out with no
|
|
@@ -572,52 +313,52 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
572
313
|
}).strict(),
|
|
573
314
|
zod.z.object({
|
|
574
315
|
op: zod.z.literal("legacy.agent.update"),
|
|
575
|
-
agentId: id,
|
|
316
|
+
agentId: id$1,
|
|
576
317
|
update: zod.z.record(zod.z.unknown())
|
|
577
318
|
}).strict(),
|
|
578
|
-
zod.z.object({ op: zod.z.literal("legacy.agent.delete"), agentId: id }).strict(),
|
|
579
|
-
zod.z.object({ op: zod.z.literal("legacy.agent.conversation.clear"), agentId: id }).strict(),
|
|
580
|
-
zod.z.object({ op: zod.z.literal("legacy.agent.systemPrompt.get"), agentId: id }).strict(),
|
|
319
|
+
zod.z.object({ op: zod.z.literal("legacy.agent.delete"), agentId: id$1 }).strict(),
|
|
320
|
+
zod.z.object({ op: zod.z.literal("legacy.agent.conversation.clear"), agentId: id$1 }).strict(),
|
|
321
|
+
zod.z.object({ op: zod.z.literal("legacy.agent.systemPrompt.get"), agentId: id$1 }).strict(),
|
|
581
322
|
zod.z.object({
|
|
582
323
|
op: zod.z.literal("legacy.agent.systemPrompt.update"),
|
|
583
|
-
agentId: id,
|
|
324
|
+
agentId: id$1,
|
|
584
325
|
systemPrompt: zod.z.string().max(DESKTOP_SYSTEM_PROMPT_MAX_CHARS)
|
|
585
326
|
}).strict(),
|
|
586
|
-
zod.z.object({ op: zod.z.literal("legacy.agent.download"), agentId: id }).strict(),
|
|
587
|
-
zod.z.object({ op: zod.z.literal("legacy.agent.variables.list"), agentId: id }).strict(),
|
|
327
|
+
zod.z.object({ op: zod.z.literal("legacy.agent.download"), agentId: id$1 }).strict(),
|
|
328
|
+
zod.z.object({ op: zod.z.literal("legacy.agent.variables.list"), agentId: id$1 }).strict(),
|
|
588
329
|
zod.z.object({
|
|
589
330
|
op: zod.z.literal("legacy.agent.variables.create"),
|
|
590
|
-
agentId: id,
|
|
331
|
+
agentId: id$1,
|
|
591
332
|
variable: zod.z.record(zod.z.unknown())
|
|
592
333
|
}).strict(),
|
|
593
334
|
zod.z.object({
|
|
594
335
|
op: zod.z.literal("legacy.agent.variables.get"),
|
|
595
|
-
agentId: id,
|
|
336
|
+
agentId: id$1,
|
|
596
337
|
key: variableKey
|
|
597
338
|
}).strict(),
|
|
598
339
|
zod.z.object({
|
|
599
340
|
op: zod.z.literal("legacy.agent.variables.update"),
|
|
600
|
-
agentId: id,
|
|
341
|
+
agentId: id$1,
|
|
601
342
|
key: variableKey,
|
|
602
343
|
variable: zod.z.record(zod.z.unknown())
|
|
603
344
|
}).strict(),
|
|
604
345
|
zod.z.object({
|
|
605
346
|
op: zod.z.literal("legacy.agent.variables.delete"),
|
|
606
|
-
agentId: id,
|
|
347
|
+
agentId: id$1,
|
|
607
348
|
key: variableKey
|
|
608
349
|
}).strict(),
|
|
609
|
-
zod.z.object({ op: zod.z.literal("legacy.job.cancel"), jobId: id }).strict(),
|
|
350
|
+
zod.z.object({ op: zod.z.literal("legacy.job.cancel"), jobId: id$1 }).strict(),
|
|
610
351
|
zod.z.object({ op: zod.z.literal("commands.list") }).strict(),
|
|
611
352
|
zod.z.object({
|
|
612
353
|
op: zod.z.literal("commands.entities"),
|
|
613
354
|
sessionId,
|
|
614
|
-
command: id,
|
|
355
|
+
command: id$1,
|
|
615
356
|
name: zod.z.string().max(128).optional()
|
|
616
357
|
}).strict(),
|
|
617
358
|
zod.z.object({ op: zod.z.literal("models.catalogue"), live: zod.z.boolean().optional() }).strict(),
|
|
618
359
|
zod.z.object({
|
|
619
360
|
op: zod.z.literal("usage.get"),
|
|
620
|
-
provider: id.optional(),
|
|
361
|
+
provider: id$1.optional(),
|
|
621
362
|
live: zod.z.boolean().optional(),
|
|
622
363
|
refresh: zod.z.boolean().optional()
|
|
623
364
|
}).strict(),
|
|
@@ -628,7 +369,7 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
628
369
|
untilMs: zod.z.number().int().nonnegative().optional(),
|
|
629
370
|
days: zod.z.number().int().min(1).max(366).optional()
|
|
630
371
|
}).strict(),
|
|
631
|
-
zod.z.object({ op: zod.z.literal("skills.list"), sessionId, name: id.optional() }).strict(),
|
|
372
|
+
zod.z.object({ op: zod.z.literal("skills.list"), sessionId, name: id$1.optional() }).strict(),
|
|
632
373
|
zod.z.object({ op: zod.z.literal("sessions.failovers"), sessionId }).strict(),
|
|
633
374
|
zod.z.object({
|
|
634
375
|
op: zod.z.literal("sessions.credential"),
|
|
@@ -737,10 +478,10 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
737
478
|
"account.agents"
|
|
738
479
|
]),
|
|
739
480
|
request_id: requestId.optional(),
|
|
740
|
-
tenant_id: id.optional(),
|
|
741
|
-
account_id: id.optional(),
|
|
742
|
-
agent_id: id.optional(),
|
|
743
|
-
comment_id: id.optional(),
|
|
481
|
+
tenant_id: id$1.optional(),
|
|
482
|
+
account_id: id$1.optional(),
|
|
483
|
+
agent_id: id$1.optional(),
|
|
484
|
+
comment_id: id$1.optional(),
|
|
744
485
|
query: zod.z.record(zod.z.union([zod.z.string().max(1024), zod.z.number().int()])).optional(),
|
|
745
486
|
payload: zod.z.record(zod.z.unknown()).optional(),
|
|
746
487
|
confirmed: zod.z.boolean().optional()
|
|
@@ -753,12 +494,12 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
753
494
|
accountId: zod.z.number().int().positive(),
|
|
754
495
|
confirmed: zod.z.literal(true)
|
|
755
496
|
}).strict(),
|
|
756
|
-
zod.z.object({ op: zod.z.literal("auth.start"), provider: id }).strict(),
|
|
757
|
-
zod.z.object({ op: zod.z.literal("auth.status"), id }).strict(),
|
|
758
|
-
zod.z.object({ op: zod.z.literal("auth.input"), id, promptId: id, value: secret }).strict(),
|
|
759
|
-
zod.z.object({ op: zod.z.literal("auth.cancel"), id }).strict(),
|
|
760
|
-
zod.z.object({ op: zod.z.literal("auth.key"), provider: id, value: secret }).strict(),
|
|
761
|
-
zod.z.object({ op: zod.z.literal("auth.logout"), provider: id }).strict(),
|
|
497
|
+
zod.z.object({ op: zod.z.literal("auth.start"), provider: id$1 }).strict(),
|
|
498
|
+
zod.z.object({ op: zod.z.literal("auth.status"), id: id$1 }).strict(),
|
|
499
|
+
zod.z.object({ op: zod.z.literal("auth.input"), id: id$1, promptId: id$1, value: secret }).strict(),
|
|
500
|
+
zod.z.object({ op: zod.z.literal("auth.cancel"), id: id$1 }).strict(),
|
|
501
|
+
zod.z.object({ op: zod.z.literal("auth.key"), provider: id$1, value: secret }).strict(),
|
|
502
|
+
zod.z.object({ op: zod.z.literal("auth.logout"), provider: id$1 }).strict(),
|
|
762
503
|
zod.z.object({ op: zod.z.literal("settings.list") }).strict(),
|
|
763
504
|
zod.z.object({
|
|
764
505
|
op: zod.z.literal("settings.edit"),
|
|
@@ -876,6 +617,16 @@ function desktopEndpoint(request) {
|
|
|
876
617
|
...request.target ? { target: request.target } : {}
|
|
877
618
|
}
|
|
878
619
|
};
|
|
620
|
+
case "sessions.preview":
|
|
621
|
+
return {
|
|
622
|
+
path: "/v1/desktop/sessions/preview",
|
|
623
|
+
method: "POST",
|
|
624
|
+
body: {
|
|
625
|
+
request_id: request.requestId,
|
|
626
|
+
cwd: request.cwd,
|
|
627
|
+
...request.target ? { target: request.target } : {}
|
|
628
|
+
}
|
|
629
|
+
};
|
|
879
630
|
case "sessions.get":
|
|
880
631
|
return {
|
|
881
632
|
path: `/v1/desktop/sessions/${request.sessionId}`,
|
|
@@ -1262,18 +1013,305 @@ function desktopEndpoint(request) {
|
|
|
1262
1013
|
return { path: "/v1/config/system-prompt", method: "GET" };
|
|
1263
1014
|
case "instructions.update":
|
|
1264
1015
|
return {
|
|
1265
|
-
path: "/v1/config/system-prompt",
|
|
1266
|
-
method: "PATCH",
|
|
1267
|
-
body: { content: request.content }
|
|
1016
|
+
path: "/v1/config/system-prompt",
|
|
1017
|
+
method: "PATCH",
|
|
1018
|
+
body: { content: request.content }
|
|
1019
|
+
};
|
|
1020
|
+
case "credentials.list":
|
|
1021
|
+
return { path: "/v1/credentials", method: "GET" };
|
|
1022
|
+
case "credentials.update":
|
|
1023
|
+
return {
|
|
1024
|
+
path: "/v1/credentials",
|
|
1025
|
+
method: "PATCH",
|
|
1026
|
+
body: { key: request.key, value: request.value }
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
const MAX_PROBE_PATHS = 64;
|
|
1031
|
+
const MAX_FILE_READ_BYTES = 64 * 1024 * 1024;
|
|
1032
|
+
const id = zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/);
|
|
1033
|
+
const MAX_UPLOAD_BYTES = 16 * 1024 * 1024;
|
|
1034
|
+
const MAX_DOWNLOAD_BYTES = 32 * 1024 * 1024;
|
|
1035
|
+
const mediaRequestSchema = zod.z.discriminatedUnion("op", [
|
|
1036
|
+
zod.z.object({
|
|
1037
|
+
op: zod.z.literal("speech.create"),
|
|
1038
|
+
request: zod.z.record(zod.z.unknown())
|
|
1039
|
+
}).strict(),
|
|
1040
|
+
zod.z.object({
|
|
1041
|
+
op: zod.z.literal("speech.agent"),
|
|
1042
|
+
agentId: id,
|
|
1043
|
+
request: zod.z.record(zod.z.unknown())
|
|
1044
|
+
}).strict(),
|
|
1045
|
+
zod.z.object({
|
|
1046
|
+
op: zod.z.literal("transcription.create"),
|
|
1047
|
+
fileName: zod.z.string().min(1).max(255),
|
|
1048
|
+
mimeType: zod.z.string().min(1).max(128),
|
|
1049
|
+
fields: zod.z.record(zod.z.string().max(4096))
|
|
1050
|
+
}).strict(),
|
|
1051
|
+
zod.z.object({
|
|
1052
|
+
op: zod.z.literal("agent.import"),
|
|
1053
|
+
fileName: zod.z.string().min(1).max(255)
|
|
1054
|
+
}).strict(),
|
|
1055
|
+
// Export is a gated GET that answers with a ZIP, so it belongs on this relay
|
|
1056
|
+
// rather than the JSON transport: `desktopEndpoint`'s envelope has nowhere to
|
|
1057
|
+
// put binary. It is the only read here, which is why `endpoint()` below
|
|
1058
|
+
// returns a method instead of assuming POST.
|
|
1059
|
+
zod.z.object({ op: zod.z.literal("agent.export"), agentId: id }).strict(),
|
|
1060
|
+
// A durable transcript row references an image by content digest with the
|
|
1061
|
+
// payload stripped, so rendering a screenshot after a reload means fetching
|
|
1062
|
+
// bytes. That cannot go through the JSON transport (its envelope has nowhere
|
|
1063
|
+
// to put them), which is what puts it on this relay.
|
|
1064
|
+
//
|
|
1065
|
+
// Both identifiers are shape-constrained here as well as by the backend
|
|
1066
|
+
// route: this file's whole premise is that renderer code cannot pick a URL,
|
|
1067
|
+
// and a digest that reaches `endpoint()` unvalidated is renderer-controlled
|
|
1068
|
+
// path text.
|
|
1069
|
+
zod.z.object({
|
|
1070
|
+
op: zod.z.literal("sessions.attachment"),
|
|
1071
|
+
sessionId: zod.z.string().regex(/^[a-f0-9]{12}$/),
|
|
1072
|
+
digest: zod.z.string().regex(/^[a-f0-9]{32}$/)
|
|
1073
|
+
}).strict()
|
|
1074
|
+
]);
|
|
1075
|
+
function endpoint(request) {
|
|
1076
|
+
switch (request.op) {
|
|
1077
|
+
case "speech.create":
|
|
1078
|
+
return { path: "/v1/tools/speech", method: "POST" };
|
|
1079
|
+
case "speech.agent":
|
|
1080
|
+
return { path: `/v1/agents/${request.agentId}/speech`, method: "POST" };
|
|
1081
|
+
case "transcription.create":
|
|
1082
|
+
return { path: "/v1/transcriptions", method: "POST" };
|
|
1083
|
+
case "agent.import":
|
|
1084
|
+
return { path: "/v1/agents/import", method: "POST" };
|
|
1085
|
+
case "agent.export":
|
|
1086
|
+
return { path: `/v1/agents/${request.agentId}/export`, method: "GET" };
|
|
1087
|
+
case "sessions.attachment":
|
|
1088
|
+
return {
|
|
1089
|
+
path: `/v1/desktop/sessions/${request.sessionId}/attachments/${request.digest}`,
|
|
1090
|
+
method: "GET"
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
async function requestDesktopMedia(input, bytes, backendUrl, token) {
|
|
1095
|
+
const parsed = mediaRequestSchema.safeParse(input);
|
|
1096
|
+
if (!parsed.success) {
|
|
1097
|
+
return { status: 422, kind: "error", detail: "Invalid media operation." };
|
|
1098
|
+
}
|
|
1099
|
+
if (!token) {
|
|
1100
|
+
return {
|
|
1101
|
+
status: 503,
|
|
1102
|
+
kind: "error",
|
|
1103
|
+
detail: "Restart with a desktop-managed backend to use these controls."
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
const request = parsed.data;
|
|
1107
|
+
if (bytes && bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
1108
|
+
return { status: 413, kind: "error", detail: "This file is too large." };
|
|
1109
|
+
}
|
|
1110
|
+
const target2 = endpoint(request);
|
|
1111
|
+
let body;
|
|
1112
|
+
let contentType;
|
|
1113
|
+
if (request.op === "agent.export" || request.op === "sessions.attachment") {
|
|
1114
|
+
body = void 0;
|
|
1115
|
+
} else if (request.op === "speech.create" || request.op === "speech.agent") {
|
|
1116
|
+
body = JSON.stringify(request.request);
|
|
1117
|
+
contentType = "application/json";
|
|
1118
|
+
} else {
|
|
1119
|
+
if (!bytes) {
|
|
1120
|
+
return { status: 422, kind: "error", detail: "A file is required." };
|
|
1121
|
+
}
|
|
1122
|
+
const form = new FormData();
|
|
1123
|
+
const blob = new Blob([bytes], {
|
|
1124
|
+
type: request.op === "transcription.create" ? request.mimeType : "application/zip"
|
|
1125
|
+
});
|
|
1126
|
+
form.append("file", blob, request.fileName);
|
|
1127
|
+
if (request.op === "transcription.create") {
|
|
1128
|
+
for (const [key, value] of Object.entries(request.fields)) {
|
|
1129
|
+
form.append(key, value);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
body = form;
|
|
1133
|
+
}
|
|
1134
|
+
try {
|
|
1135
|
+
const response = await fetch(new URL(target2.path, backendUrl), {
|
|
1136
|
+
method: target2.method,
|
|
1137
|
+
headers: {
|
|
1138
|
+
Accept: "application/json, audio/*, image/*, application/octet-stream",
|
|
1139
|
+
...contentType ? { "Content-Type": contentType } : {},
|
|
1140
|
+
Authorization: `Bearer ${token}`
|
|
1141
|
+
},
|
|
1142
|
+
body,
|
|
1143
|
+
redirect: "error",
|
|
1144
|
+
signal: AbortSignal.timeout(12e4)
|
|
1145
|
+
});
|
|
1146
|
+
const responseType = response.headers.get("content-type") ?? "";
|
|
1147
|
+
if (!response.ok) {
|
|
1148
|
+
let detail = "The media request failed.";
|
|
1149
|
+
if (responseType.includes("application/json")) {
|
|
1150
|
+
try {
|
|
1151
|
+
const parsedError = await response.json();
|
|
1152
|
+
const text = parsedError.detail ?? parsedError.message;
|
|
1153
|
+
if (typeof text === "string") detail = text.slice(0, 512);
|
|
1154
|
+
} catch {
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return { status: response.status, kind: "error", detail };
|
|
1158
|
+
}
|
|
1159
|
+
if (responseType.includes("application/json")) {
|
|
1160
|
+
return {
|
|
1161
|
+
status: response.status,
|
|
1162
|
+
kind: "json",
|
|
1163
|
+
body: await response.json()
|
|
1268
1164
|
};
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1165
|
+
}
|
|
1166
|
+
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
1167
|
+
if (buffer.byteLength > MAX_DOWNLOAD_BYTES) {
|
|
1272
1168
|
return {
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1169
|
+
status: 502,
|
|
1170
|
+
kind: "error",
|
|
1171
|
+
detail: "The backend returned too much data."
|
|
1276
1172
|
};
|
|
1173
|
+
}
|
|
1174
|
+
return {
|
|
1175
|
+
status: response.status,
|
|
1176
|
+
kind: "bytes",
|
|
1177
|
+
mimeType: responseType || "application/octet-stream",
|
|
1178
|
+
data: buffer
|
|
1179
|
+
};
|
|
1180
|
+
} catch {
|
|
1181
|
+
return {
|
|
1182
|
+
status: 503,
|
|
1183
|
+
kind: "error",
|
|
1184
|
+
detail: "The backend could not complete this request. Check its connection and try again."
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
const SESSION_ID = /^[a-f0-9]{12}$/;
|
|
1189
|
+
const SAFE_ID = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
1190
|
+
const MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
|
1191
|
+
class DesktopStreamRelay {
|
|
1192
|
+
constructor(backendUrl, token) {
|
|
1193
|
+
this.backendUrl = backendUrl;
|
|
1194
|
+
this.token = token;
|
|
1195
|
+
}
|
|
1196
|
+
streams = /* @__PURE__ */ new Map();
|
|
1197
|
+
/** Frame observer for main-owned side effects (notifications). It sees
|
|
1198
|
+
* every parsed frame before the renderer does, on the same subscription. */
|
|
1199
|
+
observer = null;
|
|
1200
|
+
observe(observer) {
|
|
1201
|
+
this.observer = observer;
|
|
1202
|
+
}
|
|
1203
|
+
/** True when this relay can authenticate; a false answer is why the UI
|
|
1204
|
+
* shows the pairing/setup state rather than a dead transcript. */
|
|
1205
|
+
get available() {
|
|
1206
|
+
return Boolean(this.token);
|
|
1207
|
+
}
|
|
1208
|
+
subscribe(args, emit) {
|
|
1209
|
+
if (!SESSION_ID.test(args.sessionId)) {
|
|
1210
|
+
throw new Error("Invalid session for streaming.");
|
|
1211
|
+
}
|
|
1212
|
+
if (args.epoch !== void 0 && !SAFE_ID.test(args.epoch)) {
|
|
1213
|
+
throw new Error("Invalid stream epoch.");
|
|
1214
|
+
}
|
|
1215
|
+
if (args.afterSeq !== void 0 && (!Number.isInteger(args.afterSeq) || args.afterSeq < 0)) {
|
|
1216
|
+
throw new Error("Invalid stream cursor.");
|
|
1217
|
+
}
|
|
1218
|
+
if (!this.token) {
|
|
1219
|
+
throw new Error("This backend was not started with desktop controls.");
|
|
1220
|
+
}
|
|
1221
|
+
const streamId = node_crypto.randomBytes(16).toString("hex");
|
|
1222
|
+
const controller = new AbortController();
|
|
1223
|
+
this.streams.set(streamId, controller);
|
|
1224
|
+
const query = new URLSearchParams();
|
|
1225
|
+
if (args.epoch) query.set("epoch", args.epoch);
|
|
1226
|
+
if (args.afterSeq !== void 0)
|
|
1227
|
+
query.set("after_seq", String(args.afterSeq));
|
|
1228
|
+
const suffix = query.size > 0 ? `?${query}` : "";
|
|
1229
|
+
const url = new URL(
|
|
1230
|
+
`/v1/desktop/sessions/${args.sessionId}/events${suffix}`,
|
|
1231
|
+
this.backendUrl
|
|
1232
|
+
);
|
|
1233
|
+
void this.pump(
|
|
1234
|
+
streamId,
|
|
1235
|
+
args.sessionId,
|
|
1236
|
+
url,
|
|
1237
|
+
controller.signal,
|
|
1238
|
+
emit
|
|
1239
|
+
).finally(() => {
|
|
1240
|
+
this.streams.delete(streamId);
|
|
1241
|
+
});
|
|
1242
|
+
return { streamId };
|
|
1243
|
+
}
|
|
1244
|
+
async pump(streamId, sessionId2, url, signal, emit) {
|
|
1245
|
+
try {
|
|
1246
|
+
const response = await fetch(url, {
|
|
1247
|
+
headers: {
|
|
1248
|
+
Accept: "text/event-stream",
|
|
1249
|
+
Authorization: `Bearer ${this.token}`
|
|
1250
|
+
},
|
|
1251
|
+
redirect: "error",
|
|
1252
|
+
signal
|
|
1253
|
+
});
|
|
1254
|
+
if (!response.ok || !response.body) {
|
|
1255
|
+
emit({
|
|
1256
|
+
streamId,
|
|
1257
|
+
kind: "error",
|
|
1258
|
+
detail: `The event stream was refused (${response.status}).`
|
|
1259
|
+
});
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
const reader = response.body.getReader();
|
|
1263
|
+
const decoder = new TextDecoder();
|
|
1264
|
+
let buffer = "";
|
|
1265
|
+
for (; ; ) {
|
|
1266
|
+
const { done, value } = await reader.read();
|
|
1267
|
+
if (done) break;
|
|
1268
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1269
|
+
for (; ; ) {
|
|
1270
|
+
const boundary = buffer.indexOf("\n\n");
|
|
1271
|
+
if (boundary < 0) {
|
|
1272
|
+
if (buffer.length > MAX_FRAME_BYTES) {
|
|
1273
|
+
emit({
|
|
1274
|
+
streamId,
|
|
1275
|
+
kind: "error",
|
|
1276
|
+
detail: "The event stream exceeded its frame budget."
|
|
1277
|
+
});
|
|
1278
|
+
reader.cancel().catch(() => void 0);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1283
|
+
const record = buffer.slice(0, boundary);
|
|
1284
|
+
buffer = buffer.slice(boundary + 2);
|
|
1285
|
+
for (const line of record.split("\n")) {
|
|
1286
|
+
if (line.startsWith("data:")) {
|
|
1287
|
+
const data = line.slice(5).replace(/^ /, "");
|
|
1288
|
+
this.observer?.(sessionId2, data);
|
|
1289
|
+
emit({ streamId, kind: "data", data });
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
emit({ streamId, kind: "end" });
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
if (signal.aborted) {
|
|
1297
|
+
emit({ streamId, kind: "end" });
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
emit({
|
|
1301
|
+
streamId,
|
|
1302
|
+
kind: "error",
|
|
1303
|
+
detail: error instanceof Error && error.name === "AbortError" ? "The event stream ended." : "The event stream connection failed."
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
unsubscribe(streamId) {
|
|
1308
|
+
if (!/^[a-f0-9]{32}$/.test(streamId)) return;
|
|
1309
|
+
this.streams.get(streamId)?.abort();
|
|
1310
|
+
this.streams.delete(streamId);
|
|
1311
|
+
}
|
|
1312
|
+
dispose() {
|
|
1313
|
+
for (const controller of this.streams.values()) controller.abort();
|
|
1314
|
+
this.streams.clear();
|
|
1277
1315
|
}
|
|
1278
1316
|
}
|
|
1279
1317
|
async function requestDesktop(input, backendUrl, token) {
|
|
@@ -7492,6 +7530,14 @@ function describeWindowLaunch(plan) {
|
|
|
7492
7530
|
return `window mode ${plan.mode}: ${plan.width}x${plan.height}, ${behaviour}, page throttling ${plan.backgroundThrottling ? "on" : "off"}`;
|
|
7493
7531
|
}
|
|
7494
7532
|
const BASE64_FILE_EXTENSIONS = ["csv", "tsv", "xls", "xlsx", "ods"];
|
|
7533
|
+
const resolveUserPath = (filePath, cwd) => {
|
|
7534
|
+
if (filePath === "~") return electron.app.getPath("home");
|
|
7535
|
+
if (filePath.startsWith("~/"))
|
|
7536
|
+
return path$1.join(electron.app.getPath("home"), filePath.slice(2));
|
|
7537
|
+
if (cwd && !filePath.startsWith("/"))
|
|
7538
|
+
return path$1.join(cwd.startsWith("~/") ? resolveUserPath(cwd) : cwd, filePath);
|
|
7539
|
+
return filePath;
|
|
7540
|
+
};
|
|
7495
7541
|
electron.app.setName("Local Operator");
|
|
7496
7542
|
const image = electron.nativeImage.createFromPath(icon);
|
|
7497
7543
|
if (process.platform === "darwin" && electron.app.dock) {
|
|
@@ -7856,8 +7902,7 @@ electron.app.whenReady().then(async () => {
|
|
|
7856
7902
|
"read-file",
|
|
7857
7903
|
async (_, filePath, encoding = "utf-8") => {
|
|
7858
7904
|
try {
|
|
7859
|
-
const
|
|
7860
|
-
const data = fs.readFileSync(normalizedPath, encoding);
|
|
7905
|
+
const data = fs.readFileSync(resolveUserPath(filePath), encoding);
|
|
7861
7906
|
return { success: true, data };
|
|
7862
7907
|
} catch (error) {
|
|
7863
7908
|
logger.error("Error reading file:", LogFileType.BACKEND, error);
|
|
@@ -7865,6 +7910,82 @@ electron.app.whenReady().then(async () => {
|
|
|
7865
7910
|
}
|
|
7866
7911
|
}
|
|
7867
7912
|
);
|
|
7913
|
+
electron.ipcMain.handle(
|
|
7914
|
+
"probe-files",
|
|
7915
|
+
async (_, paths, cwd) => {
|
|
7916
|
+
const asked = Array.isArray(paths) ? paths.filter((path2) => typeof path2 === "string") : [];
|
|
7917
|
+
return asked.slice(0, MAX_PROBE_PATHS).map((input) => {
|
|
7918
|
+
let resolved = input;
|
|
7919
|
+
try {
|
|
7920
|
+
resolved = resolveUserPath(input, cwd);
|
|
7921
|
+
const stat = fs.statSync(resolved, { throwIfNoEntry: false });
|
|
7922
|
+
return {
|
|
7923
|
+
input,
|
|
7924
|
+
resolved,
|
|
7925
|
+
exists: stat !== void 0,
|
|
7926
|
+
isFile: stat?.isFile() ?? false,
|
|
7927
|
+
sizeBytes: stat?.isFile() ? stat.size : null,
|
|
7928
|
+
mtimeMs: stat?.isFile() ? stat.mtimeMs : null
|
|
7929
|
+
};
|
|
7930
|
+
} catch (error) {
|
|
7931
|
+
return {
|
|
7932
|
+
input,
|
|
7933
|
+
resolved,
|
|
7934
|
+
exists: false,
|
|
7935
|
+
isFile: false,
|
|
7936
|
+
sizeBytes: null,
|
|
7937
|
+
mtimeMs: null,
|
|
7938
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7939
|
+
};
|
|
7940
|
+
}
|
|
7941
|
+
});
|
|
7942
|
+
}
|
|
7943
|
+
);
|
|
7944
|
+
electron.ipcMain.handle(
|
|
7945
|
+
"read-file-bytes",
|
|
7946
|
+
async (_, filePath, maxBytes = MAX_FILE_READ_BYTES) => {
|
|
7947
|
+
const cap = Number.isFinite(maxBytes) && maxBytes > 0 ? Math.min(maxBytes, MAX_FILE_READ_BYTES) : MAX_FILE_READ_BYTES;
|
|
7948
|
+
try {
|
|
7949
|
+
const resolved = resolveUserPath(filePath);
|
|
7950
|
+
const stat = fs.statSync(resolved, { throwIfNoEntry: false });
|
|
7951
|
+
if (!stat) {
|
|
7952
|
+
return {
|
|
7953
|
+
success: false,
|
|
7954
|
+
code: "not-found",
|
|
7955
|
+
error: `No file at ${resolved}`
|
|
7956
|
+
};
|
|
7957
|
+
}
|
|
7958
|
+
if (!stat.isFile()) {
|
|
7959
|
+
return {
|
|
7960
|
+
success: false,
|
|
7961
|
+
code: "not-a-file",
|
|
7962
|
+
error: `${resolved} is not a regular file`
|
|
7963
|
+
};
|
|
7964
|
+
}
|
|
7965
|
+
if (stat.size > cap) {
|
|
7966
|
+
return {
|
|
7967
|
+
success: false,
|
|
7968
|
+
code: "too-large",
|
|
7969
|
+
error: `${resolved} is ${stat.size} bytes, over the ${cap}-byte preview cap`,
|
|
7970
|
+
sizeBytes: stat.size
|
|
7971
|
+
};
|
|
7972
|
+
}
|
|
7973
|
+
const buffer = fs.readFileSync(resolved);
|
|
7974
|
+
return {
|
|
7975
|
+
success: true,
|
|
7976
|
+
data: new Uint8Array(buffer),
|
|
7977
|
+
sizeBytes: buffer.byteLength
|
|
7978
|
+
};
|
|
7979
|
+
} catch (error) {
|
|
7980
|
+
logger.error("Error reading file bytes:", LogFileType.BACKEND, error);
|
|
7981
|
+
return {
|
|
7982
|
+
success: false,
|
|
7983
|
+
code: "unreadable",
|
|
7984
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7985
|
+
};
|
|
7986
|
+
}
|
|
7987
|
+
}
|
|
7988
|
+
);
|
|
7868
7989
|
electron.ipcMain.handle("open-external", async (_, url) => {
|
|
7869
7990
|
try {
|
|
7870
7991
|
await electron.shell.openExternal(url);
|
|
@@ -7883,8 +8004,7 @@ electron.app.whenReady().then(async () => {
|
|
|
7883
8004
|
"save-file",
|
|
7884
8005
|
async (_, filePath, content, encoding = "utf-8") => {
|
|
7885
8006
|
try {
|
|
7886
|
-
|
|
7887
|
-
fs.writeFileSync(normalizedPath, content, encoding);
|
|
8007
|
+
fs.writeFileSync(resolveUserPath(filePath), content, encoding);
|
|
7888
8008
|
} catch (error) {
|
|
7889
8009
|
logger.error("Error saving file:", LogFileType.BACKEND, error);
|
|
7890
8010
|
throw error;
|
|
@@ -7892,14 +8012,13 @@ electron.app.whenReady().then(async () => {
|
|
|
7892
8012
|
}
|
|
7893
8013
|
);
|
|
7894
8014
|
electron.ipcMain.handle("file-exists", async (_, filePath) => {
|
|
7895
|
-
|
|
7896
|
-
return fs.existsSync(normalizedPath);
|
|
8015
|
+
return fs.existsSync(resolveUserPath(filePath));
|
|
7897
8016
|
});
|
|
7898
8017
|
electron.ipcMain.handle("directory-exists", async (_, dirPath) => {
|
|
7899
|
-
const home = electron.app.getPath("home");
|
|
7900
|
-
const normalizedPath = dirPath === "~" ? home : dirPath.startsWith("~/") ? path$1.join(home, dirPath.slice(2)) : dirPath;
|
|
7901
8018
|
try {
|
|
7902
|
-
return fs.statSync(
|
|
8019
|
+
return fs.statSync(resolveUserPath(dirPath), {
|
|
8020
|
+
throwIfNoEntry: false
|
|
8021
|
+
})?.isDirectory() ?? false;
|
|
7903
8022
|
} catch {
|
|
7904
8023
|
return false;
|
|
7905
8024
|
}
|