@sideboard-ai/core 0.1.275 → 0.1.276
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/dist/index.cjs +355 -62
- package/dist/index.d.cts +22 -2
- package/dist/index.d.ts +22 -2
- package/dist/index.js +348 -61
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -38738,12 +38738,61 @@ init_global_workspace();
|
|
|
38738
38738
|
init_thread_store();
|
|
38739
38739
|
init_implied_first_prompt();
|
|
38740
38740
|
|
|
38741
|
+
// src/remote/phone-media.ts
|
|
38742
|
+
init_diff();
|
|
38743
|
+
var PHONE_IMAGE_MAX_BYTES = 15e5;
|
|
38744
|
+
var IMAGE_MIME = {
|
|
38745
|
+
png: "image/png",
|
|
38746
|
+
jpg: "image/jpeg",
|
|
38747
|
+
jpeg: "image/jpeg",
|
|
38748
|
+
gif: "image/gif",
|
|
38749
|
+
webp: "image/webp",
|
|
38750
|
+
bmp: "image/bmp"
|
|
38751
|
+
};
|
|
38752
|
+
function phoneWorktreeRelative(worktreePath, raw) {
|
|
38753
|
+
let text6 = raw.trim();
|
|
38754
|
+
if (!text6) return null;
|
|
38755
|
+
if (/^file:\/\//i.test(text6)) {
|
|
38756
|
+
try {
|
|
38757
|
+
text6 = decodeURIComponent(new URL(text6).pathname);
|
|
38758
|
+
} catch {
|
|
38759
|
+
return null;
|
|
38760
|
+
}
|
|
38761
|
+
}
|
|
38762
|
+
text6 = (text6.split("#")[0] ?? text6).replace(/\\/g, "/");
|
|
38763
|
+
if (!text6 || text6.includes("\0") || text6.split("/").includes("..")) return null;
|
|
38764
|
+
const root = worktreePath.replace(/\/+$/, "");
|
|
38765
|
+
if (!root || root === "/") return null;
|
|
38766
|
+
if (text6.startsWith("/")) {
|
|
38767
|
+
if (text6 !== root && !text6.startsWith(`${root}/`)) return null;
|
|
38768
|
+
text6 = text6 === root ? "" : text6.slice(root.length + 1);
|
|
38769
|
+
}
|
|
38770
|
+
text6 = text6.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
38771
|
+
if (!text6 || text6 === "." || text6.startsWith("/")) return null;
|
|
38772
|
+
return text6;
|
|
38773
|
+
}
|
|
38774
|
+
function phoneImageMime(relativePath) {
|
|
38775
|
+
const ext = relativePath.split(".").pop()?.toLowerCase() ?? "";
|
|
38776
|
+
return IMAGE_MIME[ext] ?? null;
|
|
38777
|
+
}
|
|
38778
|
+
function phoneImageDataUrl(worktreePath, raw) {
|
|
38779
|
+
const relative = phoneWorktreeRelative(worktreePath, raw);
|
|
38780
|
+
if (!relative) return null;
|
|
38781
|
+
const mime = phoneImageMime(relative);
|
|
38782
|
+
if (!mime) return null;
|
|
38783
|
+
try {
|
|
38784
|
+
const file = readWorktreeFileForUpload(worktreePath, relative, { maxBytes: PHONE_IMAGE_MAX_BYTES });
|
|
38785
|
+
return `data:${mime};base64,${file.contentBase64}`;
|
|
38786
|
+
} catch {
|
|
38787
|
+
return null;
|
|
38788
|
+
}
|
|
38789
|
+
}
|
|
38790
|
+
|
|
38741
38791
|
// src/remote/phone-chats.ts
|
|
38742
38792
|
var import_node_crypto25 = require("crypto");
|
|
38743
38793
|
init_list_models();
|
|
38744
38794
|
init_home_board();
|
|
38745
38795
|
init_stage_files();
|
|
38746
|
-
init_worktree();
|
|
38747
38796
|
init_worktree_labels();
|
|
38748
38797
|
init_thinking_effort();
|
|
38749
38798
|
init_app_settings();
|
|
@@ -38807,6 +38856,20 @@ function parsePhoneControl(value) {
|
|
|
38807
38856
|
const chatId = typeof value.chatId === "string" ? value.chatId.trim() : "";
|
|
38808
38857
|
if (!chatId) return "invalid";
|
|
38809
38858
|
if (op === "open" || op === "archive" || op === "restore" || op === "stop") return { op, chatId };
|
|
38859
|
+
if (op === "open-file" || op === "media") {
|
|
38860
|
+
const path2 = textField(value, "path");
|
|
38861
|
+
if (!path2 || path2.length > 2e3) return "invalid";
|
|
38862
|
+
if (op === "media") return { op, chatId, path: path2 };
|
|
38863
|
+
const startLine = lineField(value, "startLine");
|
|
38864
|
+
const endLine = lineField(value, "endLine");
|
|
38865
|
+
return {
|
|
38866
|
+
op,
|
|
38867
|
+
chatId,
|
|
38868
|
+
path: path2,
|
|
38869
|
+
...startLine != null ? { startLine } : {},
|
|
38870
|
+
...endLine != null ? { endLine } : {}
|
|
38871
|
+
};
|
|
38872
|
+
}
|
|
38810
38873
|
const draft = parsePhoneDraft(value);
|
|
38811
38874
|
if (draft === "invalid") return "invalid";
|
|
38812
38875
|
if (op === "options") return { op: "options", chatId, ...draft };
|
|
@@ -38822,6 +38885,11 @@ function textField(value, key) {
|
|
|
38822
38885
|
const raw = value[key];
|
|
38823
38886
|
return typeof raw === "string" ? raw.trim() : "";
|
|
38824
38887
|
}
|
|
38888
|
+
function lineField(value, key) {
|
|
38889
|
+
const raw = value[key];
|
|
38890
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1 || raw > 1e6) return void 0;
|
|
38891
|
+
return raw;
|
|
38892
|
+
}
|
|
38825
38893
|
function agentField(value) {
|
|
38826
38894
|
const agent = textField(value, "agent");
|
|
38827
38895
|
if (!agent) return void 0;
|
|
@@ -39061,13 +39129,28 @@ function listPhoneHistory(opts) {
|
|
|
39061
39129
|
...more && last ? { next: historyCursor(last) } : {}
|
|
39062
39130
|
};
|
|
39063
39131
|
}
|
|
39064
|
-
function
|
|
39132
|
+
function matchPhoneThread(chatId) {
|
|
39065
39133
|
const id = chatId.trim();
|
|
39066
39134
|
if (!id) return null;
|
|
39067
|
-
const
|
|
39135
|
+
const exact = readThread(id);
|
|
39136
|
+
if (exact) return exact;
|
|
39137
|
+
const match = findThreadByRef(id);
|
|
39138
|
+
if (!match) return null;
|
|
39139
|
+
if (match.id !== id && !match.id.startsWith(id)) return null;
|
|
39140
|
+
return match;
|
|
39141
|
+
}
|
|
39142
|
+
function visiblePhoneThread(thread) {
|
|
39143
|
+
if (isGlobalThread(thread)) return true;
|
|
39144
|
+
return isProjectPath(thread.repoPath);
|
|
39145
|
+
}
|
|
39146
|
+
function findPhoneChat(chatId) {
|
|
39147
|
+
const thread = matchPhoneThread(chatId);
|
|
39148
|
+
if (!thread || !visiblePhoneThread(thread)) return null;
|
|
39149
|
+
return thread;
|
|
39150
|
+
}
|
|
39151
|
+
function readPhoneThread(chatId) {
|
|
39152
|
+
const thread = findPhoneChat(chatId);
|
|
39068
39153
|
if (!thread || thread.status === "archived") return null;
|
|
39069
|
-
if (isGlobalThread(thread)) return thread;
|
|
39070
|
-
if (!isProjectPath(thread.repoPath)) return null;
|
|
39071
39154
|
return thread;
|
|
39072
39155
|
}
|
|
39073
39156
|
function phoneComposerOptions(thread) {
|
|
@@ -39213,6 +39296,22 @@ async function createPhoneProjectWorktree(repoPathOrInput) {
|
|
|
39213
39296
|
});
|
|
39214
39297
|
return openedFrom(thread, projectPlace(thread));
|
|
39215
39298
|
}
|
|
39299
|
+
async function listPhoneModels(agent) {
|
|
39300
|
+
const catalogs = await listModelsForAgent(agent);
|
|
39301
|
+
return (catalogs[0]?.models ?? []).slice(0, 40).map((model) => ({
|
|
39302
|
+
id: model.id,
|
|
39303
|
+
label: model.displayName || model.id
|
|
39304
|
+
}));
|
|
39305
|
+
}
|
|
39306
|
+
function archivePhoneChat(chatId) {
|
|
39307
|
+
const thread = readPhoneThread(chatId);
|
|
39308
|
+
if (!thread) return false;
|
|
39309
|
+
updateThread(thread.id, { status: "archived" });
|
|
39310
|
+
return true;
|
|
39311
|
+
}
|
|
39312
|
+
|
|
39313
|
+
// src/remote/phone-sources.ts
|
|
39314
|
+
init_worktree();
|
|
39216
39315
|
function sourceWarning(label, err) {
|
|
39217
39316
|
const message = err instanceof Error ? err.message : String(err);
|
|
39218
39317
|
return message ? `${label}: ${message}` : `${label} failed.`;
|
|
@@ -39255,60 +39354,42 @@ async function listPhoneSources(repoPath, query) {
|
|
|
39255
39354
|
...warnings.length ? { warnings } : {}
|
|
39256
39355
|
};
|
|
39257
39356
|
}
|
|
39258
|
-
async function listPhoneModels(agent) {
|
|
39259
|
-
const catalogs = await listModelsForAgent(agent);
|
|
39260
|
-
return (catalogs[0]?.models ?? []).slice(0, 40).map((model) => ({
|
|
39261
|
-
id: model.id,
|
|
39262
|
-
label: model.displayName || model.id
|
|
39263
|
-
}));
|
|
39264
|
-
}
|
|
39265
|
-
function archivePhoneChat(chatId) {
|
|
39266
|
-
const thread = readPhoneThread(chatId);
|
|
39267
|
-
if (!thread) return false;
|
|
39268
|
-
updateThread(thread.id, { status: "archived" });
|
|
39269
|
-
return true;
|
|
39270
|
-
}
|
|
39271
39357
|
|
|
39272
|
-
// src/remote/
|
|
39273
|
-
|
|
39274
|
-
|
|
39275
|
-
return `Phone
|
|
39358
|
+
// src/remote/phone-live.ts
|
|
39359
|
+
init_orchestrator();
|
|
39360
|
+
init_thread_store();
|
|
39276
39361
|
|
|
39277
|
-
|
|
39362
|
+
// src/remote/phone-stream.ts
|
|
39363
|
+
init_message_parts();
|
|
39364
|
+
var PHONE_STREAM_FLUSH_MS = 80;
|
|
39365
|
+
function phoneStreamFrame(parts) {
|
|
39366
|
+
return {
|
|
39367
|
+
text: visibleAssistantText(void 0, parts),
|
|
39368
|
+
activity: liveActivitySummary(parts)
|
|
39369
|
+
};
|
|
39278
39370
|
}
|
|
39279
|
-
|
|
39280
|
-
|
|
39371
|
+
var SKIPPED = /* @__PURE__ */ new Set(["session_id", "usage", "stderr", "exit"]);
|
|
39372
|
+
function phoneStreamParts(parts, event) {
|
|
39373
|
+
if (SKIPPED.has(event.type)) return null;
|
|
39374
|
+
return applyAgentEvent(parts, event);
|
|
39281
39375
|
}
|
|
39282
|
-
|
|
39283
|
-
|
|
39376
|
+
function phoneStreamFlushNow(event) {
|
|
39377
|
+
return event.type === "tool_use" || event.type === "tool_result" && !event.partial;
|
|
39378
|
+
}
|
|
39379
|
+
|
|
39380
|
+
// src/remote/phone-live.ts
|
|
39381
|
+
var REMOTE_STOPPED_REPLY = "Sideboard stopped the in-progress turn.";
|
|
39284
39382
|
var chatChains = /* @__PURE__ */ new Map();
|
|
39285
39383
|
var chatGeneration = /* @__PURE__ */ new Map();
|
|
39286
|
-
|
|
39287
|
-
const run2 = handleChain2.then(fn, fn);
|
|
39288
|
-
handleChain2 = run2.then(
|
|
39289
|
-
() => void 0,
|
|
39290
|
-
() => void 0
|
|
39291
|
-
);
|
|
39292
|
-
}
|
|
39293
|
-
function interruptRemoteCoordinator(deviceId, log = () => void 0) {
|
|
39294
|
-
try {
|
|
39295
|
-
const live = findRemoteCoordinator(deviceId);
|
|
39296
|
-
if (!live) return false;
|
|
39297
|
-
const fresh = readThread(live.id) ?? live;
|
|
39298
|
-
if (fresh.status !== "running" && fresh.status !== "queued") return false;
|
|
39299
|
-
getOrchestrator().stop(fresh.id, { clearQueue: true });
|
|
39300
|
-
log(`interrupt phone \u2192 coordinator ${fresh.id.slice(0, 8)} (${fresh.status})`);
|
|
39301
|
-
return true;
|
|
39302
|
-
} catch (err) {
|
|
39303
|
-
log(`interrupt: ${err instanceof Error ? err.message : String(err)}`);
|
|
39304
|
-
return false;
|
|
39305
|
-
}
|
|
39306
|
-
}
|
|
39384
|
+
var phoneTurnGen = /* @__PURE__ */ new Map();
|
|
39307
39385
|
function bumpChat(chatId) {
|
|
39308
39386
|
const next = (chatGeneration.get(chatId) ?? 0) + 1;
|
|
39309
39387
|
chatGeneration.set(chatId, next);
|
|
39310
39388
|
return next;
|
|
39311
39389
|
}
|
|
39390
|
+
function finishPhoneTurn(chatId, generation) {
|
|
39391
|
+
if (phoneTurnGen.get(chatId) === generation) phoneTurnGen.delete(chatId);
|
|
39392
|
+
}
|
|
39312
39393
|
function enqueueChat(chatId, fn) {
|
|
39313
39394
|
const prev = chatChains.get(chatId) ?? Promise.resolve();
|
|
39314
39395
|
const run2 = prev.then(fn, fn);
|
|
@@ -39323,6 +39404,49 @@ function enqueueChat(chatId, fn) {
|
|
|
39323
39404
|
function emitControl(opts, reply) {
|
|
39324
39405
|
opts.onOutbound({ type: "assistant", text: encodePhoneControl(reply) });
|
|
39325
39406
|
}
|
|
39407
|
+
function watchPhoneStream(opts, chatId, threadId, generation) {
|
|
39408
|
+
let parts = [];
|
|
39409
|
+
let timer = null;
|
|
39410
|
+
let lastKey = "";
|
|
39411
|
+
let closed = false;
|
|
39412
|
+
const alive = () => !closed && chatGeneration.get(chatId) === generation;
|
|
39413
|
+
const flush2 = () => {
|
|
39414
|
+
timer = null;
|
|
39415
|
+
if (!alive()) return;
|
|
39416
|
+
const frame = phoneStreamFrame(parts);
|
|
39417
|
+
const key = `${frame.activity}
|
|
39418
|
+
${frame.text}`;
|
|
39419
|
+
if (key === lastKey) return;
|
|
39420
|
+
if (!frame.text && !frame.activity) return;
|
|
39421
|
+
lastKey = key;
|
|
39422
|
+
emitControl(opts, { op: "stream", chatId, text: frame.text, activity: frame.activity });
|
|
39423
|
+
};
|
|
39424
|
+
const schedule = (event) => {
|
|
39425
|
+
if (phoneStreamFlushNow(event) || lastKey === "") {
|
|
39426
|
+
if (timer) {
|
|
39427
|
+
clearTimeout(timer);
|
|
39428
|
+
timer = null;
|
|
39429
|
+
}
|
|
39430
|
+
flush2();
|
|
39431
|
+
return;
|
|
39432
|
+
}
|
|
39433
|
+
if (timer) return;
|
|
39434
|
+
timer = setTimeout(flush2, PHONE_STREAM_FLUSH_MS);
|
|
39435
|
+
};
|
|
39436
|
+
const off = getOrchestrator().on((event) => {
|
|
39437
|
+
if (!alive()) return;
|
|
39438
|
+
if (event.type !== "turn_output" || event.threadId !== threadId) return;
|
|
39439
|
+
const next = phoneStreamParts(parts, event.event);
|
|
39440
|
+
if (!next) return;
|
|
39441
|
+
parts = next;
|
|
39442
|
+
schedule(event.event);
|
|
39443
|
+
});
|
|
39444
|
+
return () => {
|
|
39445
|
+
closed = true;
|
|
39446
|
+
if (timer) clearTimeout(timer);
|
|
39447
|
+
off();
|
|
39448
|
+
};
|
|
39449
|
+
}
|
|
39326
39450
|
function interruptChat(chatId, log) {
|
|
39327
39451
|
try {
|
|
39328
39452
|
const thread = readPhoneThread(chatId);
|
|
@@ -39378,20 +39502,73 @@ function phoneError(err) {
|
|
|
39378
39502
|
function deliverPhoneTurn(opts, chatId) {
|
|
39379
39503
|
const thread = readPhoneThread(chatId);
|
|
39380
39504
|
if (!thread || thread.status !== "running" && thread.status !== "queued") return;
|
|
39381
|
-
const
|
|
39382
|
-
|
|
39383
|
-
|
|
39505
|
+
const id = thread.id;
|
|
39506
|
+
const claimed = phoneTurnGen.get(id);
|
|
39507
|
+
if (claimed != null && claimed === chatGeneration.get(id)) return;
|
|
39508
|
+
const generation = bumpChat(id);
|
|
39509
|
+
phoneTurnGen.set(id, generation);
|
|
39510
|
+
enqueueChat(id, async () => {
|
|
39511
|
+
if (chatGeneration.get(id) !== generation) return;
|
|
39512
|
+
const stopStream = watchPhoneStream(opts, id, id, generation);
|
|
39384
39513
|
try {
|
|
39385
|
-
await getOrchestrator().waitForTurn(
|
|
39514
|
+
await getOrchestrator().waitForTurn(id, 14 * 60 * 1e3);
|
|
39386
39515
|
} catch (err) {
|
|
39387
|
-
|
|
39388
|
-
|
|
39516
|
+
stopStream();
|
|
39517
|
+
finishPhoneTurn(id, generation);
|
|
39518
|
+
if (chatGeneration.get(id) !== generation) return;
|
|
39519
|
+
emitControl(opts, { op: "error", chatId: id, message: `Sideboard failed: ${phoneError(err)}` });
|
|
39389
39520
|
return;
|
|
39390
39521
|
}
|
|
39391
|
-
|
|
39392
|
-
|
|
39522
|
+
stopStream();
|
|
39523
|
+
finishPhoneTurn(id, generation);
|
|
39524
|
+
if (chatGeneration.get(id) !== generation) return;
|
|
39525
|
+
emitChatTurn(opts, id, id);
|
|
39393
39526
|
});
|
|
39394
39527
|
}
|
|
39528
|
+
|
|
39529
|
+
// src/remote/listen.ts
|
|
39530
|
+
function formatRemotePrompt(text6) {
|
|
39531
|
+
return `Phone
|
|
39532
|
+
|
|
39533
|
+
${text6.trim()}`;
|
|
39534
|
+
}
|
|
39535
|
+
function isRemoteStopCommand(text6) {
|
|
39536
|
+
return text6.trim().toLowerCase() === "stop";
|
|
39537
|
+
}
|
|
39538
|
+
var handleChain2 = Promise.resolve();
|
|
39539
|
+
var inboundGeneration = 0;
|
|
39540
|
+
function enqueue(fn) {
|
|
39541
|
+
const run2 = handleChain2.then(fn, fn);
|
|
39542
|
+
handleChain2 = run2.then(
|
|
39543
|
+
() => void 0,
|
|
39544
|
+
() => void 0
|
|
39545
|
+
);
|
|
39546
|
+
}
|
|
39547
|
+
function interruptRemoteCoordinator(deviceId, log = () => void 0) {
|
|
39548
|
+
try {
|
|
39549
|
+
const live = findRemoteCoordinator(deviceId);
|
|
39550
|
+
if (!live) return false;
|
|
39551
|
+
const fresh = readThread(live.id) ?? live;
|
|
39552
|
+
if (fresh.status !== "running" && fresh.status !== "queued") return false;
|
|
39553
|
+
getOrchestrator().stop(fresh.id, { clearQueue: true });
|
|
39554
|
+
log(`interrupt phone \u2192 coordinator ${fresh.id.slice(0, 8)} (${fresh.status})`);
|
|
39555
|
+
return true;
|
|
39556
|
+
} catch (err) {
|
|
39557
|
+
log(`interrupt: ${err instanceof Error ? err.message : String(err)}`);
|
|
39558
|
+
return false;
|
|
39559
|
+
}
|
|
39560
|
+
}
|
|
39561
|
+
function publishOpenedChat(opts, chatId) {
|
|
39562
|
+
const opened = openPhoneChat(chatId);
|
|
39563
|
+
if (!opened) {
|
|
39564
|
+
emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
|
|
39565
|
+
return;
|
|
39566
|
+
}
|
|
39567
|
+
emitControl(opts, { op: "opened", ...opened });
|
|
39568
|
+
if (opened.chat.status === "running" || opened.chat.status === "queued") {
|
|
39569
|
+
deliverPhoneTurn(opts, opened.chat.id);
|
|
39570
|
+
}
|
|
39571
|
+
}
|
|
39395
39572
|
function applyPhoneOptions(chatId, draft) {
|
|
39396
39573
|
const patch = {};
|
|
39397
39574
|
if (draft.agent) patch.agent = draft.agent;
|
|
@@ -39542,12 +39719,21 @@ function handlePhoneControl(cmd, opts) {
|
|
|
39542
39719
|
return;
|
|
39543
39720
|
}
|
|
39544
39721
|
if (cmd.op === "open") {
|
|
39545
|
-
const
|
|
39546
|
-
if (!
|
|
39722
|
+
const thread = findPhoneChat(cmd.chatId);
|
|
39723
|
+
if (!thread) {
|
|
39547
39724
|
emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
|
|
39548
39725
|
return;
|
|
39549
39726
|
}
|
|
39550
|
-
|
|
39727
|
+
if (thread.status === "archived") {
|
|
39728
|
+
void getOrchestrator().restore(thread.id).then(() => {
|
|
39729
|
+
emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
|
|
39730
|
+
publishOpenedChat(opts, thread.id);
|
|
39731
|
+
}).catch((err) => {
|
|
39732
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
39733
|
+
});
|
|
39734
|
+
return;
|
|
39735
|
+
}
|
|
39736
|
+
publishOpenedChat(opts, thread.id);
|
|
39551
39737
|
return;
|
|
39552
39738
|
}
|
|
39553
39739
|
if (cmd.op === "archive") {
|
|
@@ -39576,6 +39762,41 @@ function handlePhoneControl(cmd, opts) {
|
|
|
39576
39762
|
});
|
|
39577
39763
|
return;
|
|
39578
39764
|
}
|
|
39765
|
+
if (cmd.op === "media") {
|
|
39766
|
+
const thread = readPhoneThread(cmd.chatId);
|
|
39767
|
+
const dataUrl = thread ? phoneImageDataUrl(thread.worktreePath, cmd.path) : null;
|
|
39768
|
+
emitControl(opts, {
|
|
39769
|
+
op: "media",
|
|
39770
|
+
chatId: cmd.chatId,
|
|
39771
|
+
path: cmd.path,
|
|
39772
|
+
...dataUrl ? { dataUrl } : {}
|
|
39773
|
+
});
|
|
39774
|
+
return;
|
|
39775
|
+
}
|
|
39776
|
+
if (cmd.op === "open-file") {
|
|
39777
|
+
const thread = readPhoneThread(cmd.chatId);
|
|
39778
|
+
const relative = thread ? phoneWorktreeRelative(thread.worktreePath, cmd.path) : null;
|
|
39779
|
+
if (!thread || !relative) {
|
|
39780
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
|
|
39781
|
+
return;
|
|
39782
|
+
}
|
|
39783
|
+
void getOrchestrator().statPath(thread.id, relative).then((kind) => {
|
|
39784
|
+
if (kind === "missing") {
|
|
39785
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
|
|
39786
|
+
return;
|
|
39787
|
+
}
|
|
39788
|
+
opts.onOpenWorktreeFile?.({
|
|
39789
|
+
threadId: thread.id,
|
|
39790
|
+
path: relative,
|
|
39791
|
+
...kind === "dir" ? { directory: true } : {},
|
|
39792
|
+
...cmd.startLine != null ? { startLine: cmd.startLine } : {},
|
|
39793
|
+
...cmd.endLine != null ? { endLine: cmd.endLine } : {}
|
|
39794
|
+
});
|
|
39795
|
+
}).catch(() => {
|
|
39796
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
|
|
39797
|
+
});
|
|
39798
|
+
return;
|
|
39799
|
+
}
|
|
39579
39800
|
if (cmd.op === "stop") {
|
|
39580
39801
|
if (!readPhoneThread(cmd.chatId)) {
|
|
39581
39802
|
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
|
|
@@ -39588,6 +39809,7 @@ function handlePhoneControl(cmd, opts) {
|
|
|
39588
39809
|
}
|
|
39589
39810
|
const chatId = cmd.chatId;
|
|
39590
39811
|
const generation = bumpChat(chatId);
|
|
39812
|
+
phoneTurnGen.set(chatId, generation);
|
|
39591
39813
|
interruptChat(chatId, log);
|
|
39592
39814
|
enqueueChat(chatId, async () => {
|
|
39593
39815
|
if (chatGeneration.get(chatId) !== generation) return;
|
|
@@ -39603,6 +39825,7 @@ function handlePhoneControl(cmd, opts) {
|
|
|
39603
39825
|
}
|
|
39604
39826
|
}
|
|
39605
39827
|
if (chatGeneration.get(chatId) !== generation) return;
|
|
39828
|
+
const stopStream = watchPhoneStream(opts, chatId, thread.id, generation);
|
|
39606
39829
|
try {
|
|
39607
39830
|
try {
|
|
39608
39831
|
applyPhoneOptions(thread.id, cmd);
|
|
@@ -39620,6 +39843,9 @@ function handlePhoneControl(cmd, opts) {
|
|
|
39620
39843
|
const message = err instanceof Error ? err.message : String(err);
|
|
39621
39844
|
emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${message}` });
|
|
39622
39845
|
return;
|
|
39846
|
+
} finally {
|
|
39847
|
+
stopStream();
|
|
39848
|
+
finishPhoneTurn(chatId, generation);
|
|
39623
39849
|
}
|
|
39624
39850
|
if (chatGeneration.get(chatId) !== generation) return;
|
|
39625
39851
|
emitChatTurn(opts, chatId, thread.id);
|
|
@@ -39861,7 +40087,8 @@ function runRemoteHost(opts) {
|
|
|
39861
40087
|
handleRemoteInbound("stop", {
|
|
39862
40088
|
deviceId: opts.deviceId,
|
|
39863
40089
|
onLog: log,
|
|
39864
|
-
onOutbound: emit
|
|
40090
|
+
onOutbound: emit,
|
|
40091
|
+
onOpenWorktreeFile: opts.onOpenWorktreeFile
|
|
39865
40092
|
});
|
|
39866
40093
|
return;
|
|
39867
40094
|
}
|
|
@@ -39870,7 +40097,8 @@ function runRemoteHost(opts) {
|
|
|
39870
40097
|
deviceId: opts.deviceId,
|
|
39871
40098
|
onLog: log,
|
|
39872
40099
|
onOutbound: emit,
|
|
39873
|
-
transcribeWav: opts.transcribeWav
|
|
40100
|
+
transcribeWav: opts.transcribeWav,
|
|
40101
|
+
onOpenWorktreeFile: opts.onOpenWorktreeFile
|
|
39874
40102
|
});
|
|
39875
40103
|
}
|
|
39876
40104
|
};
|
|
@@ -40269,9 +40497,74 @@ function sendJson2(res, status, body) {
|
|
|
40269
40497
|
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
40270
40498
|
res.end(JSON.stringify(body));
|
|
40271
40499
|
}
|
|
40500
|
+
function relayOAuthReturnPage(title, body) {
|
|
40501
|
+
return `<!DOCTYPE html>
|
|
40502
|
+
<html lang="en">
|
|
40503
|
+
<head>
|
|
40504
|
+
<meta charset="utf-8">
|
|
40505
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
40506
|
+
<meta name="robots" content="noindex">
|
|
40507
|
+
<title>${escapeHtml2(title)}</title>
|
|
40508
|
+
<style>
|
|
40509
|
+
:root {
|
|
40510
|
+
--bg: #09090b;
|
|
40511
|
+
--card: #16171b;
|
|
40512
|
+
--line: rgba(255, 255, 255, 0.08);
|
|
40513
|
+
--text: #f4f5f7;
|
|
40514
|
+
--muted: #9aa3ad;
|
|
40515
|
+
--blue: #3b9eff;
|
|
40516
|
+
--blue-fill: #0f7ed4;
|
|
40517
|
+
}
|
|
40518
|
+
* { box-sizing: border-box }
|
|
40519
|
+
body {
|
|
40520
|
+
margin: 0;
|
|
40521
|
+
min-height: 100vh;
|
|
40522
|
+
display: grid;
|
|
40523
|
+
place-items: center;
|
|
40524
|
+
padding: 48px 24px;
|
|
40525
|
+
background:
|
|
40526
|
+
radial-gradient(closest-side, rgba(15, 126, 212, 0.22), transparent 72%) center -120px / 720px 480px no-repeat,
|
|
40527
|
+
var(--bg);
|
|
40528
|
+
color: var(--text);
|
|
40529
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", sans-serif;
|
|
40530
|
+
-webkit-font-smoothing: antialiased;
|
|
40531
|
+
}
|
|
40532
|
+
main {
|
|
40533
|
+
width: min(28rem, 100%);
|
|
40534
|
+
background: var(--card);
|
|
40535
|
+
border: 1px solid var(--line);
|
|
40536
|
+
border-radius: 16px;
|
|
40537
|
+
padding: 28px 28px 32px;
|
|
40538
|
+
}
|
|
40539
|
+
.brand {
|
|
40540
|
+
display: flex;
|
|
40541
|
+
align-items: center;
|
|
40542
|
+
gap: 10px;
|
|
40543
|
+
margin: 0 0 22px;
|
|
40544
|
+
font-weight: 650;
|
|
40545
|
+
font-size: 17px;
|
|
40546
|
+
letter-spacing: -0.02em;
|
|
40547
|
+
}
|
|
40548
|
+
.mark { position: relative; display: inline-block; width: 26px; height: 26px; flex: none }
|
|
40549
|
+
.mark i { position: absolute; width: 18px; height: 18px; border-radius: 3px; transform: rotate(14deg) }
|
|
40550
|
+
.mark i:first-child { left: 6px; top: 6px; background: var(--blue-fill) }
|
|
40551
|
+
.mark i:last-child { left: 1px; top: 1px; border: 2px solid #d7dbdf }
|
|
40552
|
+
h1 { margin: 0; font-size: 1.5rem; line-height: 1.2; letter-spacing: -0.03em; font-weight: 650 }
|
|
40553
|
+
p { margin: 10px 0 0; line-height: 1.55; color: var(--muted) }
|
|
40554
|
+
a { color: var(--blue) }
|
|
40555
|
+
</style>
|
|
40556
|
+
</head>
|
|
40557
|
+
<body>
|
|
40558
|
+
<main>
|
|
40559
|
+
<div class="brand"><span class="mark" aria-hidden="true"><i></i><i></i></span>Sideboard</div>
|
|
40560
|
+
${body}
|
|
40561
|
+
</main>
|
|
40562
|
+
</body>
|
|
40563
|
+
</html>`;
|
|
40564
|
+
}
|
|
40272
40565
|
function sendHtml(res, status, title, body) {
|
|
40273
40566
|
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
40274
|
-
res.end(
|
|
40567
|
+
res.end(relayOAuthReturnPage(title, body));
|
|
40275
40568
|
}
|
|
40276
40569
|
async function handleRelayAccountHttp(opts) {
|
|
40277
40570
|
const reqUrl = opts.req.url || "/";
|
package/dist/index.d.cts
CHANGED
|
@@ -7151,6 +7151,14 @@ interface IpcApi {
|
|
|
7151
7151
|
};
|
|
7152
7152
|
/** Subscribe to orchestrator events; returns unsubscribe. */
|
|
7153
7153
|
onEvent(listener: (event: OrchestratorEvent) => void): () => void;
|
|
7154
|
+
/** Phone asked the Mac to open a worktree file or reveal a folder. */
|
|
7155
|
+
onPhoneOpenFile(listener: (request: {
|
|
7156
|
+
threadId: string;
|
|
7157
|
+
path: string;
|
|
7158
|
+
directory?: boolean;
|
|
7159
|
+
startLine?: number;
|
|
7160
|
+
endLine?: number;
|
|
7161
|
+
}) => void): () => void;
|
|
7154
7162
|
/** Subscribe to store directory changes (CLI threads appear live). */
|
|
7155
7163
|
onThreadsChanged(listener: () => void): () => void;
|
|
7156
7164
|
getRepoPath(): Promise<string>;
|
|
@@ -7767,6 +7775,14 @@ declare class RemoteHub {
|
|
|
7767
7775
|
private fromHost;
|
|
7768
7776
|
}
|
|
7769
7777
|
|
|
7778
|
+
type PhoneOpenFileRequest = {
|
|
7779
|
+
threadId: string;
|
|
7780
|
+
path: string;
|
|
7781
|
+
directory?: boolean;
|
|
7782
|
+
startLine?: number;
|
|
7783
|
+
endLine?: number;
|
|
7784
|
+
};
|
|
7785
|
+
|
|
7770
7786
|
interface RemoteHostClientOptions {
|
|
7771
7787
|
url: string;
|
|
7772
7788
|
deviceId: string;
|
|
@@ -7783,6 +7799,8 @@ interface RemoteHostClientOptions {
|
|
|
7783
7799
|
onLog?: (line: string) => void;
|
|
7784
7800
|
/** Mac speech recognizer for a phone recording (wav base64). */
|
|
7785
7801
|
transcribeWav?: (wavBase64: string) => Promise<string>;
|
|
7802
|
+
/** Open a worktree file or folder in the desktop window. */
|
|
7803
|
+
onOpenWorktreeFile?: (request: PhoneOpenFileRequest) => void;
|
|
7786
7804
|
/** Tests: override keepalive cadence. */
|
|
7787
7805
|
pingIntervalMs?: number;
|
|
7788
7806
|
pongTimeoutMs?: number;
|
|
@@ -7858,8 +7876,6 @@ type RemoteServerMessage = {
|
|
|
7858
7876
|
type: 'pong';
|
|
7859
7877
|
};
|
|
7860
7878
|
|
|
7861
|
-
declare function formatRemotePrompt(text: string): string;
|
|
7862
|
-
declare function isRemoteStopCommand(text: string): boolean;
|
|
7863
7879
|
type RemoteOutbound = {
|
|
7864
7880
|
type: 'assistant';
|
|
7865
7881
|
text: string;
|
|
@@ -7868,12 +7884,16 @@ type RemoteOutbound = {
|
|
|
7868
7884
|
text: string;
|
|
7869
7885
|
questions: RemoteAskQuestion[];
|
|
7870
7886
|
};
|
|
7887
|
+
|
|
7888
|
+
declare function formatRemotePrompt(text: string): string;
|
|
7889
|
+
declare function isRemoteStopCommand(text: string): boolean;
|
|
7871
7890
|
declare function handleRemoteInbound(text: string, opts: {
|
|
7872
7891
|
deviceId: string;
|
|
7873
7892
|
agent?: AgentKind;
|
|
7874
7893
|
onOutbound: (msg: RemoteOutbound) => void;
|
|
7875
7894
|
onLog?: (line: string) => void;
|
|
7876
7895
|
transcribeWav?: (wavBase64: string) => Promise<string>;
|
|
7896
|
+
onOpenWorktreeFile?: (request: PhoneOpenFileRequest) => void;
|
|
7877
7897
|
}): void;
|
|
7878
7898
|
|
|
7879
7899
|
declare function isRemoteAccountLoginCancelled(err: unknown): boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -7151,6 +7151,14 @@ interface IpcApi {
|
|
|
7151
7151
|
};
|
|
7152
7152
|
/** Subscribe to orchestrator events; returns unsubscribe. */
|
|
7153
7153
|
onEvent(listener: (event: OrchestratorEvent) => void): () => void;
|
|
7154
|
+
/** Phone asked the Mac to open a worktree file or reveal a folder. */
|
|
7155
|
+
onPhoneOpenFile(listener: (request: {
|
|
7156
|
+
threadId: string;
|
|
7157
|
+
path: string;
|
|
7158
|
+
directory?: boolean;
|
|
7159
|
+
startLine?: number;
|
|
7160
|
+
endLine?: number;
|
|
7161
|
+
}) => void): () => void;
|
|
7154
7162
|
/** Subscribe to store directory changes (CLI threads appear live). */
|
|
7155
7163
|
onThreadsChanged(listener: () => void): () => void;
|
|
7156
7164
|
getRepoPath(): Promise<string>;
|
|
@@ -7767,6 +7775,14 @@ declare class RemoteHub {
|
|
|
7767
7775
|
private fromHost;
|
|
7768
7776
|
}
|
|
7769
7777
|
|
|
7778
|
+
type PhoneOpenFileRequest = {
|
|
7779
|
+
threadId: string;
|
|
7780
|
+
path: string;
|
|
7781
|
+
directory?: boolean;
|
|
7782
|
+
startLine?: number;
|
|
7783
|
+
endLine?: number;
|
|
7784
|
+
};
|
|
7785
|
+
|
|
7770
7786
|
interface RemoteHostClientOptions {
|
|
7771
7787
|
url: string;
|
|
7772
7788
|
deviceId: string;
|
|
@@ -7783,6 +7799,8 @@ interface RemoteHostClientOptions {
|
|
|
7783
7799
|
onLog?: (line: string) => void;
|
|
7784
7800
|
/** Mac speech recognizer for a phone recording (wav base64). */
|
|
7785
7801
|
transcribeWav?: (wavBase64: string) => Promise<string>;
|
|
7802
|
+
/** Open a worktree file or folder in the desktop window. */
|
|
7803
|
+
onOpenWorktreeFile?: (request: PhoneOpenFileRequest) => void;
|
|
7786
7804
|
/** Tests: override keepalive cadence. */
|
|
7787
7805
|
pingIntervalMs?: number;
|
|
7788
7806
|
pongTimeoutMs?: number;
|
|
@@ -7858,8 +7876,6 @@ type RemoteServerMessage = {
|
|
|
7858
7876
|
type: 'pong';
|
|
7859
7877
|
};
|
|
7860
7878
|
|
|
7861
|
-
declare function formatRemotePrompt(text: string): string;
|
|
7862
|
-
declare function isRemoteStopCommand(text: string): boolean;
|
|
7863
7879
|
type RemoteOutbound = {
|
|
7864
7880
|
type: 'assistant';
|
|
7865
7881
|
text: string;
|
|
@@ -7868,12 +7884,16 @@ type RemoteOutbound = {
|
|
|
7868
7884
|
text: string;
|
|
7869
7885
|
questions: RemoteAskQuestion[];
|
|
7870
7886
|
};
|
|
7887
|
+
|
|
7888
|
+
declare function formatRemotePrompt(text: string): string;
|
|
7889
|
+
declare function isRemoteStopCommand(text: string): boolean;
|
|
7871
7890
|
declare function handleRemoteInbound(text: string, opts: {
|
|
7872
7891
|
deviceId: string;
|
|
7873
7892
|
agent?: AgentKind;
|
|
7874
7893
|
onOutbound: (msg: RemoteOutbound) => void;
|
|
7875
7894
|
onLog?: (line: string) => void;
|
|
7876
7895
|
transcribeWav?: (wavBase64: string) => Promise<string>;
|
|
7896
|
+
onOpenWorktreeFile?: (request: PhoneOpenFileRequest) => void;
|
|
7877
7897
|
}): void;
|
|
7878
7898
|
|
|
7879
7899
|
declare function isRemoteAccountLoginCancelled(err: unknown): boolean;
|
package/dist/index.js
CHANGED
|
@@ -9375,6 +9375,55 @@ var RemoteHub = class {
|
|
|
9375
9375
|
// src/remote/client.ts
|
|
9376
9376
|
import { WebSocket as WsWebSocket3 } from "ws";
|
|
9377
9377
|
|
|
9378
|
+
// src/remote/phone-media.ts
|
|
9379
|
+
var PHONE_IMAGE_MAX_BYTES = 15e5;
|
|
9380
|
+
var IMAGE_MIME = {
|
|
9381
|
+
png: "image/png",
|
|
9382
|
+
jpg: "image/jpeg",
|
|
9383
|
+
jpeg: "image/jpeg",
|
|
9384
|
+
gif: "image/gif",
|
|
9385
|
+
webp: "image/webp",
|
|
9386
|
+
bmp: "image/bmp"
|
|
9387
|
+
};
|
|
9388
|
+
function phoneWorktreeRelative(worktreePath, raw) {
|
|
9389
|
+
let text6 = raw.trim();
|
|
9390
|
+
if (!text6) return null;
|
|
9391
|
+
if (/^file:\/\//i.test(text6)) {
|
|
9392
|
+
try {
|
|
9393
|
+
text6 = decodeURIComponent(new URL(text6).pathname);
|
|
9394
|
+
} catch {
|
|
9395
|
+
return null;
|
|
9396
|
+
}
|
|
9397
|
+
}
|
|
9398
|
+
text6 = (text6.split("#")[0] ?? text6).replace(/\\/g, "/");
|
|
9399
|
+
if (!text6 || text6.includes("\0") || text6.split("/").includes("..")) return null;
|
|
9400
|
+
const root = worktreePath.replace(/\/+$/, "");
|
|
9401
|
+
if (!root || root === "/") return null;
|
|
9402
|
+
if (text6.startsWith("/")) {
|
|
9403
|
+
if (text6 !== root && !text6.startsWith(`${root}/`)) return null;
|
|
9404
|
+
text6 = text6 === root ? "" : text6.slice(root.length + 1);
|
|
9405
|
+
}
|
|
9406
|
+
text6 = text6.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
9407
|
+
if (!text6 || text6 === "." || text6.startsWith("/")) return null;
|
|
9408
|
+
return text6;
|
|
9409
|
+
}
|
|
9410
|
+
function phoneImageMime(relativePath) {
|
|
9411
|
+
const ext = relativePath.split(".").pop()?.toLowerCase() ?? "";
|
|
9412
|
+
return IMAGE_MIME[ext] ?? null;
|
|
9413
|
+
}
|
|
9414
|
+
function phoneImageDataUrl(worktreePath, raw) {
|
|
9415
|
+
const relative = phoneWorktreeRelative(worktreePath, raw);
|
|
9416
|
+
if (!relative) return null;
|
|
9417
|
+
const mime = phoneImageMime(relative);
|
|
9418
|
+
if (!mime) return null;
|
|
9419
|
+
try {
|
|
9420
|
+
const file = readWorktreeFileForUpload(worktreePath, relative, { maxBytes: PHONE_IMAGE_MAX_BYTES });
|
|
9421
|
+
return `data:${mime};base64,${file.contentBase64}`;
|
|
9422
|
+
} catch {
|
|
9423
|
+
return null;
|
|
9424
|
+
}
|
|
9425
|
+
}
|
|
9426
|
+
|
|
9378
9427
|
// src/remote/phone-chats.ts
|
|
9379
9428
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
9380
9429
|
var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
|
|
@@ -9431,6 +9480,20 @@ function parsePhoneControl(value) {
|
|
|
9431
9480
|
const chatId = typeof value.chatId === "string" ? value.chatId.trim() : "";
|
|
9432
9481
|
if (!chatId) return "invalid";
|
|
9433
9482
|
if (op === "open" || op === "archive" || op === "restore" || op === "stop") return { op, chatId };
|
|
9483
|
+
if (op === "open-file" || op === "media") {
|
|
9484
|
+
const path2 = textField(value, "path");
|
|
9485
|
+
if (!path2 || path2.length > 2e3) return "invalid";
|
|
9486
|
+
if (op === "media") return { op, chatId, path: path2 };
|
|
9487
|
+
const startLine = lineField(value, "startLine");
|
|
9488
|
+
const endLine = lineField(value, "endLine");
|
|
9489
|
+
return {
|
|
9490
|
+
op,
|
|
9491
|
+
chatId,
|
|
9492
|
+
path: path2,
|
|
9493
|
+
...startLine != null ? { startLine } : {},
|
|
9494
|
+
...endLine != null ? { endLine } : {}
|
|
9495
|
+
};
|
|
9496
|
+
}
|
|
9434
9497
|
const draft = parsePhoneDraft(value);
|
|
9435
9498
|
if (draft === "invalid") return "invalid";
|
|
9436
9499
|
if (op === "options") return { op: "options", chatId, ...draft };
|
|
@@ -9446,6 +9509,11 @@ function textField(value, key) {
|
|
|
9446
9509
|
const raw = value[key];
|
|
9447
9510
|
return typeof raw === "string" ? raw.trim() : "";
|
|
9448
9511
|
}
|
|
9512
|
+
function lineField(value, key) {
|
|
9513
|
+
const raw = value[key];
|
|
9514
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1 || raw > 1e6) return void 0;
|
|
9515
|
+
return raw;
|
|
9516
|
+
}
|
|
9449
9517
|
function agentField(value) {
|
|
9450
9518
|
const agent = textField(value, "agent");
|
|
9451
9519
|
if (!agent) return void 0;
|
|
@@ -9685,13 +9753,28 @@ function listPhoneHistory(opts) {
|
|
|
9685
9753
|
...more && last ? { next: historyCursor(last) } : {}
|
|
9686
9754
|
};
|
|
9687
9755
|
}
|
|
9688
|
-
function
|
|
9756
|
+
function matchPhoneThread(chatId) {
|
|
9689
9757
|
const id = chatId.trim();
|
|
9690
9758
|
if (!id) return null;
|
|
9691
|
-
const
|
|
9759
|
+
const exact = readThread(id);
|
|
9760
|
+
if (exact) return exact;
|
|
9761
|
+
const match = findThreadByRef(id);
|
|
9762
|
+
if (!match) return null;
|
|
9763
|
+
if (match.id !== id && !match.id.startsWith(id)) return null;
|
|
9764
|
+
return match;
|
|
9765
|
+
}
|
|
9766
|
+
function visiblePhoneThread(thread) {
|
|
9767
|
+
if (isGlobalThread(thread)) return true;
|
|
9768
|
+
return isProjectPath(thread.repoPath);
|
|
9769
|
+
}
|
|
9770
|
+
function findPhoneChat(chatId) {
|
|
9771
|
+
const thread = matchPhoneThread(chatId);
|
|
9772
|
+
if (!thread || !visiblePhoneThread(thread)) return null;
|
|
9773
|
+
return thread;
|
|
9774
|
+
}
|
|
9775
|
+
function readPhoneThread(chatId) {
|
|
9776
|
+
const thread = findPhoneChat(chatId);
|
|
9692
9777
|
if (!thread || thread.status === "archived") return null;
|
|
9693
|
-
if (isGlobalThread(thread)) return thread;
|
|
9694
|
-
if (!isProjectPath(thread.repoPath)) return null;
|
|
9695
9778
|
return thread;
|
|
9696
9779
|
}
|
|
9697
9780
|
function phoneComposerOptions(thread) {
|
|
@@ -9837,6 +9920,21 @@ async function createPhoneProjectWorktree(repoPathOrInput) {
|
|
|
9837
9920
|
});
|
|
9838
9921
|
return openedFrom(thread, projectPlace(thread));
|
|
9839
9922
|
}
|
|
9923
|
+
async function listPhoneModels(agent) {
|
|
9924
|
+
const catalogs = await listModelsForAgent(agent);
|
|
9925
|
+
return (catalogs[0]?.models ?? []).slice(0, 40).map((model) => ({
|
|
9926
|
+
id: model.id,
|
|
9927
|
+
label: model.displayName || model.id
|
|
9928
|
+
}));
|
|
9929
|
+
}
|
|
9930
|
+
function archivePhoneChat(chatId) {
|
|
9931
|
+
const thread = readPhoneThread(chatId);
|
|
9932
|
+
if (!thread) return false;
|
|
9933
|
+
updateThread(thread.id, { status: "archived" });
|
|
9934
|
+
return true;
|
|
9935
|
+
}
|
|
9936
|
+
|
|
9937
|
+
// src/remote/phone-sources.ts
|
|
9840
9938
|
function sourceWarning(label, err) {
|
|
9841
9939
|
const message = err instanceof Error ? err.message : String(err);
|
|
9842
9940
|
return message ? `${label}: ${message}` : `${label} failed.`;
|
|
@@ -9879,60 +9977,37 @@ async function listPhoneSources(repoPath, query) {
|
|
|
9879
9977
|
...warnings.length ? { warnings } : {}
|
|
9880
9978
|
};
|
|
9881
9979
|
}
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9980
|
+
|
|
9981
|
+
// src/remote/phone-stream.ts
|
|
9982
|
+
var PHONE_STREAM_FLUSH_MS = 80;
|
|
9983
|
+
function phoneStreamFrame(parts) {
|
|
9984
|
+
return {
|
|
9985
|
+
text: visibleAssistantText(void 0, parts),
|
|
9986
|
+
activity: liveActivitySummary(parts)
|
|
9987
|
+
};
|
|
9888
9988
|
}
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
if (
|
|
9892
|
-
|
|
9893
|
-
|
|
9989
|
+
var SKIPPED = /* @__PURE__ */ new Set(["session_id", "usage", "stderr", "exit"]);
|
|
9990
|
+
function phoneStreamParts(parts, event) {
|
|
9991
|
+
if (SKIPPED.has(event.type)) return null;
|
|
9992
|
+
return applyAgentEvent(parts, event);
|
|
9993
|
+
}
|
|
9994
|
+
function phoneStreamFlushNow(event) {
|
|
9995
|
+
return event.type === "tool_use" || event.type === "tool_result" && !event.partial;
|
|
9894
9996
|
}
|
|
9895
9997
|
|
|
9896
|
-
// src/remote/
|
|
9998
|
+
// src/remote/phone-live.ts
|
|
9897
9999
|
var REMOTE_STOPPED_REPLY = "Sideboard stopped the in-progress turn.";
|
|
9898
|
-
function formatRemotePrompt(text6) {
|
|
9899
|
-
return `Phone
|
|
9900
|
-
|
|
9901
|
-
${text6.trim()}`;
|
|
9902
|
-
}
|
|
9903
|
-
function isRemoteStopCommand(text6) {
|
|
9904
|
-
return text6.trim().toLowerCase() === "stop";
|
|
9905
|
-
}
|
|
9906
|
-
var handleChain2 = Promise.resolve();
|
|
9907
|
-
var inboundGeneration = 0;
|
|
9908
10000
|
var chatChains = /* @__PURE__ */ new Map();
|
|
9909
10001
|
var chatGeneration = /* @__PURE__ */ new Map();
|
|
9910
|
-
|
|
9911
|
-
const run2 = handleChain2.then(fn, fn);
|
|
9912
|
-
handleChain2 = run2.then(
|
|
9913
|
-
() => void 0,
|
|
9914
|
-
() => void 0
|
|
9915
|
-
);
|
|
9916
|
-
}
|
|
9917
|
-
function interruptRemoteCoordinator(deviceId, log = () => void 0) {
|
|
9918
|
-
try {
|
|
9919
|
-
const live = findRemoteCoordinator(deviceId);
|
|
9920
|
-
if (!live) return false;
|
|
9921
|
-
const fresh = readThread(live.id) ?? live;
|
|
9922
|
-
if (fresh.status !== "running" && fresh.status !== "queued") return false;
|
|
9923
|
-
getOrchestrator().stop(fresh.id, { clearQueue: true });
|
|
9924
|
-
log(`interrupt phone \u2192 coordinator ${fresh.id.slice(0, 8)} (${fresh.status})`);
|
|
9925
|
-
return true;
|
|
9926
|
-
} catch (err) {
|
|
9927
|
-
log(`interrupt: ${err instanceof Error ? err.message : String(err)}`);
|
|
9928
|
-
return false;
|
|
9929
|
-
}
|
|
9930
|
-
}
|
|
10002
|
+
var phoneTurnGen = /* @__PURE__ */ new Map();
|
|
9931
10003
|
function bumpChat(chatId) {
|
|
9932
10004
|
const next = (chatGeneration.get(chatId) ?? 0) + 1;
|
|
9933
10005
|
chatGeneration.set(chatId, next);
|
|
9934
10006
|
return next;
|
|
9935
10007
|
}
|
|
10008
|
+
function finishPhoneTurn(chatId, generation) {
|
|
10009
|
+
if (phoneTurnGen.get(chatId) === generation) phoneTurnGen.delete(chatId);
|
|
10010
|
+
}
|
|
9936
10011
|
function enqueueChat(chatId, fn) {
|
|
9937
10012
|
const prev = chatChains.get(chatId) ?? Promise.resolve();
|
|
9938
10013
|
const run2 = prev.then(fn, fn);
|
|
@@ -9947,6 +10022,49 @@ function enqueueChat(chatId, fn) {
|
|
|
9947
10022
|
function emitControl(opts, reply) {
|
|
9948
10023
|
opts.onOutbound({ type: "assistant", text: encodePhoneControl(reply) });
|
|
9949
10024
|
}
|
|
10025
|
+
function watchPhoneStream(opts, chatId, threadId, generation) {
|
|
10026
|
+
let parts = [];
|
|
10027
|
+
let timer = null;
|
|
10028
|
+
let lastKey = "";
|
|
10029
|
+
let closed = false;
|
|
10030
|
+
const alive = () => !closed && chatGeneration.get(chatId) === generation;
|
|
10031
|
+
const flush = () => {
|
|
10032
|
+
timer = null;
|
|
10033
|
+
if (!alive()) return;
|
|
10034
|
+
const frame = phoneStreamFrame(parts);
|
|
10035
|
+
const key = `${frame.activity}
|
|
10036
|
+
${frame.text}`;
|
|
10037
|
+
if (key === lastKey) return;
|
|
10038
|
+
if (!frame.text && !frame.activity) return;
|
|
10039
|
+
lastKey = key;
|
|
10040
|
+
emitControl(opts, { op: "stream", chatId, text: frame.text, activity: frame.activity });
|
|
10041
|
+
};
|
|
10042
|
+
const schedule = (event) => {
|
|
10043
|
+
if (phoneStreamFlushNow(event) || lastKey === "") {
|
|
10044
|
+
if (timer) {
|
|
10045
|
+
clearTimeout(timer);
|
|
10046
|
+
timer = null;
|
|
10047
|
+
}
|
|
10048
|
+
flush();
|
|
10049
|
+
return;
|
|
10050
|
+
}
|
|
10051
|
+
if (timer) return;
|
|
10052
|
+
timer = setTimeout(flush, PHONE_STREAM_FLUSH_MS);
|
|
10053
|
+
};
|
|
10054
|
+
const off = getOrchestrator().on((event) => {
|
|
10055
|
+
if (!alive()) return;
|
|
10056
|
+
if (event.type !== "turn_output" || event.threadId !== threadId) return;
|
|
10057
|
+
const next = phoneStreamParts(parts, event.event);
|
|
10058
|
+
if (!next) return;
|
|
10059
|
+
parts = next;
|
|
10060
|
+
schedule(event.event);
|
|
10061
|
+
});
|
|
10062
|
+
return () => {
|
|
10063
|
+
closed = true;
|
|
10064
|
+
if (timer) clearTimeout(timer);
|
|
10065
|
+
off();
|
|
10066
|
+
};
|
|
10067
|
+
}
|
|
9950
10068
|
function interruptChat(chatId, log) {
|
|
9951
10069
|
try {
|
|
9952
10070
|
const thread = readPhoneThread(chatId);
|
|
@@ -10002,20 +10120,73 @@ function phoneError(err) {
|
|
|
10002
10120
|
function deliverPhoneTurn(opts, chatId) {
|
|
10003
10121
|
const thread = readPhoneThread(chatId);
|
|
10004
10122
|
if (!thread || thread.status !== "running" && thread.status !== "queued") return;
|
|
10005
|
-
const
|
|
10006
|
-
|
|
10007
|
-
|
|
10123
|
+
const id = thread.id;
|
|
10124
|
+
const claimed = phoneTurnGen.get(id);
|
|
10125
|
+
if (claimed != null && claimed === chatGeneration.get(id)) return;
|
|
10126
|
+
const generation = bumpChat(id);
|
|
10127
|
+
phoneTurnGen.set(id, generation);
|
|
10128
|
+
enqueueChat(id, async () => {
|
|
10129
|
+
if (chatGeneration.get(id) !== generation) return;
|
|
10130
|
+
const stopStream = watchPhoneStream(opts, id, id, generation);
|
|
10008
10131
|
try {
|
|
10009
|
-
await getOrchestrator().waitForTurn(
|
|
10132
|
+
await getOrchestrator().waitForTurn(id, 14 * 60 * 1e3);
|
|
10010
10133
|
} catch (err) {
|
|
10011
|
-
|
|
10012
|
-
|
|
10134
|
+
stopStream();
|
|
10135
|
+
finishPhoneTurn(id, generation);
|
|
10136
|
+
if (chatGeneration.get(id) !== generation) return;
|
|
10137
|
+
emitControl(opts, { op: "error", chatId: id, message: `Sideboard failed: ${phoneError(err)}` });
|
|
10013
10138
|
return;
|
|
10014
10139
|
}
|
|
10015
|
-
|
|
10016
|
-
|
|
10140
|
+
stopStream();
|
|
10141
|
+
finishPhoneTurn(id, generation);
|
|
10142
|
+
if (chatGeneration.get(id) !== generation) return;
|
|
10143
|
+
emitChatTurn(opts, id, id);
|
|
10017
10144
|
});
|
|
10018
10145
|
}
|
|
10146
|
+
|
|
10147
|
+
// src/remote/listen.ts
|
|
10148
|
+
function formatRemotePrompt(text6) {
|
|
10149
|
+
return `Phone
|
|
10150
|
+
|
|
10151
|
+
${text6.trim()}`;
|
|
10152
|
+
}
|
|
10153
|
+
function isRemoteStopCommand(text6) {
|
|
10154
|
+
return text6.trim().toLowerCase() === "stop";
|
|
10155
|
+
}
|
|
10156
|
+
var handleChain2 = Promise.resolve();
|
|
10157
|
+
var inboundGeneration = 0;
|
|
10158
|
+
function enqueue(fn) {
|
|
10159
|
+
const run2 = handleChain2.then(fn, fn);
|
|
10160
|
+
handleChain2 = run2.then(
|
|
10161
|
+
() => void 0,
|
|
10162
|
+
() => void 0
|
|
10163
|
+
);
|
|
10164
|
+
}
|
|
10165
|
+
function interruptRemoteCoordinator(deviceId, log = () => void 0) {
|
|
10166
|
+
try {
|
|
10167
|
+
const live = findRemoteCoordinator(deviceId);
|
|
10168
|
+
if (!live) return false;
|
|
10169
|
+
const fresh = readThread(live.id) ?? live;
|
|
10170
|
+
if (fresh.status !== "running" && fresh.status !== "queued") return false;
|
|
10171
|
+
getOrchestrator().stop(fresh.id, { clearQueue: true });
|
|
10172
|
+
log(`interrupt phone \u2192 coordinator ${fresh.id.slice(0, 8)} (${fresh.status})`);
|
|
10173
|
+
return true;
|
|
10174
|
+
} catch (err) {
|
|
10175
|
+
log(`interrupt: ${err instanceof Error ? err.message : String(err)}`);
|
|
10176
|
+
return false;
|
|
10177
|
+
}
|
|
10178
|
+
}
|
|
10179
|
+
function publishOpenedChat(opts, chatId) {
|
|
10180
|
+
const opened = openPhoneChat(chatId);
|
|
10181
|
+
if (!opened) {
|
|
10182
|
+
emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
|
|
10183
|
+
return;
|
|
10184
|
+
}
|
|
10185
|
+
emitControl(opts, { op: "opened", ...opened });
|
|
10186
|
+
if (opened.chat.status === "running" || opened.chat.status === "queued") {
|
|
10187
|
+
deliverPhoneTurn(opts, opened.chat.id);
|
|
10188
|
+
}
|
|
10189
|
+
}
|
|
10019
10190
|
function applyPhoneOptions(chatId, draft) {
|
|
10020
10191
|
const patch = {};
|
|
10021
10192
|
if (draft.agent) patch.agent = draft.agent;
|
|
@@ -10166,12 +10337,21 @@ function handlePhoneControl(cmd, opts) {
|
|
|
10166
10337
|
return;
|
|
10167
10338
|
}
|
|
10168
10339
|
if (cmd.op === "open") {
|
|
10169
|
-
const
|
|
10170
|
-
if (!
|
|
10340
|
+
const thread = findPhoneChat(cmd.chatId);
|
|
10341
|
+
if (!thread) {
|
|
10171
10342
|
emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
|
|
10172
10343
|
return;
|
|
10173
10344
|
}
|
|
10174
|
-
|
|
10345
|
+
if (thread.status === "archived") {
|
|
10346
|
+
void getOrchestrator().restore(thread.id).then(() => {
|
|
10347
|
+
emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
|
|
10348
|
+
publishOpenedChat(opts, thread.id);
|
|
10349
|
+
}).catch((err) => {
|
|
10350
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
10351
|
+
});
|
|
10352
|
+
return;
|
|
10353
|
+
}
|
|
10354
|
+
publishOpenedChat(opts, thread.id);
|
|
10175
10355
|
return;
|
|
10176
10356
|
}
|
|
10177
10357
|
if (cmd.op === "archive") {
|
|
@@ -10200,6 +10380,41 @@ function handlePhoneControl(cmd, opts) {
|
|
|
10200
10380
|
});
|
|
10201
10381
|
return;
|
|
10202
10382
|
}
|
|
10383
|
+
if (cmd.op === "media") {
|
|
10384
|
+
const thread = readPhoneThread(cmd.chatId);
|
|
10385
|
+
const dataUrl = thread ? phoneImageDataUrl(thread.worktreePath, cmd.path) : null;
|
|
10386
|
+
emitControl(opts, {
|
|
10387
|
+
op: "media",
|
|
10388
|
+
chatId: cmd.chatId,
|
|
10389
|
+
path: cmd.path,
|
|
10390
|
+
...dataUrl ? { dataUrl } : {}
|
|
10391
|
+
});
|
|
10392
|
+
return;
|
|
10393
|
+
}
|
|
10394
|
+
if (cmd.op === "open-file") {
|
|
10395
|
+
const thread = readPhoneThread(cmd.chatId);
|
|
10396
|
+
const relative = thread ? phoneWorktreeRelative(thread.worktreePath, cmd.path) : null;
|
|
10397
|
+
if (!thread || !relative) {
|
|
10398
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
|
|
10399
|
+
return;
|
|
10400
|
+
}
|
|
10401
|
+
void getOrchestrator().statPath(thread.id, relative).then((kind) => {
|
|
10402
|
+
if (kind === "missing") {
|
|
10403
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
|
|
10404
|
+
return;
|
|
10405
|
+
}
|
|
10406
|
+
opts.onOpenWorktreeFile?.({
|
|
10407
|
+
threadId: thread.id,
|
|
10408
|
+
path: relative,
|
|
10409
|
+
...kind === "dir" ? { directory: true } : {},
|
|
10410
|
+
...cmd.startLine != null ? { startLine: cmd.startLine } : {},
|
|
10411
|
+
...cmd.endLine != null ? { endLine: cmd.endLine } : {}
|
|
10412
|
+
});
|
|
10413
|
+
}).catch(() => {
|
|
10414
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
|
|
10415
|
+
});
|
|
10416
|
+
return;
|
|
10417
|
+
}
|
|
10203
10418
|
if (cmd.op === "stop") {
|
|
10204
10419
|
if (!readPhoneThread(cmd.chatId)) {
|
|
10205
10420
|
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
|
|
@@ -10212,6 +10427,7 @@ function handlePhoneControl(cmd, opts) {
|
|
|
10212
10427
|
}
|
|
10213
10428
|
const chatId = cmd.chatId;
|
|
10214
10429
|
const generation = bumpChat(chatId);
|
|
10430
|
+
phoneTurnGen.set(chatId, generation);
|
|
10215
10431
|
interruptChat(chatId, log);
|
|
10216
10432
|
enqueueChat(chatId, async () => {
|
|
10217
10433
|
if (chatGeneration.get(chatId) !== generation) return;
|
|
@@ -10227,6 +10443,7 @@ function handlePhoneControl(cmd, opts) {
|
|
|
10227
10443
|
}
|
|
10228
10444
|
}
|
|
10229
10445
|
if (chatGeneration.get(chatId) !== generation) return;
|
|
10446
|
+
const stopStream = watchPhoneStream(opts, chatId, thread.id, generation);
|
|
10230
10447
|
try {
|
|
10231
10448
|
try {
|
|
10232
10449
|
applyPhoneOptions(thread.id, cmd);
|
|
@@ -10244,6 +10461,9 @@ function handlePhoneControl(cmd, opts) {
|
|
|
10244
10461
|
const message = err instanceof Error ? err.message : String(err);
|
|
10245
10462
|
emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${message}` });
|
|
10246
10463
|
return;
|
|
10464
|
+
} finally {
|
|
10465
|
+
stopStream();
|
|
10466
|
+
finishPhoneTurn(chatId, generation);
|
|
10247
10467
|
}
|
|
10248
10468
|
if (chatGeneration.get(chatId) !== generation) return;
|
|
10249
10469
|
emitChatTurn(opts, chatId, thread.id);
|
|
@@ -10485,7 +10705,8 @@ function runRemoteHost(opts) {
|
|
|
10485
10705
|
handleRemoteInbound("stop", {
|
|
10486
10706
|
deviceId: opts.deviceId,
|
|
10487
10707
|
onLog: log,
|
|
10488
|
-
onOutbound: emit
|
|
10708
|
+
onOutbound: emit,
|
|
10709
|
+
onOpenWorktreeFile: opts.onOpenWorktreeFile
|
|
10489
10710
|
});
|
|
10490
10711
|
return;
|
|
10491
10712
|
}
|
|
@@ -10494,7 +10715,8 @@ function runRemoteHost(opts) {
|
|
|
10494
10715
|
deviceId: opts.deviceId,
|
|
10495
10716
|
onLog: log,
|
|
10496
10717
|
onOutbound: emit,
|
|
10497
|
-
transcribeWav: opts.transcribeWav
|
|
10718
|
+
transcribeWav: opts.transcribeWav,
|
|
10719
|
+
onOpenWorktreeFile: opts.onOpenWorktreeFile
|
|
10498
10720
|
});
|
|
10499
10721
|
}
|
|
10500
10722
|
};
|
|
@@ -10893,9 +11115,74 @@ function sendJson2(res, status, body) {
|
|
|
10893
11115
|
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
10894
11116
|
res.end(JSON.stringify(body));
|
|
10895
11117
|
}
|
|
11118
|
+
function relayOAuthReturnPage(title, body) {
|
|
11119
|
+
return `<!DOCTYPE html>
|
|
11120
|
+
<html lang="en">
|
|
11121
|
+
<head>
|
|
11122
|
+
<meta charset="utf-8">
|
|
11123
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11124
|
+
<meta name="robots" content="noindex">
|
|
11125
|
+
<title>${escapeHtml(title)}</title>
|
|
11126
|
+
<style>
|
|
11127
|
+
:root {
|
|
11128
|
+
--bg: #09090b;
|
|
11129
|
+
--card: #16171b;
|
|
11130
|
+
--line: rgba(255, 255, 255, 0.08);
|
|
11131
|
+
--text: #f4f5f7;
|
|
11132
|
+
--muted: #9aa3ad;
|
|
11133
|
+
--blue: #3b9eff;
|
|
11134
|
+
--blue-fill: #0f7ed4;
|
|
11135
|
+
}
|
|
11136
|
+
* { box-sizing: border-box }
|
|
11137
|
+
body {
|
|
11138
|
+
margin: 0;
|
|
11139
|
+
min-height: 100vh;
|
|
11140
|
+
display: grid;
|
|
11141
|
+
place-items: center;
|
|
11142
|
+
padding: 48px 24px;
|
|
11143
|
+
background:
|
|
11144
|
+
radial-gradient(closest-side, rgba(15, 126, 212, 0.22), transparent 72%) center -120px / 720px 480px no-repeat,
|
|
11145
|
+
var(--bg);
|
|
11146
|
+
color: var(--text);
|
|
11147
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", sans-serif;
|
|
11148
|
+
-webkit-font-smoothing: antialiased;
|
|
11149
|
+
}
|
|
11150
|
+
main {
|
|
11151
|
+
width: min(28rem, 100%);
|
|
11152
|
+
background: var(--card);
|
|
11153
|
+
border: 1px solid var(--line);
|
|
11154
|
+
border-radius: 16px;
|
|
11155
|
+
padding: 28px 28px 32px;
|
|
11156
|
+
}
|
|
11157
|
+
.brand {
|
|
11158
|
+
display: flex;
|
|
11159
|
+
align-items: center;
|
|
11160
|
+
gap: 10px;
|
|
11161
|
+
margin: 0 0 22px;
|
|
11162
|
+
font-weight: 650;
|
|
11163
|
+
font-size: 17px;
|
|
11164
|
+
letter-spacing: -0.02em;
|
|
11165
|
+
}
|
|
11166
|
+
.mark { position: relative; display: inline-block; width: 26px; height: 26px; flex: none }
|
|
11167
|
+
.mark i { position: absolute; width: 18px; height: 18px; border-radius: 3px; transform: rotate(14deg) }
|
|
11168
|
+
.mark i:first-child { left: 6px; top: 6px; background: var(--blue-fill) }
|
|
11169
|
+
.mark i:last-child { left: 1px; top: 1px; border: 2px solid #d7dbdf }
|
|
11170
|
+
h1 { margin: 0; font-size: 1.5rem; line-height: 1.2; letter-spacing: -0.03em; font-weight: 650 }
|
|
11171
|
+
p { margin: 10px 0 0; line-height: 1.55; color: var(--muted) }
|
|
11172
|
+
a { color: var(--blue) }
|
|
11173
|
+
</style>
|
|
11174
|
+
</head>
|
|
11175
|
+
<body>
|
|
11176
|
+
<main>
|
|
11177
|
+
<div class="brand"><span class="mark" aria-hidden="true"><i></i><i></i></span>Sideboard</div>
|
|
11178
|
+
${body}
|
|
11179
|
+
</main>
|
|
11180
|
+
</body>
|
|
11181
|
+
</html>`;
|
|
11182
|
+
}
|
|
10896
11183
|
function sendHtml(res, status, title, body) {
|
|
10897
11184
|
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
10898
|
-
res.end(
|
|
11185
|
+
res.end(relayOAuthReturnPage(title, body));
|
|
10899
11186
|
}
|
|
10900
11187
|
async function handleRelayAccountHttp(opts) {
|
|
10901
11188
|
const reqUrl = opts.req.url || "/";
|