@sideboard-ai/core 0.1.276 → 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
  }
@@ -38802,6 +38802,144 @@ init_workspaces();
38802
38802
  init_chat_tabs();
38803
38803
  init_create();
38804
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
38805
38943
  var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
38806
38944
  var PHONE_USER_PREFIX = "Phone\n\n";
38807
38945
  var TRANSCRIPT_LIMIT = 40;
@@ -38856,6 +38994,7 @@ function parsePhoneControl(value) {
38856
38994
  const chatId = typeof value.chatId === "string" ? value.chatId.trim() : "";
38857
38995
  if (!chatId) return "invalid";
38858
38996
  if (op === "open" || op === "archive" || op === "restore" || op === "stop") return { op, chatId };
38997
+ if (op === "open-artifact") return parseOpenArtifact(value, chatId);
38859
38998
  if (op === "open-file" || op === "media") {
38860
38999
  const path2 = textField(value, "path");
38861
39000
  if (!path2 || path2.length > 2e3) return "invalid";
@@ -39033,7 +39172,8 @@ function summarize(thread) {
39033
39172
  title: thread.title?.trim() || "Untitled",
39034
39173
  status: thread.status,
39035
39174
  preview: previewOf(thread),
39036
- updatedAt: thread.updatedAt
39175
+ updatedAt: thread.updatedAt,
39176
+ agent: thread.agent
39037
39177
  };
39038
39178
  }
39039
39179
  function previewOf(thread) {
@@ -39047,10 +39187,8 @@ function listPhoneMessages(thread) {
39047
39187
  for (const message of thread.messages) {
39048
39188
  if (message.role !== "user" && message.role !== "agent") continue;
39049
39189
  const names = (message.attachments ?? []).map((item) => item.name.trim()).filter(Boolean);
39050
- const text6 = clip(
39051
- [phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n"),
39052
- MESSAGE_LIMIT
39053
- );
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);
39054
39192
  if (!text6) continue;
39055
39193
  out.push({ role: message.role, text: text6 });
39056
39194
  }
@@ -39364,7 +39502,7 @@ init_message_parts();
39364
39502
  var PHONE_STREAM_FLUSH_MS = 80;
39365
39503
  function phoneStreamFrame(parts) {
39366
39504
  return {
39367
- text: visibleAssistantText(void 0, parts),
39505
+ text: phoneArtifactText(visibleAssistantText(void 0, parts), parts),
39368
39506
  activity: liveActivitySummary(parts)
39369
39507
  };
39370
39508
  }
@@ -39494,7 +39632,12 @@ function emitChatTurn(opts, chatId, threadId) {
39494
39632
  completedEmpty: () => "",
39495
39633
  failed: (detail) => detail ? `Sideboard failed: ${detail}` : "Sideboard failed before producing a result."
39496
39634
  });
39497
- 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
+ });
39498
39641
  }
39499
39642
  function phoneError(err) {
39500
39643
  return err instanceof Error ? err.message : String(err);
@@ -39797,6 +39940,19 @@ function handlePhoneControl(cmd, opts) {
39797
39940
  });
39798
39941
  return;
39799
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
+ }
39800
39956
  if (cmd.op === "stop") {
39801
39957
  if (!readPhoneThread(cmd.chatId)) {
39802
39958
  emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
@@ -40098,7 +40254,8 @@ function runRemoteHost(opts) {
40098
40254
  onLog: log,
40099
40255
  onOutbound: emit,
40100
40256
  transcribeWav: opts.transcribeWav,
40101
- onOpenWorktreeFile: opts.onOpenWorktreeFile
40257
+ onOpenWorktreeFile: opts.onOpenWorktreeFile,
40258
+ onOpenArtifact: opts.onOpenArtifact
40102
40259
  });
40103
40260
  }
40104
40261
  };
package/dist/index.d.cts CHANGED
@@ -7159,6 +7159,12 @@ interface IpcApi {
7159
7159
  startLine?: number;
7160
7160
  endLine?: number;
7161
7161
  }) => void): () => void;
