@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.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 marker = `[\u2026crushed body; ${original.length} chars originally. Pass include=full to read the rest.]`;
4102
- let used = must.reduce((n, b) => n + b.length, 0) + marker.length;
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(marker);
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 text6 = clip(
9607
- [phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n"),
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 readPhoneThread(chatId) {
9894
+ function matchPhoneThread(chatId) {
9689
9895
  const id = chatId.trim();
9690
9896
  if (!id) return null;
9691
- const thread = readThread(id);
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
- async function listPhoneModels(agent) {
9883
- const catalogs = await listModelsForAgent(agent);
9884
- return (catalogs[0]?.models ?? []).slice(0, 40).map((model) => ({
9885
- id: model.id,
9886
- label: model.displayName || model.id
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
- function archivePhoneChat(chatId) {
9890
- const thread = readPhoneThread(chatId);
9891
- if (!thread) return false;
9892
- updateThread(thread.id, { status: "archived" });
9893
- return true;
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/listen.ts
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
- function enqueue(fn) {
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
- emitControl(opts, { op: "assistant", chatId, text: reply.trim() });
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 generation = bumpChat(chatId);
10006
- enqueueChat(chatId, async () => {
10007
- if (chatGeneration.get(chatId) !== generation) return;
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(chatId, 14 * 60 * 1e3);
10275
+ await getOrchestrator().waitForTurn(id, 14 * 60 * 1e3);
10010
10276
  } catch (err) {
10011
- if (chatGeneration.get(chatId) !== generation) return;
10012
- emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${phoneError(err)}` });
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
- if (chatGeneration.get(chatId) !== generation) return;
10016
- emitChatTurn(opts, chatId, chatId);
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 opened = openPhoneChat(cmd.chatId);
10170
- if (!opened) {
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
- emitControl(opts, { op: "opened", ...opened });
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(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title></head><body>${body}</body></html>`);
11342
+ res.end(relayOAuthReturnPage(title, body));
10899
11343
  }
10900
11344
  async function handleRelayAccountHttp(opts) {
10901
11345
  const reqUrl = opts.req.url || "/";