@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 CHANGED
@@ -829,9 +829,9 @@ function resolveViewerProfile(account, project) {
829
829
  };
830
830
  }
831
831
  function compactNotes(notes, max = 160) {
832
- const oneLine = notes.replace(/\s+/g, " ").trim();
833
- if (oneLine.length <= max) return oneLine;
834
- return `${oneLine.slice(0, max - 1).trimEnd()}\u2026`;
832
+ const oneLine2 = notes.replace(/\s+/g, " ").trim();
833
+ if (oneLine2.length <= max) return oneLine2;
834
+ return `${oneLine2.slice(0, max - 1).trimEnd()}\u2026`;
835
835
  }
836
836
  function formatAccountProfilePlaybookLine(profile) {
837
837
  if (!profile.accountNotes) return "";
@@ -4747,13 +4747,13 @@ function parseDiffStat(result) {
4747
4747
  }
4748
4748
  function clipThinkingForStore(text6, max = THINKING_PART_MAX_CHARS) {
4749
4749
  if (text6.length <= max) return text6;
4750
- const marker = "\u2026(earlier thinking trimmed)\n";
4751
- return marker + text6.slice(text6.length - Math.max(1, max - marker.length));
4750
+ const marker2 = "\u2026(earlier thinking trimmed)\n";
4751
+ return marker2 + text6.slice(text6.length - Math.max(1, max - marker2.length));
4752
4752
  }
4753
4753
  function clipEventTail(text6, max) {
4754
4754
  if (text6.length <= max) return text6;
4755
- const marker = "\u2026(truncated)\n";
4756
- return marker + text6.slice(text6.length - Math.max(1, max - marker.length));
4755
+ const marker2 = "\u2026(truncated)\n";
4756
+ return marker2 + text6.slice(text6.length - Math.max(1, max - marker2.length));
4757
4757
  }
4758
4758
  function clipAgentEventForPaint(event) {
4759
4759
  if (event.type === "tool_result" && event.content) {
@@ -18311,18 +18311,18 @@ function budgetSeedBlocks(messages, blocks, maxChars) {
18311
18311
  used = next;
18312
18312
  }
18313
18313
  const dropped = keep.filter((k) => !k).length;
18314
- const marker = `_(${dropped} older message${dropped === 1 ? "" : "s"} omitted for length)_`;
18314
+ const marker2 = `_(${dropped} older message${dropped === 1 ? "" : "s"} omitted for length)_`;
18315
18315
  const kept = [];
18316
18316
  let markerPlaced = false;
18317
18317
  for (let i = 0; i < blocks.length; i++) {
18318
18318
  if (!keep[i]) continue;
18319
18319
  if (dropped > 0 && !markerPlaced && messages[i].role !== "summary") {
18320
- kept.push(marker);
18320
+ kept.push(marker2);
18321
18321
  markerPlaced = true;
18322
18322
  }
18323
18323
  kept.push(blocks[i]);
18324
18324
  }
18325
- if (dropped > 0 && !markerPlaced) kept.push(marker);
18325
+ if (dropped > 0 && !markerPlaced) kept.push(marker2);
18326
18326
  return kept.join("\n\n");
18327
18327
  }
18328
18328
  function buildSessionSeed(messages, opts) {
@@ -33375,8 +33375,8 @@ function crushMarkdown(text6, opts) {
33375
33375
  }
33376
33376
  const must = blocks.filter((b) => looksLikeMustKeep(b));
33377
33377
  const rest = blocks.filter((b) => !looksLikeMustKeep(b));
33378
- const marker = `[\u2026crushed body; ${original.length} chars originally. Pass include=full to read the rest.]`;
33379
- let used = must.reduce((n, b) => n + b.length, 0) + marker.length;
33378
+ const marker2 = `[\u2026crushed body; ${original.length} chars originally. Pass include=full to read the rest.]`;
33379
+ let used = must.reduce((n, b) => n + b.length, 0) + marker2.length;
33380
33380
  const keptRest = [];
33381
33381
  const headN = Math.max(1, Math.ceil(rest.length * 0.3));
33382
33382
  const tailN = Math.max(1, Math.ceil(rest.length * 0.15));
@@ -33416,7 +33416,7 @@ function crushMarkdown(text6, opts) {
33416
33416
  continue;
33417
33417
  }
33418
33418
  if (!inserted) {
33419
- out.push(marker);
33419
+ out.push(marker2);
33420
33420
  inserted = true;
33421
33421
  }
33422
33422
  }
@@ -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();
@@ -38753,6 +38802,144 @@ init_workspaces();
38753
38802
  init_chat_tabs();
38754
38803
  init_create();
38755
38804
  init_orchestrator();
38805
+
38806
+ // src/remote/phone-artifact.ts
38807
+ var FENCE_RE = /```([a-zA-Z0-9_+-]*)[^\n]*\n([\s\S]*?)(?:```|$)/g;
38808
+ var DOC_LANGS = /* @__PURE__ */ new Set(["html", "htm", "xhtml", "svg", "markdown", "md", "mdx", "jsx", "tsx", "react"]);
38809
+ var MIN_FENCE_CHARS = 40;
38810
+ var MIN_MD_CHARS = 120;
38811
+ function oneLine(value, max) {
38812
+ return value.replace(/[`\r\n]/g, " ").replace(/\s+/g, " ").trim().slice(0, max);
38813
+ }
38814
+ function asRecord13(value) {
38815
+ if (typeof value === "string") {
38816
+ try {
38817
+ return asRecord13(JSON.parse(value));
38818
+ } catch {
38819
+ return void 0;
38820
+ }
38821
+ }
38822
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
38823
+ return void 0;
38824
+ }
38825
+ function str6(value) {
38826
+ return typeof value === "string" ? value : "";
38827
+ }
38828
+ function normalizedBody(content) {
38829
+ return content.trim().replace(/\s+/g, " ");
38830
+ }
38831
+ function artifactHint(content) {
38832
+ const body = normalizedBody(content);
38833
+ if (!body) return "";
38834
+ return fnv1a32(body).toString(36);
38835
+ }
38836
+ function fnv1a32(value) {
38837
+ let hash = 2166136261;
38838
+ for (let i = 0; i < value.length; i++) {
38839
+ hash ^= value.charCodeAt(i);
38840
+ hash = Math.imul(hash, 16777619);
38841
+ }
38842
+ return hash >>> 0;
38843
+ }
38844
+ function looksLikeHtml(content) {
38845
+ const head = content.trim().slice(0, 200).toLowerCase();
38846
+ return head.startsWith("<!doctype") || head.startsWith("<html") || head.startsWith("<svg");
38847
+ }
38848
+ function kindFor(language, content) {
38849
+ const lang = language.toLowerCase();
38850
+ if (!lang) {
38851
+ if (!looksLikeHtml(content) || content.trim().length < MIN_FENCE_CHARS) return null;
38852
+ return content.trim().toLowerCase().startsWith("<svg") ? "svg" : "html";
38853
+ }
38854
+ if (!DOC_LANGS.has(lang)) return null;
38855
+ const min = lang === "markdown" || lang === "md" || lang === "mdx" ? MIN_MD_CHARS : MIN_FENCE_CHARS;
38856
+ if (content.trim().length < min) return null;
38857
+ if (lang === "svg") return "svg";
38858
+ if (lang === "markdown" || lang === "md" || lang === "mdx") return "markdown";
38859
+ if (lang === "jsx" || lang === "tsx" || lang === "react") return "react";
38860
+ return "html";
38861
+ }
38862
+ function titleFor(kind, content) {
38863
+ if (kind === "html" || kind === "svg") {
38864
+ const title = /<title[^>]*>([^<]+)<\/title>/i.exec(content)?.[1]?.trim();
38865
+ if (title) return oneLine(title, 80);
38866
+ }
38867
+ if (kind === "markdown") {
38868
+ const heading = /^#\s+(.+)$/m.exec(content)?.[1]?.trim();
38869
+ if (heading) return oneLine(heading, 80);
38870
+ }
38871
+ if (kind === "react") {
38872
+ const name = /export\s+default\s+(?:function|class)\s+([A-Za-z_$][\w$]*)/.exec(content)?.[1];
38873
+ if (name) return name;
38874
+ }
38875
+ if (kind === "svg") return "SVG artifact";
38876
+ if (kind === "markdown") return "Document";
38877
+ if (kind === "react") return "React artifact";
38878
+ return "HTML artifact";
38879
+ }
38880
+ function marker(title, hint) {
38881
+ return hint ? `\`\`\`sb-artifact
38882
+ ${title}
38883
+ ${hint}
38884
+ \`\`\`` : `\`\`\`sb-artifact
38885
+ ${title}
38886
+ \`\`\``;
38887
+ }
38888
+ function toolContent(input) {
38889
+ if (!input) return "";
38890
+ const direct = input.content;
38891
+ if (typeof direct === "string") return direct;
38892
+ const nested = asRecord13(direct);
38893
+ if (!nested) return "";
38894
+ for (const key of ["html", "markup", "text", "source", "content", "code"]) {
38895
+ if (typeof nested[key] === "string") return nested[key];
38896
+ }
38897
+ return "";
38898
+ }
38899
+ function isArtifactTool(name) {
38900
+ const short = name.split("__").pop() ?? name;
38901
+ return /^(present_artifact|create_artifact|update_artifact)$/i.test(short);
38902
+ }
38903
+ function phoneArtifactText(text6, parts, limit = Number.POSITIVE_INFINITY) {
38904
+ const hints = /* @__PURE__ */ new Set();
38905
+ const body = text6.replace(new RegExp(FENCE_RE.source, "g"), (full, language, content) => {
38906
+ const kind = kindFor(language, content);
38907
+ if (!kind) return full;
38908
+ const hint = artifactHint(content);
38909
+ hints.add(hint);
38910
+ return marker(titleFor(kind, content), hint);
38911
+ });
38912
+ const extras = [];
38913
+ for (const part of parts ?? []) {
38914
+ if (part.type !== "tool" || part.parentId || !isArtifactTool(part.name)) continue;
38915
+ const input = asRecord13(part.input);
38916
+ const content = toolContent(input);
38917
+ const hint = artifactHint(content);
38918
+ if (hint && hints.has(hint)) continue;
38919
+ const kind = kindFor("", content);
38920
+ const title = oneLine(str6(input?.title), 80) || (kind ? titleFor(kind, content) : "Artifact");
38921
+ if (!str6(input?.title) && content.trim().length < MIN_FENCE_CHARS) continue;
38922
+ if (hint) hints.add(hint);
38923
+ extras.push(marker(title, hint));
38924
+ }
38925
+ const tail = extras.join("\n\n");
38926
+ const room = tail ? Math.max(0, limit - tail.length - 2) : limit;
38927
+ const prose = body.length > room ? `${body.slice(0, Math.max(0, room - 1))}\u2026` : body;
38928
+ if (!tail) return prose;
38929
+ return prose ? `${prose}
38930
+
38931
+ ${tail}` : tail;
38932
+ }
38933
+ function parseOpenArtifact(value, chatId) {
38934
+ const record = asRecord13(value);
38935
+ if (!record) return "invalid";
38936
+ const title = oneLine(str6(record.title), 200);
38937
+ if (!title) return "invalid";
38938
+ const hint = oneLine(str6(record.hint), 200);
38939
+ return { op: "open-artifact", chatId, title, ...hint ? { hint } : {} };
38940
+ }
38941
+
38942
+ // src/remote/phone-chats.ts
38756
38943
  var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
38757
38944
  var PHONE_USER_PREFIX = "Phone\n\n";
38758
38945
  var TRANSCRIPT_LIMIT = 40;
@@ -38807,6 +38994,21 @@ function parsePhoneControl(value) {
38807
38994
  const chatId = typeof value.chatId === "string" ? value.chatId.trim() : "";
38808
38995
  if (!chatId) return "invalid";
38809
38996
  if (op === "open" || op === "archive" || op === "restore" || op === "stop") return { op, chatId };
38997
+ if (op === "open-artifact") return parseOpenArtifact(value, chatId);
38998
+ if (op === "open-file" || op === "media") {
38999
+ const path2 = textField(value, "path");
39000
+ if (!path2 || path2.length > 2e3) return "invalid";
39001
+ if (op === "media") return { op, chatId, path: path2 };
39002
+ const startLine = lineField(value, "startLine");
39003
+ const endLine = lineField(value, "endLine");
39004
+ return {
39005
+ op,
39006
+ chatId,
39007
+ path: path2,
39008
+ ...startLine != null ? { startLine } : {},
39009
+ ...endLine != null ? { endLine } : {}
39010
+ };
39011
+ }
38810
39012
  const draft = parsePhoneDraft(value);
38811
39013
  if (draft === "invalid") return "invalid";
38812
39014
  if (op === "options") return { op: "options", chatId, ...draft };
@@ -38822,6 +39024,11 @@ function textField(value, key) {
38822
39024
  const raw = value[key];
38823
39025
  return typeof raw === "string" ? raw.trim() : "";
38824
39026
  }
39027
+ function lineField(value, key) {
39028
+ const raw = value[key];
39029
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1 || raw > 1e6) return void 0;
39030
+ return raw;
39031
+ }
38825
39032
  function agentField(value) {
38826
39033
  const agent = textField(value, "agent");
38827
39034
  if (!agent) return void 0;
@@ -38965,7 +39172,8 @@ function summarize(thread) {
38965
39172
  title: thread.title?.trim() || "Untitled",
38966
39173
  status: thread.status,
38967
39174
  preview: previewOf(thread),
38968
- updatedAt: thread.updatedAt
39175
+ updatedAt: thread.updatedAt,
39176
+ agent: thread.agent
38969
39177
  };
38970
39178
  }
38971
39179
  function previewOf(thread) {
@@ -38979,10 +39187,8 @@ function listPhoneMessages(thread) {
38979
39187
  for (const message of thread.messages) {
38980
39188
  if (message.role !== "user" && message.role !== "agent") continue;
38981
39189
  const names = (message.attachments ?? []).map((item) => item.name.trim()).filter(Boolean);
38982
- const text6 = clip(
38983
- [phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n"),
38984
- MESSAGE_LIMIT
38985
- );
39190
+ const visible = [phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n");
39191
+ const text6 = message.role === "agent" ? phoneArtifactText(visible, message.parts, MESSAGE_LIMIT) : clip(visible, MESSAGE_LIMIT);
38986
39192
  if (!text6) continue;
38987
39193
  out.push({ role: message.role, text: text6 });
38988
39194
  }
@@ -39061,13 +39267,28 @@ function listPhoneHistory(opts) {
39061
39267
  ...more && last ? { next: historyCursor(last) } : {}
39062
39268
  };
39063
39269
  }
39064
- function readPhoneThread(chatId) {
39270
+ function matchPhoneThread(chatId) {
39065
39271
  const id = chatId.trim();
39066
39272
  if (!id) return null;
39067
- const thread = readThread(id);
39273
+ const exact = readThread(id);
39274
+ if (exact) return exact;
39275
+ const match = findThreadByRef(id);
39276
+ if (!match) return null;
39277
+ if (match.id !== id && !match.id.startsWith(id)) return null;
39278
+ return match;
39279
+ }
39280
+ function visiblePhoneThread(thread) {
39281
+ if (isGlobalThread(thread)) return true;
39282
+ return isProjectPath(thread.repoPath);
39283
+ }
39284
+ function findPhoneChat(chatId) {
39285
+ const thread = matchPhoneThread(chatId);
39286
+ if (!thread || !visiblePhoneThread(thread)) return null;
39287
+ return thread;
39288
+ }
39289
+ function readPhoneThread(chatId) {
39290
+ const thread = findPhoneChat(chatId);
39068
39291
  if (!thread || thread.status === "archived") return null;
39069
- if (isGlobalThread(thread)) return thread;
39070
- if (!isProjectPath(thread.repoPath)) return null;
39071
39292
  return thread;
39072
39293
  }
39073
39294
  function phoneComposerOptions(thread) {
@@ -39213,6 +39434,22 @@ async function createPhoneProjectWorktree(repoPathOrInput) {
39213
39434
  });
39214
39435
  return openedFrom(thread, projectPlace(thread));
39215
39436
  }
39437
+ async function listPhoneModels(agent) {
39438
+ const catalogs = await listModelsForAgent(agent);
39439
+ return (catalogs[0]?.models ?? []).slice(0, 40).map((model) => ({
39440
+ id: model.id,
39441
+ label: model.displayName || model.id
39442
+ }));
39443
+ }
39444
+ function archivePhoneChat(chatId) {
39445
+ const thread = readPhoneThread(chatId);
39446
+ if (!thread) return false;
39447
+ updateThread(thread.id, { status: "archived" });
39448
+ return true;
39449
+ }
39450
+
39451
+ // src/remote/phone-sources.ts
39452
+ init_worktree();
39216
39453
  function sourceWarning(label, err) {
39217
39454
  const message = err instanceof Error ? err.message : String(err);
39218
39455
  return message ? `${label}: ${message}` : `${label} failed.`;
@@ -39255,60 +39492,42 @@ async function listPhoneSources(repoPath, query) {
39255
39492
  ...warnings.length ? { warnings } : {}
39256
39493
  };
39257
39494
  }
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
39495
 
39272
- // src/remote/listen.ts
39273
- var REMOTE_STOPPED_REPLY = "Sideboard stopped the in-progress turn.";
39274
- function formatRemotePrompt(text6) {
39275
- return `Phone
39496
+ // src/remote/phone-live.ts
39497
+ init_orchestrator();
39498
+ init_thread_store();
39276
39499
 
39277
- ${text6.trim()}`;
39500
+ // src/remote/phone-stream.ts
39501
+ init_message_parts();
39502
+ var PHONE_STREAM_FLUSH_MS = 80;
39503
+ function phoneStreamFrame(parts) {
39504
+ return {
39505
+ text: phoneArtifactText(visibleAssistantText(void 0, parts), parts),
39506
+ activity: liveActivitySummary(parts)
39507
+ };
39278
39508
  }
39279
- function isRemoteStopCommand(text6) {
39280
- return text6.trim().toLowerCase() === "stop";
39509
+ var SKIPPED = /* @__PURE__ */ new Set(["session_id", "usage", "stderr", "exit"]);
39510
+ function phoneStreamParts(parts, event) {
39511
+ if (SKIPPED.has(event.type)) return null;
39512
+ return applyAgentEvent(parts, event);
39281
39513
  }
39282
- var handleChain2 = Promise.resolve();
39283
- var inboundGeneration = 0;
39514
+ function phoneStreamFlushNow(event) {
39515
+ return event.type === "tool_use" || event.type === "tool_result" && !event.partial;
39516
+ }
39517
+
39518
+ // src/remote/phone-live.ts
39519
+ var REMOTE_STOPPED_REPLY = "Sideboard stopped the in-progress turn.";
39284
39520
  var chatChains = /* @__PURE__ */ new Map();
39285
39521
  var chatGeneration = /* @__PURE__ */ new Map();
39286
- function enqueue(fn) {
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
- }
39522
+ var phoneTurnGen = /* @__PURE__ */ new Map();
39307
39523
  function bumpChat(chatId) {
39308
39524
  const next = (chatGeneration.get(chatId) ?? 0) + 1;
39309
39525
  chatGeneration.set(chatId, next);
39310
39526
  return next;
39311
39527
  }
39528
+ function finishPhoneTurn(chatId, generation) {
39529
+ if (phoneTurnGen.get(chatId) === generation) phoneTurnGen.delete(chatId);
39530
+ }
39312
39531
  function enqueueChat(chatId, fn) {
39313
39532
  const prev = chatChains.get(chatId) ?? Promise.resolve();
39314
39533
  const run2 = prev.then(fn, fn);
@@ -39323,6 +39542,49 @@ function enqueueChat(chatId, fn) {
39323
39542
  function emitControl(opts, reply) {
39324
39543
  opts.onOutbound({ type: "assistant", text: encodePhoneControl(reply) });
39325
39544
  }
39545
+ function watchPhoneStream(opts, chatId, threadId, generation) {
39546
+ let parts = [];
39547
+ let timer = null;
39548
+ let lastKey = "";
39549
+ let closed = false;
39550
+ const alive = () => !closed && chatGeneration.get(chatId) === generation;
39551
+ const flush2 = () => {
39552
+ timer = null;
39553
+ if (!alive()) return;
39554
+ const frame = phoneStreamFrame(parts);
39555
+ const key = `${frame.activity}
39556
+ ${frame.text}`;
39557
+ if (key === lastKey) return;
39558
+ if (!frame.text && !frame.activity) return;
39559
+ lastKey = key;
39560
+ emitControl(opts, { op: "stream", chatId, text: frame.text, activity: frame.activity });
39561
+ };
39562
+ const schedule = (event) => {
39563
+ if (phoneStreamFlushNow(event) || lastKey === "") {
39564
+ if (timer) {
39565
+ clearTimeout(timer);
39566
+ timer = null;
39567
+ }
39568
+ flush2();
39569
+ return;
39570
+ }
39571
+ if (timer) return;
39572
+ timer = setTimeout(flush2, PHONE_STREAM_FLUSH_MS);
39573
+ };
39574
+ const off = getOrchestrator().on((event) => {
39575
+ if (!alive()) return;
39576
+ if (event.type !== "turn_output" || event.threadId !== threadId) return;
39577
+ const next = phoneStreamParts(parts, event.event);
39578
+ if (!next) return;
39579
+ parts = next;
39580
+ schedule(event.event);
39581
+ });
39582
+ return () => {
39583
+ closed = true;
39584
+ if (timer) clearTimeout(timer);
39585
+ off();
39586
+ };
39587
+ }
39326
39588
  function interruptChat(chatId, log) {
39327
39589
  try {
39328
39590
  const thread = readPhoneThread(chatId);
@@ -39370,7 +39632,12 @@ function emitChatTurn(opts, chatId, threadId) {
39370
39632
  completedEmpty: () => "",
39371
39633
  failed: (detail) => detail ? `Sideboard failed: ${detail}` : "Sideboard failed before producing a result."
39372
39634
  });
39373
- emitControl(opts, { op: "assistant", chatId, text: reply.trim() });
39635
+ const agent = [...thread.messages].reverse().find((m) => m.role === "agent");
39636
+ emitControl(opts, {
39637
+ op: "assistant",
39638
+ chatId,
39639
+ text: phoneArtifactText(reply.trim(), agent?.parts)
39640
+ });
39374
39641
  }
39375
39642
  function phoneError(err) {
39376
39643
  return err instanceof Error ? err.message : String(err);
@@ -39378,20 +39645,73 @@ function phoneError(err) {
39378
39645
  function deliverPhoneTurn(opts, chatId) {
39379
39646
  const thread = readPhoneThread(chatId);
39380
39647
  if (!thread || thread.status !== "running" && thread.status !== "queued") return;
39381
- const generation = bumpChat(chatId);
39382
- enqueueChat(chatId, async () => {
39383
- if (chatGeneration.get(chatId) !== generation) return;
39648
+ const id = thread.id;
39649
+ const claimed = phoneTurnGen.get(id);
39650
+ if (claimed != null && claimed === chatGeneration.get(id)) return;
39651
+ const generation = bumpChat(id);
39652
+ phoneTurnGen.set(id, generation);
39653
+ enqueueChat(id, async () => {
39654
+ if (chatGeneration.get(id) !== generation) return;
39655
+ const stopStream = watchPhoneStream(opts, id, id, generation);
39384
39656
  try {
39385
- await getOrchestrator().waitForTurn(chatId, 14 * 60 * 1e3);
39657
+ await getOrchestrator().waitForTurn(id, 14 * 60 * 1e3);
39386
39658
  } catch (err) {
39387
- if (chatGeneration.get(chatId) !== generation) return;
39388
- emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${phoneError(err)}` });
39659
+ stopStream();
39660
+ finishPhoneTurn(id, generation);
39661
+ if (chatGeneration.get(id) !== generation) return;
39662
+ emitControl(opts, { op: "error", chatId: id, message: `Sideboard failed: ${phoneError(err)}` });
39389
39663
  return;
39390
39664
  }
39391
- if (chatGeneration.get(chatId) !== generation) return;
39392
- emitChatTurn(opts, chatId, chatId);
39665
+ stopStream();
39666
+ finishPhoneTurn(id, generation);
39667
+ if (chatGeneration.get(id) !== generation) return;
39668
+ emitChatTurn(opts, id, id);
39393
39669
  });
39394
39670
  }
39671
+
39672
+ // src/remote/listen.ts
39673
+ function formatRemotePrompt(text6) {
39674
+ return `Phone
39675
+
39676
+ ${text6.trim()}`;
39677
+ }
39678
+ function isRemoteStopCommand(text6) {
39679
+ return text6.trim().toLowerCase() === "stop";
39680
+ }
39681
+ var handleChain2 = Promise.resolve();
39682
+ var inboundGeneration = 0;
39683
+ function enqueue(fn) {
39684
+ const run2 = handleChain2.then(fn, fn);
39685
+ handleChain2 = run2.then(
39686
+ () => void 0,
39687
+ () => void 0
39688
+ );
39689
+ }
39690
+ function interruptRemoteCoordinator(deviceId, log = () => void 0) {
39691
+ try {
39692
+ const live = findRemoteCoordinator(deviceId);
39693
+ if (!live) return false;
39694
+ const fresh = readThread(live.id) ?? live;
39695
+ if (fresh.status !== "running" && fresh.status !== "queued") return false;
39696
+ getOrchestrator().stop(fresh.id, { clearQueue: true });
39697
+ log(`interrupt phone \u2192 coordinator ${fresh.id.slice(0, 8)} (${fresh.status})`);
39698
+ return true;
39699
+ } catch (err) {
39700
+ log(`interrupt: ${err instanceof Error ? err.message : String(err)}`);
39701
+ return false;
39702
+ }
39703
+ }
39704
+ function publishOpenedChat(opts, chatId) {
39705
+ const opened = openPhoneChat(chatId);
39706
+ if (!opened) {
39707
+ emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
39708
+ return;
39709
+ }
39710
+ emitControl(opts, { op: "opened", ...opened });
39711
+ if (opened.chat.status === "running" || opened.chat.status === "queued") {
39712
+ deliverPhoneTurn(opts, opened.chat.id);
39713
+ }
39714
+ }
39395
39715
  function applyPhoneOptions(chatId, draft) {
39396
39716
  const patch = {};
39397
39717
  if (draft.agent) patch.agent = draft.agent;
@@ -39542,12 +39862,21 @@ function handlePhoneControl(cmd, opts) {
39542
39862
  return;
39543
39863
  }
39544
39864
  if (cmd.op === "open") {
39545
- const opened = openPhoneChat(cmd.chatId);
39546
- if (!opened) {
39865
+ const thread = findPhoneChat(cmd.chatId);
39866
+ if (!thread) {
39547
39867
  emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
39548
39868
  return;
39549
39869
  }
39550
- emitControl(opts, { op: "opened", ...opened });
39870
+ if (thread.status === "archived") {
39871
+ void getOrchestrator().restore(thread.id).then(() => {
39872
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
39873
+ publishOpenedChat(opts, thread.id);
39874
+ }).catch((err) => {
39875
+ emitControl(opts, { op: "error", message: phoneError(err) });
39876
+ });
39877
+ return;
39878
+ }
39879
+ publishOpenedChat(opts, thread.id);
39551
39880
  return;
39552
39881
  }
39553
39882
  if (cmd.op === "archive") {
@@ -39576,6 +39905,54 @@ function handlePhoneControl(cmd, opts) {
39576
39905
  });
39577
39906
  return;
39578
39907
  }
39908
+ if (cmd.op === "media") {
39909
+ const thread = readPhoneThread(cmd.chatId);
39910
+ const dataUrl = thread ? phoneImageDataUrl(thread.worktreePath, cmd.path) : null;
39911
+ emitControl(opts, {
39912
+ op: "media",
39913
+ chatId: cmd.chatId,
39914
+ path: cmd.path,
39915
+ ...dataUrl ? { dataUrl } : {}
39916
+ });
39917
+ return;
39918
+ }
39919
+ if (cmd.op === "open-file") {
39920
+ const thread = readPhoneThread(cmd.chatId);
39921
+ const relative = thread ? phoneWorktreeRelative(thread.worktreePath, cmd.path) : null;
39922
+ if (!thread || !relative) {
39923
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
39924
+ return;
39925
+ }
39926
+ void getOrchestrator().statPath(thread.id, relative).then((kind) => {
39927
+ if (kind === "missing") {
39928
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
39929
+ return;
39930
+ }
39931
+ opts.onOpenWorktreeFile?.({
39932
+ threadId: thread.id,
39933
+ path: relative,
39934
+ ...kind === "dir" ? { directory: true } : {},
39935
+ ...cmd.startLine != null ? { startLine: cmd.startLine } : {},
39936
+ ...cmd.endLine != null ? { endLine: cmd.endLine } : {}
39937
+ });
39938
+ }).catch(() => {
39939
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
39940
+ });
39941
+ return;
39942
+ }
39943
+ if (cmd.op === "open-artifact") {
39944
+ const thread = readPhoneThread(cmd.chatId);
39945
+ if (!thread) {
39946
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
39947
+ return;
39948
+ }
39949
+ opts.onOpenArtifact?.({
39950
+ threadId: thread.id,
39951
+ title: cmd.title,
39952
+ ...cmd.hint ? { hint: cmd.hint } : {}
39953
+ });
39954
+ return;
39955
+ }
39579
39956
  if (cmd.op === "stop") {
39580
39957
  if (!readPhoneThread(cmd.chatId)) {
39581
39958
  emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
@@ -39588,6 +39965,7 @@ function handlePhoneControl(cmd, opts) {
39588
39965
  }
39589
39966
  const chatId = cmd.chatId;
39590
39967
  const generation = bumpChat(chatId);
39968
+ phoneTurnGen.set(chatId, generation);
39591
39969
  interruptChat(chatId, log);
39592
39970
  enqueueChat(chatId, async () => {
39593
39971
  if (chatGeneration.get(chatId) !== generation) return;
@@ -39603,6 +39981,7 @@ function handlePhoneControl(cmd, opts) {
39603
39981
  }
39604
39982
  }
39605
39983
  if (chatGeneration.get(chatId) !== generation) return;
39984
+ const stopStream = watchPhoneStream(opts, chatId, thread.id, generation);
39606
39985
  try {
39607
39986
  try {
39608
39987
  applyPhoneOptions(thread.id, cmd);
@@ -39620,6 +39999,9 @@ function handlePhoneControl(cmd, opts) {
39620
39999
  const message = err instanceof Error ? err.message : String(err);
39621
40000
  emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${message}` });
39622
40001
  return;
40002
+ } finally {
40003
+ stopStream();
40004
+ finishPhoneTurn(chatId, generation);
39623
40005
  }
39624
40006
  if (chatGeneration.get(chatId) !== generation) return;
39625
40007
  emitChatTurn(opts, chatId, thread.id);
@@ -39861,7 +40243,8 @@ function runRemoteHost(opts) {
39861
40243
  handleRemoteInbound("stop", {
39862
40244
  deviceId: opts.deviceId,
39863
40245
  onLog: log,
39864
- onOutbound: emit
40246
+ onOutbound: emit,
40247
+ onOpenWorktreeFile: opts.onOpenWorktreeFile
39865
40248
  });
39866
40249
  return;
39867
40250
  }
@@ -39870,7 +40253,9 @@ function runRemoteHost(opts) {
39870
40253
  deviceId: opts.deviceId,
39871
40254
  onLog: log,
39872
40255
  onOutbound: emit,
39873
- transcribeWav: opts.transcribeWav
40256
+ transcribeWav: opts.transcribeWav,
40257
+ onOpenWorktreeFile: opts.onOpenWorktreeFile,
40258
+ onOpenArtifact: opts.onOpenArtifact
39874
40259
  });
39875
40260
  }
39876
40261
  };
@@ -40269,9 +40654,74 @@ function sendJson2(res, status, body) {
40269
40654
  res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
40270
40655
  res.end(JSON.stringify(body));
40271
40656
  }
40657
+ function relayOAuthReturnPage(title, body) {
40658
+ return `<!DOCTYPE html>
40659
+ <html lang="en">
40660
+ <head>
40661
+ <meta charset="utf-8">
40662
+ <meta name="viewport" content="width=device-width, initial-scale=1">
40663
+ <meta name="robots" content="noindex">
40664
+ <title>${escapeHtml2(title)}</title>
40665
+ <style>
40666
+ :root {
40667
+ --bg: #09090b;
40668
+ --card: #16171b;
40669
+ --line: rgba(255, 255, 255, 0.08);
40670
+ --text: #f4f5f7;
40671
+ --muted: #9aa3ad;
40672
+ --blue: #3b9eff;
40673
+ --blue-fill: #0f7ed4;
40674
+ }
40675
+ * { box-sizing: border-box }
40676
+ body {
40677
+ margin: 0;
40678
+ min-height: 100vh;
40679
+ display: grid;
40680
+ place-items: center;
40681
+ padding: 48px 24px;
40682
+ background:
40683
+ radial-gradient(closest-side, rgba(15, 126, 212, 0.22), transparent 72%) center -120px / 720px 480px no-repeat,
40684
+ var(--bg);
40685
+ color: var(--text);
40686
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", sans-serif;
40687
+ -webkit-font-smoothing: antialiased;
40688
+ }
40689
+ main {
40690
+ width: min(28rem, 100%);
40691
+ background: var(--card);
40692
+ border: 1px solid var(--line);
40693
+ border-radius: 16px;
40694
+ padding: 28px 28px 32px;
40695
+ }
40696
+ .brand {
40697
+ display: flex;
40698
+ align-items: center;
40699
+ gap: 10px;
40700
+ margin: 0 0 22px;
40701
+ font-weight: 650;
40702
+ font-size: 17px;
40703
+ letter-spacing: -0.02em;
40704
+ }
40705
+ .mark { position: relative; display: inline-block; width: 26px; height: 26px; flex: none }
40706
+ .mark i { position: absolute; width: 18px; height: 18px; border-radius: 3px; transform: rotate(14deg) }
40707
+ .mark i:first-child { left: 6px; top: 6px; background: var(--blue-fill) }
40708
+ .mark i:last-child { left: 1px; top: 1px; border: 2px solid #d7dbdf }
40709
+ h1 { margin: 0; font-size: 1.5rem; line-height: 1.2; letter-spacing: -0.03em; font-weight: 650 }
40710
+ p { margin: 10px 0 0; line-height: 1.55; color: var(--muted) }
40711
+ a { color: var(--blue) }
40712
+ </style>
40713
+ </head>
40714
+ <body>
40715
+ <main>
40716
+ <div class="brand"><span class="mark" aria-hidden="true"><i></i><i></i></span>Sideboard</div>
40717
+ ${body}
40718
+ </main>
40719
+ </body>
40720
+ </html>`;
40721
+ }
40272
40722
  function sendHtml(res, status, title, body) {
40273
40723
  res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
40274
- res.end(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>${escapeHtml2(title)}</title></head><body>${body}</body></html>`);
40724
+ res.end(relayOAuthReturnPage(title, body));
40275
40725
  }
40276
40726
  async function handleRelayAccountHttp(opts) {
40277
40727
  const reqUrl = opts.req.url || "/";