7162
+ /** Phone asked the Mac to open a chat artifact in the side column. */
7163
+ onPhoneOpenArtifact(listener: (request: {
7164
+ threadId: string;
7165
+ title: string;
7166
+ hint?: string;
7167
+ }) => void): () => void;
7162
7168
  /** Subscribe to store directory changes (CLI threads appear live). */
7163
7169
  onThreadsChanged(listener: () => void): () => void;
7164
7170
  getRepoPath(): Promise<string>;
@@ -7775,6 +7781,12 @@ declare class RemoteHub {
7775
7781
  private fromHost;
7776
7782
  }
7777
7783
 
7784
+ type PhoneOpenArtifactRequest = {
7785
+ threadId: string;
7786
+ title: string;
7787
+ hint?: string;
7788
+ };
7789
+
7778
7790
  type PhoneOpenFileRequest = {
7779
7791
  threadId: string;
7780
7792
  path: string;
@@ -7801,6 +7813,8 @@ interface RemoteHostClientOptions {
7801
7813
  transcribeWav?: (wavBase64: string) => Promise<string>;
7802
7814
  /** Open a worktree file or folder in the desktop window. */
7803
7815
  onOpenWorktreeFile?: (request: PhoneOpenFileRequest) => void;
7816
+ /** Open a chat artifact in the desktop side column. */
7817
+ onOpenArtifact?: (request: PhoneOpenArtifactRequest) => void;
7804
7818
  /** Tests: override keepalive cadence. */
7805
7819
  pingIntervalMs?: number;
7806
7820
  pongTimeoutMs?: number;
@@ -7894,6 +7908,7 @@ declare function handleRemoteInbound(text: string, opts: {
7894
7908
  onLog?: (line: string) => void;
7895
7909
  transcribeWav?: (wavBase64: string) => Promise<string>;
7896
7910
  onOpenWorktreeFile?: (request: PhoneOpenFileRequest) => void;
7911
+ onOpenArtifact?: (request: PhoneOpenArtifactRequest) => void;
7897
7912
  }): void;
7898
7913
 
7899
7914
  declare function isRemoteAccountLoginCancelled(err: unknown): boolean;
package/dist/index.d.ts CHANGED
@@ -7159,6 +7159,12 @@ interface IpcApi {
7159
7159
  startLine?: number;
7160
7160
  endLine?: number;
7161
7161
  }) => void): () => void;
7162
+ /** Phone asked the Mac to open a chat artifact in the side column. */
7163
+ onPhoneOpenArtifact(listener: (request: {
7164
+ threadId: string;
7165
+ title: string;
7166
+ hint?: string;
7167
+ }) => void): () => void;
7162
7168
  /** Subscribe to store directory changes (CLI threads appear live). */
7163
7169
  onThreadsChanged(listener: () => void): () => void;
7164
7170
  getRepoPath(): Promise<string>;
@@ -7775,6 +7781,12 @@ declare class RemoteHub {
7775
7781
  private fromHost;
7776
7782
  }
7777
7783
 
