@sideboard-ai/core 0.1.275 → 0.1.277
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 +531 -81
- package/dist/index.d.cts +37 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.js +514 -70
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4098,8 +4098,8 @@ function crushMarkdown(text6, opts) {
|
|
|
4098
4098
|
}
|
|
4099
4099
|
const must = blocks.filter((b) => looksLikeMustKeep(b));
|
|
4100
4100
|
const rest = blocks.filter((b) => !looksLikeMustKeep(b));
|
|
4101
|
-
const
|
|
4102
|
-
let used = must.reduce((n, b) => n + b.length, 0) +
|
|
4101
|
+
const marker2 = `[\u2026crushed body; ${original.length} chars originally. Pass include=full to read the rest.]`;
|
|
4102
|
+
let used = must.reduce((n, b) => n + b.length, 0) + marker2.length;
|
|
4103
4103
|
const keptRest = [];
|
|
4104
4104
|
const headN = Math.max(1, Math.ceil(rest.length * 0.3));
|
|
4105
4105
|
const tailN = Math.max(1, Math.ceil(rest.length * 0.15));
|
|
@@ -4139,7 +4139,7 @@ function crushMarkdown(text6, opts) {
|
|
|
4139
4139
|
continue;
|
|
4140
4140
|
}
|
|
4141
4141
|
if (!inserted) {
|
|
4142
|
-
out.push(
|
|
4142
|
+
out.push(marker2);
|
|
4143
4143
|
inserted = true;
|
|
4144
4144
|
}
|
|
4145
4145
|
}
|
|
@@ -9375,8 +9375,195 @@ 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";
|
|
9429
|
+
|
|
9430
|
+
// src/remote/phone-artifact.ts
|
|
9431
|
+
var FENCE_RE = /```([a-zA-Z0-9_+-]*)[^\n]*\n([\s\S]*?)(?:```|$)/g;
|
|
9432
|
+
var DOC_LANGS = /* @__PURE__ */ new Set(["html", "htm", "xhtml", "svg", "markdown", "md", "mdx", "jsx", "tsx", "react"]);
|
|
9433
|
+
var MIN_FENCE_CHARS = 40;
|
|
9434
|
+
var MIN_MD_CHARS = 120;
|
|
9435
|
+
function oneLine(value, max) {
|
|
9436
|
+
return value.replace(/[`\r\n]/g, " ").replace(/\s+/g, " ").trim().slice(0, max);
|
|
9437
|
+
}
|
|
9438
|
+
function asRecord3(value) {
|
|
9439
|
+
if (typeof value === "string") {
|
|
9440
|
+
try {
|
|
9441
|
+
return asRecord3(JSON.parse(value));
|
|
9442
|
+
} catch {
|
|
9443
|
+
return void 0;
|
|
9444
|
+
}
|
|
9445
|
+
}
|
|
9446
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
9447
|
+
return void 0;
|
|
9448
|
+
}
|
|
9449
|
+
function str3(value) {
|
|
9450
|
+
return typeof value === "string" ? value : "";
|
|
9451
|
+
}
|
|
9452
|
+
function normalizedBody(content) {
|
|
9453
|
+
return content.trim().replace(/\s+/g, " ");
|
|
9454
|
+
}
|
|
9455
|
+
function artifactHint(content) {
|
|
9456
|
+
const body = normalizedBody(content);
|
|
9457
|
+
if (!body) return "";
|
|
9458
|
+
return fnv1a32(body).toString(36);
|
|
9459
|
+
}
|
|
9460
|
+
function fnv1a32(value) {
|
|
9461
|
+
let hash = 2166136261;
|
|
9462
|
+
for (let i = 0; i < value.length; i++) {
|
|
9463
|
+
hash ^= value.charCodeAt(i);
|
|
9464
|
+
hash = Math.imul(hash, 16777619);
|
|
9465
|
+
}
|
|
9466
|
+
return hash >>> 0;
|
|
9467
|
+
}
|
|
9468
|
+
function looksLikeHtml(content) {
|
|
9469
|
+
const head = content.trim().slice(0, 200).toLowerCase();
|
|
9470
|
+
return head.startsWith("<!doctype") || head.startsWith("<html") || head.startsWith("<svg");
|
|
9471
|
+
}
|
|
9472
|
+
function kindFor(language, content) {
|
|
9473
|
+
const lang = language.toLowerCase();
|
|
9474
|
+
if (!lang) {
|
|
9475
|
+
if (!looksLikeHtml(content) || content.trim().length < MIN_FENCE_CHARS) return null;
|
|
9476
|
+
return content.trim().toLowerCase().startsWith("<svg") ? "svg" : "html";
|
|
9477
|
+
}
|
|
9478
|
+
if (!DOC_LANGS.has(lang)) return null;
|
|
9479
|
+
const min = lang === "markdown" || lang === "md" || lang === "mdx" ? MIN_MD_CHARS : MIN_FENCE_CHARS;
|
|
9480
|
+
if (content.trim().length < min) return null;
|
|
9481
|
+
if (lang === "svg") return "svg";
|
|
9482
|
+
if (lang === "markdown" || lang === "md" || lang === "mdx") return "markdown";
|
|
9483
|
+
if (lang === "jsx" || lang === "tsx" || lang === "react") return "react";
|
|
9484
|
+
return "html";
|
|
9485
|
+
}
|
|
9486
|
+
function titleFor(kind, content) {
|
|
9487
|
+
if (kind === "html" || kind === "svg") {
|
|
9488
|
+
const title = /<title[^>]*>([^<]+)<\/title>/i.exec(content)?.[1]?.trim();
|
|
9489
|
+
if (title) return oneLine(title, 80);
|
|
9490
|
+
}
|
|
9491
|
+
if (kind === "markdown") {
|
|
9492
|
+
const heading = /^#\s+(.+)$/m.exec(content)?.[1]?.trim();
|
|
9493
|
+
if (heading) return oneLine(heading, 80);
|
|
9494
|
+
}
|
|
9495
|
+
if (kind === "react") {
|
|
9496
|
+
const name = /export\s+default\s+(?:function|class)\s+([A-Za-z_$][\w$]*)/.exec(content)?.[1];
|
|
9497
|
+
if (name) return name;
|
|
9498
|
+
}
|
|
9499
|
+
if (kind === "svg") return "SVG artifact";
|
|
9500
|
+
if (kind === "markdown") return "Document";
|
|
9501
|
+
if (kind === "react") return "React artifact";
|
|
9502
|
+
return "HTML artifact";
|
|
9503
|
+
}
|
|
9504
|
+
function marker(title, hint) {
|
|
9505
|
+
return hint ? `\`\`\`sb-artifact
|
|
9506
|
+
${title}
|
|
9507
|
+
${hint}
|
|
9508
|
+
\`\`\`` : `\`\`\`sb-artifact
|
|
9509
|
+
${title}
|
|
9510
|
+
\`\`\``;
|
|
9511
|
+
}
|
|
9512
|
+
function toolContent(input) {
|
|
9513
|
+
if (!input) return "";
|
|
9514
|
+
const direct = input.content;
|
|
9515
|
+
if (typeof direct === "string") return direct;
|
|
9516
|
+
const nested = asRecord3(direct);
|
|
9517
|
+
if (!nested) return "";
|
|
9518
|
+
for (const key of ["html", "markup", "text", "source", "content", "code"]) {
|
|
9519
|
+
if (typeof nested[key] === "string") return nested[key];
|
|
9520
|
+
}
|
|
9521
|
+
return "";
|
|
9522
|
+
}
|
|
9523
|
+
function isArtifactTool(name) {
|
|
9524
|
+
const short = name.split("__").pop() ?? name;
|
|
9525
|
+
return /^(present_artifact|create_artifact|update_artifact)$/i.test(short);
|
|
9526
|
+
}
|
|
9527
|
+
function phoneArtifactText(text6, parts, limit = Number.POSITIVE_INFINITY) {
|
|
9528
|
+
const hints = /* @__PURE__ */ new Set();
|
|
9529
|
+
const body = text6.replace(new RegExp(FENCE_RE.source, "g"), (full, language, content) => {
|
|
9530
|
+
const kind = kindFor(language, content);
|
|
9531
|
+
if (!kind) return full;
|
|
9532
|
+
const hint = artifactHint(content);
|
|
9533
|
+
hints.add(hint);
|
|
9534
|
+
return marker(titleFor(kind, content), hint);
|
|
9535
|
+
});
|
|
9536
|
+
const extras = [];
|
|
9537
|
+
for (const part of parts ?? []) {
|
|
9538
|
+
if (part.type !== "tool" || part.parentId || !isArtifactTool(part.name)) continue;
|
|
9539
|
+
const input = asRecord3(part.input);
|
|
9540
|
+
const content = toolContent(input);
|
|
9541
|
+
const hint = artifactHint(content);
|
|
9542
|
+
if (hint && hints.has(hint)) continue;
|
|
9543
|
+
const kind = kindFor("", content);
|
|
9544
|
+
const title = oneLine(str3(input?.title), 80) || (kind ? titleFor(kind, content) : "Artifact");
|
|
9545
|
+
if (!str3(input?.title) && content.trim().length < MIN_FENCE_CHARS) continue;
|
|
9546
|
+
if (hint) hints.add(hint);
|
|
9547
|
+
extras.push(marker(title, hint));
|
|
9548
|
+
}
|
|
9549
|
+
const tail = extras.join("\n\n");
|
|
9550
|
+
const room = tail ? Math.max(0, limit - tail.length - 2) : limit;
|
|
9551
|
+
const prose = body.length > room ? `${body.slice(0, Math.max(0, room - 1))}\u2026` : body;
|
|
9552
|
+
if (!tail) return prose;
|
|
9553
|
+
return prose ? `${prose}
|
|
9554
|
+
|
|
9555
|
+
${tail}` : tail;
|
|
9556
|
+
}
|
|
9557
|
+
function parseOpenArtifact(value, chatId) {
|
|
9558
|
+
const record = asRecord3(value);
|
|
9559
|
+
if (!record) return "invalid";
|
|
9560
|
+
const title = oneLine(str3(record.title), 200);
|
|
9561
|
+
if (!title) return "invalid";
|
|
9562
|
+
const hint = oneLine(str3(record.hint), 200);
|
|
9563
|
+
return { op: "open-artifact", chatId, title, ...hint ? { hint } : {} };
|
|
9564
|
+
}
|
|
9565
|
+
|
|
9566
|
+
// src/remote/phone-chats.ts
|
|
9380
9567
|
var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
|
|
9381
9568
|
var PHONE_USER_PREFIX = "Phone\n\n";
|
|
9382
9569
|
var TRANSCRIPT_LIMIT = 40;
|
|
@@ -9431,6 +9618,21 @@ function parsePhoneControl(value) {
|
|
|
9431
9618
|
const chatId = typeof value.chatId === "string" ? value.chatId.trim() : "";
|
|
9432
9619
|
if (!chatId) return "invalid";
|
|
9433
9620
|
if (op === "open" || op === "archive" || op === "restore" || op === "stop") return { op, chatId };
|
|
9621
|
+
if (op === "open-artifact") return parseOpenArtifact(value, chatId);
|
|
9622
|
+
if (op === "open-file" || op === "media") {
|
|
9623
|
+
const path2 = textField(value, "path");
|
|
9624
|
+
if (!path2 || path2.length > 2e3) return "invalid";
|
|
9625
|
+
if (op === "media") return { op, chatId, path: path2 };
|
|
9626
|
+
const startLine = lineField(value, "startLine");
|
|
9627
|
+
const endLine = lineField(value, "endLine");
|
|
9628
|
+
return {
|
|
9629
|
+
op,
|
|
9630
|
+
chatId,
|
|
9631
|
+
path: path2,
|
|
9632
|
+
...startLine != null ? { startLine } : {},
|
|
9633
|
+
...endLine != null ? { endLine } : {}
|
|
9634
|
+
};
|
|
9635
|
+
}
|
|
9434
9636
|
const draft = parsePhoneDraft(value);
|
|
9435
9637
|
if (draft === "invalid") return "invalid";
|
|
9436
9638
|
if (op === "options") return { op: "options", chatId, ...draft };
|
|
@@ -9446,6 +9648,11 @@ function textField(value, key) {
|
|
|
9446
9648
|
const raw = value[key];
|
|
9447
9649
|
return typeof raw === "string" ? raw.trim() : "";
|
|
9448
9650
|
}
|
|
9651
|
+
function lineField(value, key) {
|
|
9652
|
+
const raw = value[key];
|
|
9653
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1 || raw > 1e6) return void 0;
|
|
9654
|
+
return raw;
|
|
9655
|
+
}
|
|
9449
9656
|
function agentField(value) {
|
|
9450
9657
|
const agent = textField(value, "agent");
|
|
9451
9658
|
if (!agent) return void 0;
|
|
@@ -9589,7 +9796,8 @@ function summarize(thread) {
|
|
|
9589
9796
|
title: thread.title?.trim() || "Untitled",
|
|
9590
9797
|
status: thread.status,
|
|
9591
9798
|
preview: previewOf(thread),
|
|
9592
|
-
updatedAt: thread.updatedAt
|
|
9799
|
+
updatedAt: thread.updatedAt,
|
|
9800
|
+
agent: thread.agent
|
|
9593
9801
|
};
|
|
9594
9802
|
}
|
|
9595
9803
|
function previewOf(thread) {
|
|
@@ -9603,10 +9811,8 @@ function listPhoneMessages(thread) {
|
|
|
9603
9811
|
for (const message of thread.messages) {
|
|
9604
9812
|
if (message.role !== "user" && message.role !== "agent") continue;
|
|
9605
9813
|
const names = (message.attachments ?? []).map((item) => item.name.trim()).filter(Boolean);
|
|
9606
|
-
const
|
|
9607
|
-
|
|
9608
|
-
MESSAGE_LIMIT
|
|
9609
|
-
);
|
|
9814
|
+
const visible = [phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n");
|
|
9815
|
+
const text6 = message.role === "agent" ? phoneArtifactText(visible, message.parts, MESSAGE_LIMIT) : clip(visible, MESSAGE_LIMIT);
|
|
9610
9816
|
if (!text6) continue;
|
|
9611
9817
|
out.push({ role: message.role, text: text6 });
|
|
9612
9818
|
}
|
|
@@ -9685,13 +9891,28 @@ function listPhoneHistory(opts) {
|
|
|
9685
9891
|
...more && last ? { next: historyCursor(last) } : {}
|
|
9686
9892
|
};
|
|
9687
9893
|
}
|
|
9688
|
-
function
|
|
9894
|
+
function matchPhoneThread(chatId) {
|
|
9689
9895
|
const id = chatId.trim();
|
|
9690
9896
|
if (!id) return null;
|
|
9691
|
-
const
|
|
9897
|
+
const exact = readThread(id);
|
|
9898
|
+
if (exact) return exact;
|
|
9899
|
+
const match = findThreadByRef(id);
|
|
9900
|
+
if (!match) return null;
|
|
9901
|
+
if (match.id !== id && !match.id.startsWith(id)) return null;
|
|
9902
|
+
return match;
|
|
9903
|
+
}
|
|
9904
|
+
function visiblePhoneThread(thread) {
|
|
9905
|
+
if (isGlobalThread(thread)) return true;
|
|
9906
|
+
return isProjectPath(thread.repoPath);
|
|
9907
|
+
}
|
|
9908
|
+
function findPhoneChat(chatId) {
|
|
9909
|
+
const thread = matchPhoneThread(chatId);
|
|
9910
|
+
if (!thread || !visiblePhoneThread(thread)) return null;
|
|
9911
|
+
return thread;
|
|
9912
|
+
}
|
|
9913
|
+
function readPhoneThread(chatId) {
|
|
9914
|
+
const thread = findPhoneChat(chatId);
|
|
9692
9915
|
if (!thread || thread.status === "archived") return null;
|
|
9693
|
-
if (isGlobalThread(thread)) return thread;
|
|
9694
|
-
if (!isProjectPath(thread.repoPath)) return null;
|
|
9695
9916
|
return thread;
|
|
9696
9917
|
}
|
|
9697
9918
|
function phoneComposerOptions(thread) {
|
|
@@ -9837,6 +10058,21 @@ async function createPhoneProjectWorktree(repoPathOrInput) {
|
|
|
9837
10058
|
});
|
|
9838
10059
|
return openedFrom(thread, projectPlace(thread));
|
|
9839
10060
|
}
|
|
10061
|
+
async function listPhoneModels(agent) {
|
|
10062
|
+
const catalogs = await listModelsForAgent(agent);
|
|
10063
|
+
return (catalogs[0]?.models ?? []).slice(0, 40).map((model) => ({
|
|
10064
|
+
id: model.id,
|
|
10065
|
+
label: model.displayName || model.id
|
|
10066
|
+
}));
|
|
10067
|
+
}
|
|
10068
|
+
function archivePhoneChat(chatId) {
|
|
10069
|
+
const thread = readPhoneThread(chatId);
|
|
10070
|
+
if (!thread) return false;
|
|
10071
|
+
updateThread(thread.id, { status: "archived" });
|
|
10072
|
+
return true;
|
|
10073
|
+
}
|
|
10074
|
+
|
|
10075
|
+
// src/remote/phone-sources.ts
|
|
9840
10076
|
function sourceWarning(label, err) {
|
|
9841
10077
|
const message = err instanceof Error ? err.message : String(err);
|
|
9842
10078
|
return message ? `${label}: ${message}` : `${label} failed.`;
|
|
@@ -9879,60 +10115,37 @@ async function listPhoneSources(repoPath, query) {
|
|
|
9879
10115
|
...warnings.length ? { warnings } : {}
|
|
9880
10116
|
};
|
|
9881
10117
|
}
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
10118
|
+
|
|
10119
|
+
// src/remote/phone-stream.ts
|
|
10120
|
+
var PHONE_STREAM_FLUSH_MS = 80;
|
|
10121
|
+
function phoneStreamFrame(parts) {
|
|
10122
|
+
return {
|
|
10123
|
+
text: phoneArtifactText(visibleAssistantText(void 0, parts), parts),
|
|
10124
|
+
activity: liveActivitySummary(parts)
|
|
10125
|
+
};
|
|
9888
10126
|
}
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
if (
|
|
9892
|
-
|
|
9893
|
-
|
|
10127
|
+
var SKIPPED = /* @__PURE__ */ new Set(["session_id", "usage", "stderr", "exit"]);
|
|
10128
|
+
function phoneStreamParts(parts, event) {
|
|
10129
|
+
if (SKIPPED.has(event.type)) return null;
|
|
10130
|
+
return applyAgentEvent(parts, event);
|
|
10131
|
+
}
|
|
10132
|
+
function phoneStreamFlushNow(event) {
|
|
10133
|
+
return event.type === "tool_use" || event.type === "tool_result" && !event.partial;
|
|
9894
10134
|
}
|
|
9895
10135
|
|
|
9896
|
-
// src/remote/
|
|
10136
|
+
// src/remote/phone-live.ts
|
|
9897
10137
|
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
10138
|
var chatChains = /* @__PURE__ */ new Map();
|
|
9909
10139
|
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
|
-
}
|
|
10140
|
+
var phoneTurnGen = /* @__PURE__ */ new Map();
|
|
9931
10141
|
function bumpChat(chatId) {
|
|
9932
10142
|
const next = (chatGeneration.get(chatId) ?? 0) + 1;
|
|
9933
10143
|
chatGeneration.set(chatId, next);
|
|
9934
10144
|
return next;
|
|
9935
10145
|
}
|
|
10146
|
+
function finishPhoneTurn(chatId, generation) {
|
|
10147
|
+
if (phoneTurnGen.get(chatId) === generation) phoneTurnGen.delete(chatId);
|
|
10148
|
+
}
|
|
9936
10149
|
function enqueueChat(chatId, fn) {
|
|
9937
10150
|
const prev = chatChains.get(chatId) ?? Promise.resolve();
|
|
9938
10151
|
const run2 = prev.then(fn, fn);
|
|
@@ -9947,6 +10160,49 @@ function enqueueChat(chatId, fn) {
|
|
|
9947
10160
|
function emitControl(opts, reply) {
|
|
9948
10161
|
opts.onOutbound({ type: "assistant", text: encodePhoneControl(reply) });
|
|
9949
10162
|
}
|
|
10163
|
+
function watchPhoneStream(opts, chatId, threadId, generation) {
|
|
10164
|
+
let parts = [];
|
|
10165
|
+
let timer = null;
|
|
10166
|
+
let lastKey = "";
|
|
10167
|
+
let closed = false;
|
|
10168
|
+
const alive = () => !closed && chatGeneration.get(chatId) === generation;
|
|
10169
|
+
const flush = () => {
|
|
10170
|
+
timer = null;
|
|
10171
|
+
if (!alive()) return;
|
|
10172
|
+
const frame = phoneStreamFrame(parts);
|
|
10173
|
+
const key = `${frame.activity}
|
|
10174
|
+
${frame.text}`;
|
|
10175
|
+
if (key === lastKey) return;
|
|
10176
|
+
if (!frame.text && !frame.activity) return;
|
|
10177
|
+
lastKey = key;
|
|
10178
|
+
emitControl(opts, { op: "stream", chatId, text: frame.text, activity: frame.activity });
|
|
10179
|
+
};
|
|
10180
|
+
const schedule = (event) => {
|
|
10181
|
+
if (phoneStreamFlushNow(event) || lastKey === "") {
|
|
10182
|
+
if (timer) {
|
|
10183
|
+
clearTimeout(timer);
|
|
10184
|
+
timer = null;
|
|
10185
|
+
}
|
|
10186
|
+
flush();
|
|
10187
|
+
return;
|
|
10188
|
+
}
|
|
10189
|
+
if (timer) return;
|
|
10190
|
+
timer = setTimeout(flush, PHONE_STREAM_FLUSH_MS);
|
|
10191
|
+
};
|
|
10192
|
+
const off = getOrchestrator().on((event) => {
|
|
10193
|
+
if (!alive()) return;
|
|
10194
|
+
if (event.type !== "turn_output" || event.threadId !== threadId) return;
|
|
10195
|
+
const next = phoneStreamParts(parts, event.event);
|
|
10196
|
+
if (!next) return;
|
|
10197
|
+
parts = next;
|
|
10198
|
+
schedule(event.event);
|
|
10199
|
+
});
|
|
10200
|
+
return () => {
|
|
10201
|
+
closed = true;
|
|
10202
|
+
if (timer) clearTimeout(timer);
|
|
10203
|
+
off();
|
|
10204
|
+
};
|
|
10205
|
+
}
|
|
9950
10206
|
function interruptChat(chatId, log) {
|
|
9951
10207
|
try {
|
|
9952
10208
|
const thread = readPhoneThread(chatId);
|
|
@@ -9994,7 +10250,12 @@ function emitChatTurn(opts, chatId, threadId) {
|
|
|
9994
10250
|
completedEmpty: () => "",
|
|
9995
10251
|
failed: (detail) => detail ? `Sideboard failed: ${detail}` : "Sideboard failed before producing a result."
|
|
9996
10252
|
});
|
|
9997
|
-
|
|
10253
|
+
const agent = [...thread.messages].reverse().find((m) => m.role === "agent");
|
|
10254
|
+
emitControl(opts, {
|
|
10255
|
+
op: "assistant",
|
|
10256
|
+
chatId,
|
|
10257
|
+
text: phoneArtifactText(reply.trim(), agent?.parts)
|
|
10258
|
+
});
|
|
9998
10259
|
}
|
|
9999
10260
|
function phoneError(err) {
|
|
10000
10261
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -10002,20 +10263,73 @@ function phoneError(err) {
|
|
|
10002
10263
|
function deliverPhoneTurn(opts, chatId) {
|
|
10003
10264
|
const thread = readPhoneThread(chatId);
|
|
10004
10265
|
if (!thread || thread.status !== "running" && thread.status !== "queued") return;
|
|
10005
|
-
const
|
|
10006
|
-
|
|
10007
|
-
|
|
10266
|
+
const id = thread.id;
|
|
10267
|
+
const claimed = phoneTurnGen.get(id);
|
|
10268
|
+
if (claimed != null && claimed === chatGeneration.get(id)) return;
|
|
10269
|
+
const generation = bumpChat(id);
|
|
10270
|
+
phoneTurnGen.set(id, generation);
|
|
10271
|
+
enqueueChat(id, async () => {
|
|
10272
|
+
if (chatGeneration.get(id) !== generation) return;
|
|
10273
|
+
const stopStream = watchPhoneStream(opts, id, id, generation);
|
|
10008
10274
|
try {
|
|
10009
|
-
await getOrchestrator().waitForTurn(
|
|
10275
|
+
await getOrchestrator().waitForTurn(id, 14 * 60 * 1e3);
|
|
10010
10276
|
} catch (err) {
|
|
10011
|
-
|
|
10012
|
-
|
|
10277
|
+
stopStream();
|
|
10278
|
+
finishPhoneTurn(id, generation);
|
|
10279
|
+
if (chatGeneration.get(id) !== generation) return;
|
|
10280
|
+
emitControl(opts, { op: "error", chatId: id, message: `Sideboard failed: ${phoneError(err)}` });
|
|
10013
10281
|
return;
|
|
10014
10282
|
}
|
|
10015
|
-
|
|
10016
|
-
|
|
10283
|
+
stopStream();
|
|
10284
|
+
finishPhoneTurn(id, generation);
|
|
10285
|
+
if (chatGeneration.get(id) !== generation) return;
|
|
10286
|
+
emitChatTurn(opts, id, id);
|
|
10017
10287
|
});
|
|
10018
10288
|
}
|
|
10289
|
+
|
|
10290
|
+
// src/remote/listen.ts
|
|
10291
|
+
function formatRemotePrompt(text6) {
|
|
10292
|
+
return `Phone
|
|
10293
|
+
|
|
10294
|
+
${text6.trim()}`;
|
|
10295
|
+
}
|
|
10296
|
+
function isRemoteStopCommand(text6) {
|
|
10297
|
+
return text6.trim().toLowerCase() === "stop";
|
|
10298
|
+
}
|
|
10299
|
+
var handleChain2 = Promise.resolve();
|
|
10300
|
+
var inboundGeneration = 0;
|
|
10301
|
+
function enqueue(fn) {
|
|
10302
|
+
const run2 = handleChain2.then(fn, fn);
|
|
10303
|
+
handleChain2 = run2.then(
|
|
10304
|
+
() => void 0,
|
|
10305
|
+
() => void 0
|
|
10306
|
+
);
|
|
10307
|
+
}
|
|
10308
|
+
function interruptRemoteCoordinator(deviceId, log = () => void 0) {
|
|
10309
|
+
try {
|
|
10310
|
+
const live = findRemoteCoordinator(deviceId);
|
|
10311
|
+
if (!live) return false;
|
|
10312
|
+
const fresh = readThread(live.id) ?? live;
|
|
10313
|
+
if (fresh.status !== "running" && fresh.status !== "queued") return false;
|
|
10314
|
+
getOrchestrator().stop(fresh.id, { clearQueue: true });
|
|
10315
|
+
log(`interrupt phone \u2192 coordinator ${fresh.id.slice(0, 8)} (${fresh.status})`);
|
|
10316
|
+
return true;
|
|
10317
|
+
} catch (err) {
|
|
10318
|
+
log(`interrupt: ${err instanceof Error ? err.message : String(err)}`);
|
|
10319
|
+
return false;
|
|
10320
|
+
}
|
|
10321
|
+
}
|
|
10322
|
+
function publishOpenedChat(opts, chatId) {
|
|
10323
|
+
const opened = openPhoneChat(chatId);
|
|
10324
|
+
if (!opened) {
|
|
10325
|
+
emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
|
|
10326
|
+
return;
|
|
10327
|
+
}
|
|
10328
|
+
emitControl(opts, { op: "opened", ...opened });
|
|
10329
|
+
if (opened.chat.status === "running" || opened.chat.status === "queued") {
|
|
10330
|
+
deliverPhoneTurn(opts, opened.chat.id);
|
|
10331
|
+
}
|
|
10332
|
+
}
|
|
10019
10333
|
function applyPhoneOptions(chatId, draft) {
|
|
10020
10334
|
const patch = {};
|
|
10021
10335
|
if (draft.agent) patch.agent = draft.agent;
|
|
@@ -10166,12 +10480,21 @@ function handlePhoneControl(cmd, opts) {
|
|
|
10166
10480
|
return;
|
|
10167
10481
|
}
|
|
10168
10482
|
if (cmd.op === "open") {
|
|
10169
|
-
const
|
|
10170
|
-
if (!
|
|
10483
|
+
const thread = findPhoneChat(cmd.chatId);
|
|
10484
|
+
if (!thread) {
|
|
10171
10485
|
emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
|
|
10172
10486
|
return;
|
|
10173
10487
|
}
|
|
10174
|
-
|
|
10488
|
+
if (thread.status === "archived") {
|
|
10489
|
+
void getOrchestrator().restore(thread.id).then(() => {
|
|
10490
|
+
emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
|
|
10491
|
+
publishOpenedChat(opts, thread.id);
|
|
10492
|
+
}).catch((err) => {
|
|
10493
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
10494
|
+
});
|
|
10495
|
+
return;
|
|
10496
|
+
}
|
|
10497
|
+
publishOpenedChat(opts, thread.id);
|
|
10175
10498
|
return;
|
|
10176
10499
|
}
|
|
10177
10500
|
if (cmd.op === "archive") {
|
|
@@ -10200,6 +10523,54 @@ function handlePhoneControl(cmd, opts) {
|
|
|
10200
10523
|
});
|
|
10201
10524
|
return;
|
|
10202
10525
|
}
|
|
10526
|
+
if (cmd.op === "media") {
|
|
10527
|
+
const thread = readPhoneThread(cmd.chatId);
|
|
10528
|
+
const dataUrl = thread ? phoneImageDataUrl(thread.worktreePath, cmd.path) : null;
|
|
10529
|
+
emitControl(opts, {
|
|
10530
|
+
op: "media",
|
|
10531
|
+
chatId: cmd.chatId,
|
|
10532
|
+
path: cmd.path,
|
|
10533
|
+
...dataUrl ? { dataUrl } : {}
|
|
10534
|
+
});
|
|
10535
|
+
return;
|
|
10536
|
+
}
|
|
10537
|
+
if (cmd.op === "open-file") {
|
|
10538
|
+
const thread = readPhoneThread(cmd.chatId);
|
|
10539
|
+
const relative = thread ? phoneWorktreeRelative(thread.worktreePath, cmd.path) : null;
|
|
10540
|
+
if (!thread || !relative) {
|
|
10541
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
|
|
10542
|
+
return;
|
|
10543
|
+
}
|
|
10544
|
+
void getOrchestrator().statPath(thread.id, relative).then((kind) => {
|
|
10545
|
+
if (kind === "missing") {
|
|
10546
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
|
|
10547
|
+
return;
|
|
10548
|
+
}
|
|
10549
|
+
opts.onOpenWorktreeFile?.({
|
|
10550
|
+
threadId: thread.id,
|
|
10551
|
+
path: relative,
|
|
10552
|
+
...kind === "dir" ? { directory: true } : {},
|
|
10553
|
+
...cmd.startLine != null ? { startLine: cmd.startLine } : {},
|
|
10554
|
+
...cmd.endLine != null ? { endLine: cmd.endLine } : {}
|
|
10555
|
+
});
|
|
10556
|
+
}).catch(() => {
|
|
10557
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
|
|
10558
|
+
});
|
|
10559
|
+
return;
|
|
10560
|
+
}
|
|
10561
|
+
if (cmd.op === "open-artifact") {
|
|
10562
|
+
const thread = readPhoneThread(cmd.chatId);
|
|
10563
|
+
if (!thread) {
|
|
10564
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
|
|
10565
|
+
return;
|
|
10566
|
+
}
|
|
10567
|
+
opts.onOpenArtifact?.({
|
|
10568
|
+
threadId: thread.id,
|
|
10569
|
+
title: cmd.title,
|
|
10570
|
+
...cmd.hint ? { hint: cmd.hint } : {}
|
|
10571
|
+
});
|
|
10572
|
+
return;
|
|
10573
|
+
}
|
|
10203
10574
|
if (cmd.op === "stop") {
|
|
10204
10575
|
if (!readPhoneThread(cmd.chatId)) {
|
|
10205
10576
|
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
|
|
@@ -10212,6 +10583,7 @@ function handlePhoneControl(cmd, opts) {
|
|
|
10212
10583
|
}
|
|
10213
10584
|
const chatId = cmd.chatId;
|
|
10214
10585
|
const generation = bumpChat(chatId);
|
|
10586
|
+
phoneTurnGen.set(chatId, generation);
|
|
10215
10587
|
interruptChat(chatId, log);
|
|
10216
10588
|
enqueueChat(chatId, async () => {
|
|
10217
10589
|
if (chatGeneration.get(chatId) !== generation) return;
|
|
@@ -10227,6 +10599,7 @@ function handlePhoneControl(cmd, opts) {
|
|
|
10227
10599
|
}
|
|
10228
10600
|
}
|
|
10229
10601
|
if (chatGeneration.get(chatId) !== generation) return;
|
|
10602
|
+
const stopStream = watchPhoneStream(opts, chatId, thread.id, generation);
|
|
10230
10603
|
try {
|
|
10231
10604
|
try {
|
|
10232
10605
|
applyPhoneOptions(thread.id, cmd);
|
|
@@ -10244,6 +10617,9 @@ function handlePhoneControl(cmd, opts) {
|
|
|
10244
10617
|
const message = err instanceof Error ? err.message : String(err);
|
|
10245
10618
|
emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${message}` });
|
|
10246
10619
|
return;
|
|
10620
|
+
} finally {
|
|
10621
|
+
stopStream();
|
|
10622
|
+
finishPhoneTurn(chatId, generation);
|
|
10247
10623
|
}
|
|
10248
10624
|
if (chatGeneration.get(chatId) !== generation) return;
|
|
10249
10625
|
emitChatTurn(opts, chatId, thread.id);
|
|
@@ -10485,7 +10861,8 @@ function runRemoteHost(opts) {
|
|
|
10485
10861
|
handleRemoteInbound("stop", {
|
|
10486
10862
|
deviceId: opts.deviceId,
|
|
10487
10863
|
onLog: log,
|
|
10488
|
-
onOutbound: emit
|
|
10864
|
+
onOutbound: emit,
|
|
10865
|
+
onOpenWorktreeFile: opts.onOpenWorktreeFile
|
|
10489
10866
|
});
|
|
10490
10867
|
return;
|
|
10491
10868
|
}
|
|
@@ -10494,7 +10871,9 @@ function runRemoteHost(opts) {
|
|
|
10494
10871
|
deviceId: opts.deviceId,
|
|
10495
10872
|
onLog: log,
|
|
10496
10873
|
onOutbound: emit,
|
|
10497
|
-
transcribeWav: opts.transcribeWav
|
|
10874
|
+
transcribeWav: opts.transcribeWav,
|
|
10875
|
+
onOpenWorktreeFile: opts.onOpenWorktreeFile,
|
|
10876
|
+
onOpenArtifact: opts.onOpenArtifact
|
|
10498
10877
|
});
|
|
10499
10878
|
}
|
|
10500
10879
|
};
|
|
@@ -10893,9 +11272,74 @@ function sendJson2(res, status, body) {
|
|
|
10893
11272
|
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
10894
11273
|
res.end(JSON.stringify(body));
|
|
10895
11274
|
}
|
|
11275
|
+
function relayOAuthReturnPage(title, body) {
|
|
11276
|
+
return `<!DOCTYPE html>
|
|
11277
|
+
<html lang="en">
|
|
11278
|
+
<head>
|
|
11279
|
+
<meta charset="utf-8">
|
|
11280
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11281
|
+
<meta name="robots" content="noindex">
|
|
11282
|
+
<title>${escapeHtml(title)}</title>
|
|
11283
|
+
<style>
|
|
11284
|
+
:root {
|
|
11285
|
+
--bg: #09090b;
|
|
11286
|
+
--card: #16171b;
|
|
11287
|
+
--line: rgba(255, 255, 255, 0.08);
|
|
11288
|
+
--text: #f4f5f7;
|
|
11289
|
+
--muted: #9aa3ad;
|
|
11290
|
+
--blue: #3b9eff;
|
|
11291
|
+
--blue-fill: #0f7ed4;
|
|
11292
|
+
}
|
|
11293
|
+
* { box-sizing: border-box }
|
|
11294
|
+
body {
|
|
11295
|
+
margin: 0;
|
|
11296
|
+
min-height: 100vh;
|
|
11297
|
+
display: grid;
|
|
11298
|
+
place-items: center;
|
|
11299
|
+
padding: 48px 24px;
|
|
11300
|
+
background:
|
|
11301
|
+
radial-gradient(closest-side, rgba(15, 126, 212, 0.22), transparent 72%) center -120px / 720px 480px no-repeat,
|
|
11302
|
+
var(--bg);
|
|
11303
|
+
color: var(--text);
|
|
11304
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", sans-serif;
|
|
11305
|
+
-webkit-font-smoothing: antialiased;
|
|
11306
|
+
}
|
|
11307
|
+
main {
|
|
11308
|
+
width: min(28rem, 100%);
|
|
11309
|
+
background: var(--card);
|
|
11310
|
+
border: 1px solid var(--line);
|
|
11311
|
+
border-radius: 16px;
|
|
11312
|
+
padding: 28px 28px 32px;
|
|
11313
|
+
}
|
|
11314
|
+
.brand {
|
|
11315
|
+
display: flex;
|
|
11316
|
+
align-items: center;
|
|
11317
|
+
gap: 10px;
|
|
11318
|
+
margin: 0 0 22px;
|
|
11319
|
+
font-weight: 650;
|
|
11320
|
+
font-size: 17px;
|
|
11321
|
+
letter-spacing: -0.02em;
|
|
11322
|
+
}
|
|
11323
|
+
.mark { position: relative; display: inline-block; width: 26px; height: 26px; flex: none }
|
|
11324
|
+
.mark i { position: absolute; width: 18px; height: 18px; border-radius: 3px; transform: rotate(14deg) }
|
|
11325
|
+
.mark i:first-child { left: 6px; top: 6px; background: var(--blue-fill) }
|
|
11326
|
+
.mark i:last-child { left: 1px; top: 1px; border: 2px solid #d7dbdf }
|
|
11327
|
+
h1 { margin: 0; font-size: 1.5rem; line-height: 1.2; letter-spacing: -0.03em; font-weight: 650 }
|
|
11328
|
+
p { margin: 10px 0 0; line-height: 1.55; color: var(--muted) }
|
|
11329
|
+
a { color: var(--blue) }
|
|
11330
|
+
</style>
|
|
11331
|
+
</head>
|
|
11332
|
+
<body>
|
|
11333
|
+
<main>
|
|
11334
|
+
<div class="brand"><span class="mark" aria-hidden="true"><i></i><i></i></span>Sideboard</div>
|
|
11335
|
+
${body}
|
|
11336
|
+
</main>
|
|
11337
|
+
</body>
|
|
11338
|
+
</html>`;
|
|
11339
|
+
}
|
|
10896
11340
|
function sendHtml(res, status, title, body) {
|
|
10897
11341
|
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
10898
|
-
res.end(
|
|
11342
|
+
res.end(relayOAuthReturnPage(title, body));
|
|
10899
11343
|
}
|
|
10900
11344
|
async function handleRelayAccountHttp(opts) {
|
|
10901
11345
|
const reqUrl = opts.req.url || "/";
|