@sideboard-ai/core 0.1.275 → 0.1.276

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