7784
+ type PhoneOpenArtifactRequest = {
7785
+ threadId: string;
7786
+ title: string;
7787
+ hint?: string;
7788
+ };
7789
+
7778
7790
  type PhoneOpenFileRequest = {
7779
7791
  threadId: string;
7780
7792
  path: string;
@@ -7801,6 +7813,8 @@ interface RemoteHostClientOptions {
7801
7813
  transcribeWav?: (wavBase64: string) => Promise<string>;
7802
7814
  /** Open a worktree file or folder in the desktop window. */
7803
7815
  onOpenWorktreeFile?: (request: PhoneOpenFileRequest) => void;
7816
+ /** Open a chat artifact in the desktop side column. */
7817
+ onOpenArtifact?: (request: PhoneOpenArtifactRequest) => void;
7804
7818
  /** Tests: override keepalive cadence. */
7805
7819
  pingIntervalMs?: number;
7806
7820
  pongTimeoutMs?: number;
@@ -7894,6 +7908,7 @@ declare function handleRemoteInbound(text: string, opts: {
7894
7908
  onLog?: (line: string) => void;
7895
7909
  transcribeWav?: (wavBase64: string) => Promise<string>;
7896
7910
  onOpenWorktreeFile?: (request: PhoneOpenFileRequest) => void;
7911
+ onOpenArtifact?: (request: PhoneOpenArtifactRequest) => void;
7897
7912
  }): void;
7898
7913
 
7899
7914
  declare function isRemoteAccountLoginCancelled(err: unknown): boolean;
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
  }
@@ -9426,6 +9426,144 @@ function phoneImageDataUrl(worktreePath, raw) {
9426
9426
 
9427
9427
  // src/remote/phone-chats.ts
9428
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
9429
9567
  var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
9430
9568
  var PHONE_USER_PREFIX = "Phone\n\n";
9431
9569
  var TRANSCRIPT_LIMIT = 40;
@@ -9480,6 +9618,7 @@ function parsePhoneControl(value) {
9480
9618
  const chatId = typeof value.chatId === "string" ? value.chatId.trim() : "";
9481
9619
  if (!chatId) return "invalid";
9482
9620
  if (op === "open" || op === "archive" || op === "restore" || op === "stop") return { op, chatId };
9621
+ if (op === "open-artifact") return parseOpenArtifact(value, chatId);
9483
9622
  if (op === "open-file" || op === "media") {
9484
9623
  const path2 = textField(value, "path");
9485
9624
  if (!path2 || path2.length > 2e3) return "invalid";
@@ -9657,7 +9796,8 @@ function summarize(thread) {
9657
9796
  title: thread.title?.trim() || "Untitled",
9658
9797
  status: thread.status,
9659
9798
  preview: previewOf(thread),
9660
- updatedAt: thread.updatedAt
9799
+ updatedAt: thread.updatedAt,
9800
+ agent: thread.agent
9661
9801
  };
9662
9802
  }
9663
9803
  function previewOf(thread) {
@@ -9671,10 +9811,8 @@ function listPhoneMessages(thread) {
9671
9811
  for (const message of thread.messages) {
9672
9812
  if (message.role !== "user" && message.role !== "agent") continue;
9673
9813
  const names = (message.attachments ?? []).map((item) => item.name.trim()).filter(Boolean);
9674
- const text6 = clip(
9675
- [phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n"),
9676
- MESSAGE_LIMIT
9677
- );
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);
9678
9816
  if (!text6) continue;
9679
9817
  out.push({ role: message.role, text: text6 });
9680
9818
  }
@@ -9982,7 +10120,7 @@ async function listPhoneSources(repoPath, query) {
9982
10120
  var PHONE_STREAM_FLUSH_MS = 80;
9983
10121
  function phoneStreamFrame(parts) {
9984
10122
  return {
9985
- text: visibleAssistantText(void 0, parts),
10123
+ text: phoneArtifactText(visibleAssistantText(void 0, parts), parts),
9986
10124
  activity: liveActivitySummary(parts)
9987
10125
  };
9988
10126
  }
@@ -10112,7 +10250,12 @@ function emitChatTurn(opts, chatId, threadId) {
10112
10250
  completedEmpty: () => "",
10113
10251
  failed: (detail) => detail ? `Sideboard failed: ${detail}` : "Sideboard failed before producing a result."
10114
10252
  });
10115
- 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
+ });
10116
10259
  }
10117
10260
  function phoneError(err) {
10118
10261
  return err instanceof Error ? err.message : String(err);
@@ -10415,6 +10558,19 @@ function handlePhoneControl(cmd, opts) {
10415
10558
  });
10416
10559
  return;
10417
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
+ }
10418
10574
  if (cmd.op === "stop") {
10419
10575
  if (!readPhoneThread(cmd.chatId)) {
10420
10576
  emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
@@ -10716,7 +10872,8 @@ function runRemoteHost(opts) {
10716
10872
  onLog: log,
10717
10873
  onOutbound: emit,
10718
10874
  transcribeWav: opts.transcribeWav,
10719
- onOpenWorktreeFile: opts.onOpenWorktreeFile
10875
+ onOpenWorktreeFile: opts.onOpenWorktreeFile,
10876
+ onOpenArtifact: opts.onOpenArtifact
10720
10877
  });
10721
10878
  }
10722
10879
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sideboard-ai/core",
3
- "version": "0.1.276",
3
+ "version": "0.1.277",
4
4
  "description": "Sideboard core — orchestration, agents, git worktrees, MCP server",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",