@sideboard-ai/core 0.1.301 → 0.1.303

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.
Files changed (37) hide show
  1. package/dist/agents/claude-runner.cjs +28 -7
  2. package/dist/agents/claude-runner.js +22 -8
  3. package/dist/{agents-V2SRC2LH.js → agents-3GN4E2X3.js} +5 -5
  4. package/dist/{agents-ETD2LWP4.js → agents-XLHFVC64.js} +4 -4
  5. package/dist/{chunk-FUXOZHHF.js → chunk-3TZRSKFJ.js} +4 -4
  6. package/dist/{chunk-JTW4CZEQ.js → chunk-545DY7CQ.js} +13 -13
  7. package/dist/{chunk-3XDU7RIE.js → chunk-5MVOZLN7.js} +1 -1
  8. package/dist/{chunk-2KJE53ZJ.js → chunk-7AYOOG77.js} +5 -4
  9. package/dist/{chunk-7SEWZXPT.js → chunk-BRFDPV7Z.js} +2 -2
  10. package/dist/{chunk-EP4IIKP4.js → chunk-DW6NNYUJ.js} +4 -4
  11. package/dist/{chunk-FO3NKZVJ.js → chunk-F2PVRF3B.js} +9 -0
  12. package/dist/{chunk-KIWICTLW.js → chunk-GEFH4QFX.js} +1 -1
  13. package/dist/{chunk-FPJFK4LB.js → chunk-GS6O65Q2.js} +11 -11
  14. package/dist/{chunk-WV33I2GC.js → chunk-P2GHFJOB.js} +1 -1
  15. package/dist/{chunk-OJKSDRZU.js → chunk-RAGDMEIB.js} +4 -4
  16. package/dist/{chunk-LZ744H2I.js → chunk-YCDHLK2Z.js} +2 -2
  17. package/dist/{conductor-57UKACQA.js → conductor-2AVFGSQJ.js} +4 -4
  18. package/dist/{conductor-57KZLWTB.js → conductor-ATST6OMC.js} +5 -5
  19. package/dist/{coordinator-prompt-VOWUJRVJ.js → coordinator-prompt-CQPUNMRX.js} +4 -4
  20. package/dist/{coordinator-prompt-2TBRFJZ5.js → coordinator-prompt-TIAI6A47.js} +3 -3
  21. package/dist/{global-workspace-V6SOEETA.js → global-workspace-7YABMLQG.js} +4 -4
  22. package/dist/{global-workspace-T5C7EMVV.js → global-workspace-UZJUHGQP.js} +3 -3
  23. package/dist/index.cjs +851 -367
  24. package/dist/index.d.cts +104 -2
  25. package/dist/index.d.ts +104 -2
  26. package/dist/index.js +825 -354
  27. package/dist/mcp/run-stdio.js +6 -6
  28. package/dist/{orchestrator-C43M3JDK.js → orchestrator-COX6QO2B.js} +6 -6
  29. package/dist/{orchestrator-JEF7JC3S.js → orchestrator-CZPRDLHV.js} +7 -7
  30. package/dist/{thread-store-POAYB2ID.js → thread-store-PNX4HVLO.js} +2 -2
  31. package/dist/{thread-store-BMNJEJUH.js → thread-store-VC4AIKN3.js} +1 -1
  32. package/dist/{workspaces-EWVIEJ4O.js → workspaces-6DJL4IST.js} +4 -4
  33. package/dist/{workspaces-7ZYTTB4A.js → workspaces-GZ7F5EJR.js} +3 -3
  34. package/dist/{worktree-2Q6J2VWZ.js → worktree-2BYVRYPJ.js} +3 -3
  35. package/dist/{worktree-ZAJGYKCB.js → worktree-VXPOY2LE.js} +2 -2
  36. package/package.json +1 -1
  37. /package/dist/{chunk-FBZODCO4.js → chunk-FA3V56KF.js} +0 -0
package/dist/index.js CHANGED
@@ -196,7 +196,7 @@ import {
196
196
  worktreeCleanupSettings,
197
197
  writeAgentBlock,
198
198
  writeWorktreeFile
199
- } from "./chunk-JTW4CZEQ.js";
199
+ } from "./chunk-545DY7CQ.js";
200
200
  import {
201
201
  LEGACY_PLAN_FILE_REL,
202
202
  PLAN_FILE_NAME,
@@ -261,6 +261,7 @@ import {
261
261
  listCursorModels,
262
262
  listModelsForAgent,
263
263
  listOpencodeModels,
264
+ listUserClaudeMcpServerEntries,
264
265
  mcpStatusThinkingFromClaudeInit,
265
266
  mergeSideboardIntoMcpServersJson,
266
267
  normalizeTurnInput,
@@ -283,7 +284,7 @@ import {
283
284
  userMcpNamesToDisable,
284
285
  worktreeMcpToolNames,
285
286
  writeInjectedMcpConfig
286
- } from "./chunk-2KJE53ZJ.js";
287
+ } from "./chunk-7AYOOG77.js";
287
288
  import {
288
289
  brightsyMcpServerName,
289
290
  connectBrightsyTeam,
@@ -411,7 +412,7 @@ import {
411
412
  workspaceAgentChatSummaries,
412
413
  worktreeRunPortsPath,
413
414
  wrapReviewSkillMarkdown
414
- } from "./chunk-OJKSDRZU.js";
415
+ } from "./chunk-RAGDMEIB.js";
415
416
  import {
416
417
  STDERR_EVENT_MAX_CHARS,
417
418
  STDOUT_EVENT_MAX_CHARS,
@@ -611,7 +612,7 @@ import {
611
612
  stageBuffersAsAttachments,
612
613
  syncWorkspacesFromThreads,
613
614
  takenTeamSlugsForOrchestration
614
- } from "./chunk-7SEWZXPT.js";
615
+ } from "./chunk-BRFDPV7Z.js";
615
616
  import {
616
617
  GITHUB_PR_BODY_MAX_CHARS,
617
618
  GITHUB_PR_BODY_SAFE_CHARS,
@@ -699,7 +700,7 @@ import {
699
700
  suggestSlug,
700
701
  switchWorktreeBranch,
701
702
  warmGithubAgentAuth
702
- } from "./chunk-KIWICTLW.js";
703
+ } from "./chunk-GEFH4QFX.js";
703
704
  import {
704
705
  FAMOUS_SOCCER_TEAMS,
705
706
  allocateTeamName,
@@ -732,7 +733,7 @@ import {
732
733
  worktreeNameFromPath,
733
734
  worktreeSlugForTicket,
734
735
  writeThread
735
- } from "./chunk-WV33I2GC.js";
736
+ } from "./chunk-P2GHFJOB.js";
736
737
  import "./chunk-2P4XKWV2.js";
737
738
  import {
738
739
  BRIGHTSY_OAUTH_CLIENT_ID,
@@ -758,6 +759,7 @@ import {
758
759
  CLAUDE_CHROME_ALLOWED_TOOLS,
759
760
  CLAUDE_POSTHOG_CONNECTOR_ALLOWED_TOOLS,
760
761
  CLAUDE_PRINT_BG_WAIT_CEILING_MS,
762
+ isClaudeAiConnectorMcp,
761
763
  mcpAllowTools,
762
764
  mcpAllowToolsFromNames,
763
765
  mcpAuthWarnings,
@@ -765,7 +767,7 @@ import {
765
767
  mcpServerPermissionUpdate,
766
768
  parseMcpList,
767
769
  sanitizeMcpServerName
768
- } from "./chunk-FO3NKZVJ.js";
770
+ } from "./chunk-F2PVRF3B.js";
769
771
  import {
770
772
  clipMcpToolResult,
771
773
  cursorToolErrorText
@@ -5413,7 +5415,7 @@ async function handleJobStop(params, _ctx) {
5413
5415
  return stopDetachedJob(jobCwd(rec), id, reason ? { reason } : void 0);
5414
5416
  }
5415
5417
  async function defaultTurnWait(ref, timeoutMs) {
5416
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-JEF7JC3S.js");
5418
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-CZPRDLHV.js");
5417
5419
  const orch = getOrchestrator2();
5418
5420
  const thread = await orch.waitForTurn(ref, timeoutMs, {
5419
5421
  resolveIfStillRunning: true
@@ -7505,6 +7507,359 @@ async function runCloudConnect(opts) {
7505
7507
  }
7506
7508
  }
7507
7509
 
7510
+ // src/mcp-ui/meta.ts
7511
+ var HTML_MAX = 15e5;
7512
+ function matchClaudeMcpTool(toolName, serverNames) {
7513
+ const name = toolName.trim();
7514
+ if (!name.startsWith("mcp__")) return null;
7515
+ const ranked = [...serverNames].filter((s) => s.trim()).sort((a, b) => b.length - a.length);
7516
+ for (const server of ranked) {
7517
+ const prefix = `mcp__${server}__`;
7518
+ if (!name.startsWith(prefix)) continue;
7519
+ const tool = name.slice(prefix.length).trim();
7520
+ if (!tool) continue;
7521
+ return { server, tool };
7522
+ }
7523
+ return null;
7524
+ }
7525
+ function parseMcpServerLaunch(entry) {
7526
+ const command = typeof entry.command === "string" ? entry.command.trim() : "";
7527
+ if (command) {
7528
+ const args = Array.isArray(entry.args) ? entry.args.filter((a) => typeof a === "string") : [];
7529
+ const env = stringRecord(entry.env);
7530
+ const cwd = typeof entry.cwd === "string" && entry.cwd.trim() ? entry.cwd.trim() : void 0;
7531
+ return {
7532
+ kind: "stdio",
7533
+ command,
7534
+ args,
7535
+ ...env ? { env } : {},
7536
+ ...cwd ? { cwd } : {}
7537
+ };
7538
+ }
7539
+ const url = typeof entry.url === "string" ? entry.url.trim() : "";
7540
+ if (!url) return null;
7541
+ let parsed;
7542
+ try {
7543
+ parsed = new URL(url);
7544
+ } catch {
7545
+ return null;
7546
+ }
7547
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
7548
+ const type = typeof entry.type === "string" ? entry.type.trim().toLowerCase() : "";
7549
+ const headers = stringRecord(entry.headers);
7550
+ return {
7551
+ kind: type === "sse" ? "sse" : "http",
7552
+ url: parsed.toString(),
7553
+ ...headers ? { headers } : {}
7554
+ };
7555
+ }
7556
+ function uiResourceUriFromMeta(meta) {
7557
+ const rec = asRecord3(meta);
7558
+ const nested = asRecord3(rec.ui);
7559
+ const fromNested = typeof nested.resourceUri === "string" ? nested.resourceUri.trim() : "";
7560
+ const legacy = typeof rec["ui/resourceUri"] === "string" ? rec["ui/resourceUri"].trim() : "";
7561
+ const uri = fromNested || legacy;
7562
+ return uri.startsWith("ui://") ? uri : null;
7563
+ }
7564
+ function cspFromMeta(meta) {
7565
+ const ui = asRecord3(asRecord3(meta).ui);
7566
+ const csp = asRecord3(ui.csp);
7567
+ const connectDomains = originList(csp.connectDomains);
7568
+ const resourceDomains = originList(csp.resourceDomains);
7569
+ const frameDomains = originList(csp.frameDomains);
7570
+ const baseUriDomains = originList(csp.baseUriDomains);
7571
+ if (!connectDomains?.length && !resourceDomains?.length && !frameDomains?.length && !baseUriDomains?.length) {
7572
+ return void 0;
7573
+ }
7574
+ return {
7575
+ ...connectDomains?.length ? { connectDomains } : {},
7576
+ ...resourceDomains?.length ? { resourceDomains } : {},
7577
+ ...frameDomains?.length ? { frameDomains } : {},
7578
+ ...baseUriDomains?.length ? { baseUriDomains } : {}
7579
+ };
7580
+ }
7581
+ function htmlFromResourceContent(content) {
7582
+ const uri = content.uri?.trim() ?? "";
7583
+ if (!uri.startsWith("ui://")) return null;
7584
+ const mimeType = content.mimeType?.trim() || "text/html";
7585
+ if (!isHtmlMime(mimeType)) return null;
7586
+ const html = htmlText(content.text, content.blob);
7587
+ if (!html) return null;
7588
+ return { uri, mimeType, html };
7589
+ }
7590
+ function mcpUiContentSecurityPolicy(csp) {
7591
+ const resource = (csp?.resourceDomains ?? []).join(" ");
7592
+ const connect = (csp?.connectDomains ?? []).join(" ");
7593
+ const frames = csp?.frameDomains?.length ? csp.frameDomains.join(" ") : "'none'";
7594
+ const base = csp?.baseUriDomains?.length ? csp.baseUriDomains.join(" ") : "'none'";
7595
+ return [
7596
+ "default-src 'none'",
7597
+ `script-src 'self' 'unsafe-inline' 'unsafe-eval' ${resource}`.trim(),
7598
+ `style-src 'self' 'unsafe-inline' ${resource}`.trim(),
7599
+ `img-src 'self' data: blob: ${resource}`.trim(),
7600
+ `font-src 'self' data: ${resource}`.trim(),
7601
+ `connect-src 'self' ${connect}`.trim(),
7602
+ `media-src 'self' data: blob: ${resource}`.trim(),
7603
+ `frame-src ${frames}`,
7604
+ `base-uri ${base}`,
7605
+ "object-src 'none'",
7606
+ "form-action 'self'"
7607
+ ].join("; ");
7608
+ }
7609
+ function htmlText(text5, blob) {
7610
+ if (typeof text5 === "string" && text5.trim()) {
7611
+ return text5.length <= HTML_MAX ? text5 : null;
7612
+ }
7613
+ if (typeof blob !== "string" || !blob.trim()) return null;
7614
+ try {
7615
+ const decoded = decodeBase64(blob.trim());
7616
+ if (!decoded.trim() || decoded.length > HTML_MAX) return null;
7617
+ return decoded;
7618
+ } catch {
7619
+ return null;
7620
+ }
7621
+ }
7622
+ function decodeBase64(blob) {
7623
+ if (typeof Buffer !== "undefined") return Buffer.from(blob, "base64").toString("utf8");
7624
+ const bin = atob(blob);
7625
+ const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0));
7626
+ return new TextDecoder().decode(bytes);
7627
+ }
7628
+ function isHtmlMime(mime) {
7629
+ const base = mime.split(";")[0]?.trim().toLowerCase() ?? "";
7630
+ return base === "text/html";
7631
+ }
7632
+ function originList(value) {
7633
+ if (!Array.isArray(value)) return void 0;
7634
+ const out = [];
7635
+ for (const item of value) {
7636
+ if (typeof item !== "string") continue;
7637
+ const origin = originToken(item);
7638
+ if (origin && !out.includes(origin)) out.push(origin);
7639
+ }
7640
+ return out;
7641
+ }
7642
+ function originToken(value) {
7643
+ try {
7644
+ const url = new URL(value.trim());
7645
+ if (url.protocol !== "https:" && url.protocol !== "http:" && url.protocol !== "wss:" && url.protocol !== "ws:") {
7646
+ return null;
7647
+ }
7648
+ return url.origin;
7649
+ } catch {
7650
+ return null;
7651
+ }
7652
+ }
7653
+ function stringRecord(value) {
7654
+ const rec = asRecord3(value);
7655
+ const out = {};
7656
+ for (const [key, item] of Object.entries(rec)) {
7657
+ if (typeof item === "string") out[key] = item;
7658
+ }
7659
+ return Object.keys(out).length > 0 ? out : void 0;
7660
+ }
7661
+ function asRecord3(value) {
7662
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
7663
+ return value;
7664
+ }
7665
+
7666
+ // src/mcp-ui/session.ts
7667
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7668
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
7669
+ import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
7670
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
7671
+ var REQUEST_TIMEOUT_MS = 2e4;
7672
+ var UI_CAPABILITIES = {
7673
+ extensions: {
7674
+ "io.modelcontextprotocol/ui": {
7675
+ mimeTypes: ["text/html;profile=mcp-app"]
7676
+ }
7677
+ }
7678
+ };
7679
+ async function openMcpUiSession(launch) {
7680
+ return openMcpUiSessionFromTransport(transportForLaunch(launch));
7681
+ }
7682
+ async function openMcpUiSessionFromTransport(transport) {
7683
+ const client = new Client(
7684
+ { name: "sideboard", version: "1.0.0" },
7685
+ { capabilities: UI_CAPABILITIES }
7686
+ );
7687
+ await client.connect(transport, { timeout: REQUEST_TIMEOUT_MS });
7688
+ return {
7689
+ async listTools() {
7690
+ const listed = await client.listTools(void 0, { timeout: REQUEST_TIMEOUT_MS });
7691
+ return listed.tools.map((tool) => {
7692
+ const meta = tool._meta;
7693
+ return {
7694
+ name: tool.name,
7695
+ ...tool.description ? { description: tool.description } : {},
7696
+ ...tool.inputSchema ? { inputSchema: tool.inputSchema } : {},
7697
+ resourceUri: uiResourceUriFromMeta(meta),
7698
+ ...cspFromMeta(meta) ? { csp: cspFromMeta(meta) } : {}
7699
+ };
7700
+ });
7701
+ },
7702
+ async readResource(uri) {
7703
+ const result = await client.readResource({ uri }, { timeout: REQUEST_TIMEOUT_MS });
7704
+ const content = result.contents?.[0];
7705
+ if (!content) return null;
7706
+ const text5 = "text" in content && typeof content.text === "string" ? content.text : void 0;
7707
+ const blob = "blob" in content && typeof content.blob === "string" ? content.blob : void 0;
7708
+ return {
7709
+ uri: content.uri,
7710
+ ...content.mimeType ? { mimeType: content.mimeType } : {},
7711
+ ...text5 != null ? { text: text5 } : {},
7712
+ ...blob != null ? { blob } : {},
7713
+ ...cspFromMeta(content._meta) ? { csp: cspFromMeta(content._meta) } : {}
7714
+ };
7715
+ },
7716
+ async callTool(name, args) {
7717
+ return client.callTool({ name, arguments: args }, void 0, { timeout: REQUEST_TIMEOUT_MS });
7718
+ },
7719
+ async close() {
7720
+ await client.close();
7721
+ }
7722
+ };
7723
+ }
7724
+ function transportForLaunch(launch) {
7725
+ if (launch.kind === "stdio") {
7726
+ return new StdioClientTransport({
7727
+ command: launch.command,
7728
+ args: launch.args,
7729
+ env: { ...getDefaultEnvironment(), ...launch.env },
7730
+ stderr: "ignore",
7731
+ ...launch.cwd ? { cwd: launch.cwd } : {}
7732
+ });
7733
+ }
7734
+ const url = new URL(launch.url);
7735
+ const requestInit = launch.headers ? { headers: launch.headers } : void 0;
7736
+ if (launch.kind === "sse") {
7737
+ return new SSEClientTransport(url, requestInit ? { requestInit } : void 0);
7738
+ }
7739
+ return new StreamableHTTPClientTransport(url, requestInit ? { requestInit } : void 0);
7740
+ }
7741
+
7742
+ // src/mcp-ui/host.ts
7743
+ var TOOL_TTL_MS = 5 * 60 * 1e3;
7744
+ var McpUiHost = class {
7745
+ constructor(opts) {
7746
+ this.opts = opts;
7747
+ }
7748
+ opts;
7749
+ sessions = /* @__PURE__ */ new Map();
7750
+ toolLists = /* @__PURE__ */ new Map();
7751
+ async viewForClaudeTool(toolName) {
7752
+ const entries = this.userEntries();
7753
+ const match = matchClaudeMcpTool(toolName, Object.keys(entries));
7754
+ if (!match) return null;
7755
+ const launch = parseMcpServerLaunch(entries[match.server] ?? {});
7756
+ if (!launch) return null;
7757
+ try {
7758
+ const session = await this.sessionFor(match.server, launch);
7759
+ const tools = await this.toolsFor(match.server, launch, session);
7760
+ const tool = tools.find((item) => item.name === match.tool);
7761
+ if (!tool?.resourceUri) return null;
7762
+ const resource = await session.readResource(tool.resourceUri);
7763
+ if (!resource) return null;
7764
+ const html = htmlFromResourceContent(resource);
7765
+ if (!html) return null;
7766
+ const csp = resource.csp ?? tool.csp;
7767
+ return {
7768
+ server: match.server,
7769
+ tool: match.tool,
7770
+ uri: html.uri,
7771
+ mimeType: html.mimeType,
7772
+ html: html.html,
7773
+ csp: mcpUiContentSecurityPolicy(csp)
7774
+ };
7775
+ } catch (err) {
7776
+ console.error(
7777
+ `[mcp-ui] ${match.server}/${match.tool}:`,
7778
+ err instanceof Error ? err.message : err
7779
+ );
7780
+ return null;
7781
+ }
7782
+ }
7783
+ async callTool(server, tool, args) {
7784
+ const session = await this.sessionForName(server);
7785
+ if (!session) throw new Error(`MCP server ${server} is not available`);
7786
+ return session.callTool(tool, args);
7787
+ }
7788
+ async readResource(server, uri) {
7789
+ const session = await this.sessionForName(server);
7790
+ if (!session) throw new Error(`MCP server ${server} is not available`);
7791
+ const resource = await session.readResource(uri);
7792
+ if (!resource) throw new Error(`Resource ${uri} was empty`);
7793
+ return {
7794
+ contents: [
7795
+ {
7796
+ uri: resource.uri,
7797
+ ...resource.mimeType ? { mimeType: resource.mimeType } : {},
7798
+ ...resource.text != null ? { text: resource.text } : {},
7799
+ ...resource.blob != null ? { blob: resource.blob } : {}
7800
+ }
7801
+ ]
7802
+ };
7803
+ }
7804
+ async closeAll() {
7805
+ const pending = [...this.sessions.values()];
7806
+ this.sessions.clear();
7807
+ this.toolLists.clear();
7808
+ await Promise.all(
7809
+ pending.map((session) => session.then((open) => open.close()).catch(() => void 0))
7810
+ );
7811
+ }
7812
+ userEntries() {
7813
+ const out = {};
7814
+ for (const [name, entry] of Object.entries(this.opts.entries())) {
7815
+ if (!name.trim() || isInjectedOrchMcpName(name) || !entry) continue;
7816
+ out[name] = entry;
7817
+ }
7818
+ return out;
7819
+ }
7820
+ async sessionForName(server) {
7821
+ const entry = this.userEntries()[server];
7822
+ if (!entry) return null;
7823
+ const launch = parseMcpServerLaunch(entry);
7824
+ if (!launch) return null;
7825
+ return this.sessionFor(server, launch);
7826
+ }
7827
+ sessionFor(server, launch) {
7828
+ const key = `${server}
7829
+ ${JSON.stringify(launch)}`;
7830
+ const existing = this.sessions.get(key);
7831
+ if (existing) return existing;
7832
+ const open = this.opts.openSession ?? openMcpUiSession;
7833
+ const pending = open(launch).catch((err) => {
7834
+ this.sessions.delete(key);
7835
+ throw err;
7836
+ });
7837
+ this.sessions.set(key, pending);
7838
+ return pending;
7839
+ }
7840
+ async toolsFor(server, launch, session) {
7841
+ const key = `${server}
7842
+ ${JSON.stringify(launch)}`;
7843
+ const cached2 = this.toolLists.get(key);
7844
+ if (cached2 && cached2.key === key && Date.now() - cached2.at < TOOL_TTL_MS) return cached2.tools;
7845
+ const tools = await session.listTools();
7846
+ this.toolLists.set(key, { at: Date.now(), key, tools });
7847
+ return tools;
7848
+ }
7849
+ };
7850
+ var singleton;
7851
+ function getMcpUiHost() {
7852
+ if (!singleton) {
7853
+ singleton = new McpUiHost({ entries: () => listUserClaudeMcpServerEntries() });
7854
+ }
7855
+ return singleton;
7856
+ }
7857
+ function closeMcpUiHost() {
7858
+ const host = singleton;
7859
+ singleton = void 0;
7860
+ if (host) void host.closeAll();
7861
+ }
7862
+
7508
7863
  // src/slack/workspaces.ts
7509
7864
  import { join as join4 } from "path";
7510
7865
 
@@ -9572,126 +9927,8 @@ var RemoteHub = class {
9572
9927
  // src/remote/client.ts
9573
9928
  import { WebSocket as WsWebSocket3 } from "ws";
9574
9929
 
9575
- // src/remote/phone-media.ts
9576
- import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
9577
- import { join as join6 } from "path";
9578
- var PHONE_IMAGE_MAX_BYTES = 15e5;
9579
- var PHONE_TEXT_MAX_BYTES = 18e4;
9580
- var PHONE_DIR_MAX = 200;
9581
- var IMAGE_MIME = {
9582
- png: "image/png",
9583
- jpg: "image/jpeg",
9584
- jpeg: "image/jpeg",
9585
- gif: "image/gif",
9586
- webp: "image/webp",
9587
- bmp: "image/bmp"
9588
- };
9589
- function phoneWorktreeRelative(worktreePath, raw) {
9590
- let text5 = raw.trim();
9591
- if (!text5) return null;
9592
- if (/^file:\/\//i.test(text5)) {
9593
- try {
9594
- text5 = decodeURIComponent(new URL(text5).pathname);
9595
- } catch {
9596
- return null;
9597
- }
9598
- }
9599
- text5 = (text5.split("#")[0] ?? text5).replace(/\\/g, "/");
9600
- if (!text5 || text5.includes("\0") || text5.split("/").includes("..")) return null;
9601
- const root = worktreePath.replace(/\/+$/, "");
9602
- if (!root || root === "/") return null;
9603
- if (text5.startsWith("/")) {
9604
- if (text5 !== root && !text5.startsWith(`${root}/`)) return null;
9605
- text5 = text5 === root ? "" : text5.slice(root.length + 1);
9606
- }
9607
- text5 = text5.replace(/^\.\//, "").replace(/\/+$/, "");
9608
- if (!text5 || text5 === "." || text5.startsWith("/")) return null;
9609
- return text5;
9610
- }
9611
- function phoneImageMime(relativePath) {
9612
- const ext = relativePath.split(".").pop()?.toLowerCase() ?? "";
9613
- return IMAGE_MIME[ext] ?? null;
9614
- }
9615
- function phoneImageDataUrl(worktreePath, raw) {
9616
- const relative = phoneWorktreeRelative(worktreePath, raw);
9617
- if (!relative) return null;
9618
- const mime = phoneImageMime(relative);
9619
- if (!mime) return null;
9620
- try {
9621
- const file = readWorktreeFileForUpload(worktreePath, relative, { maxBytes: PHONE_IMAGE_MAX_BYTES });
9622
- return `data:${mime};base64,${file.contentBase64}`;
9623
- } catch {
9624
- return null;
9625
- }
9626
- }
9627
- function phoneDirectoryListing(worktreePath, relative) {
9628
- try {
9629
- const entries = readdirSync2(join6(worktreePath, relative), { withFileTypes: true });
9630
- const dirs = new Set(entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name));
9631
- const names = entries.map((entry) => entry.name).filter((name) => name && !name.includes("/") && !name.includes("\0")).sort((a, b) => a.localeCompare(b));
9632
- const shown = names.slice(0, PHONE_DIR_MAX);
9633
- const lines = shown.map((name) => dirs.has(name) ? `${name}/` : name);
9634
- const truncated = names.length > shown.length;
9635
- if (truncated) lines.push(`\u2026 ${names.length - shown.length} more`);
9636
- return { content: lines.join("\n"), truncated };
9637
- } catch {
9638
- return null;
9639
- }
9640
- }
9641
- function phoneFilePreview(worktreePath, relative, kind) {
9642
- if (kind === "dir") {
9643
- const listing = phoneDirectoryListing(worktreePath, relative);
9644
- if (!listing) return null;
9645
- return {
9646
- content: listing.content,
9647
- encoding: "utf8",
9648
- binary: false,
9649
- directory: true,
9650
- truncated: listing.truncated
9651
- };
9652
- }
9653
- if (relative.toLowerCase().endsWith(".svg")) {
9654
- try {
9655
- const buf = readFileSync5(join6(worktreePath, relative));
9656
- const truncated = buf.length > PHONE_TEXT_MAX_BYTES;
9657
- return {
9658
- content: buf.subarray(0, PHONE_TEXT_MAX_BYTES).toString("utf8"),
9659
- encoding: "utf8",
9660
- binary: false,
9661
- directory: false,
9662
- truncated,
9663
- mime: "image/svg+xml"
9664
- };
9665
- } catch {
9666
- return null;
9667
- }
9668
- }
9669
- const mime = phoneImageMime(relative);
9670
- if (mime) {
9671
- const dataUrl = phoneImageDataUrl(worktreePath, relative);
9672
- if (!dataUrl) return null;
9673
- return {
9674
- content: dataUrl,
9675
- encoding: "base64",
9676
- binary: true,
9677
- directory: false,
9678
- truncated: false,
9679
- mime
9680
- };
9681
- }
9682
- try {
9683
- const file = readWorktreeFile(worktreePath, relative, { maxBytes: PHONE_TEXT_MAX_BYTES });
9684
- return {
9685
- content: file.content,
9686
- encoding: file.encoding,
9687
- binary: file.binary,
9688
- directory: false,
9689
- truncated: file.truncated
9690
- };
9691
- } catch {
9692
- return null;
9693
- }
9694
- }
9930
+ // src/remote/phone-chats.ts
9931
+ import { randomUUID as randomUUID7 } from "crypto";
9695
9932
 
9696
9933
  // src/remote/phone-artifact.ts
9697
9934
  var FENCE_RE = /```([a-zA-Z0-9_+-]*)[^\n]*\n([\s\S]*?)(?:```|$)/g;
@@ -9701,10 +9938,10 @@ var MIN_MD_CHARS = 120;
9701
9938
  function oneLine(value, max) {
9702
9939
  return value.replace(/[`\r\n]/g, " ").replace(/\s+/g, " ").trim().slice(0, max);
9703
9940
  }
9704
- function asRecord3(value) {
9941
+ function asRecord4(value) {
9705
9942
  if (typeof value === "string") {
9706
9943
  try {
9707
- return asRecord3(JSON.parse(value));
9944
+ return asRecord4(JSON.parse(value));
9708
9945
  } catch {
9709
9946
  return void 0;
9710
9947
  }
@@ -9779,7 +10016,7 @@ function toolContent(input) {
9779
10016
  if (!input) return "";
9780
10017
  const direct = input.content;
9781
10018
  if (typeof direct === "string") return direct;
9782
- const nested = asRecord3(direct);
10019
+ const nested = asRecord4(direct);
9783
10020
  if (!nested) return "";
9784
10021
  for (const key of ["html", "markup", "text", "source", "content", "code"]) {
9785
10022
  if (typeof nested[key] === "string") return nested[key];
@@ -9802,7 +10039,7 @@ function phoneArtifactText(text5, parts, limit = Number.POSITIVE_INFINITY) {
9802
10039
  const extras = [];
9803
10040
  for (const part of parts ?? []) {
9804
10041
  if (part.type !== "tool" || part.parentId || !isArtifactTool(part.name)) continue;
9805
- const input = asRecord3(part.input);
10042
+ const input = asRecord4(part.input);
9806
10043
  const content = toolContent(input);
9807
10044
  const hint = artifactHint(content);
9808
10045
  if (hint && hints.has(hint)) continue;
@@ -9821,7 +10058,7 @@ function phoneArtifactText(text5, parts, limit = Number.POSITIVE_INFINITY) {
9821
10058
  ${tail}` : tail;
9822
10059
  }
9823
10060
  function parseOpenArtifact(value, chatId) {
9824
- const record = asRecord3(value);
10061
+ const record = asRecord4(value);
9825
10062
  if (!record) return "invalid";
9826
10063
  const title = oneLine(str4(record.title), 200);
9827
10064
  if (!title) return "invalid";
@@ -9844,7 +10081,7 @@ function phoneArtifactDocuments(sources) {
9844
10081
  }
9845
10082
  for (const part of source.parts ?? []) {
9846
10083
  if (part.type !== "tool" || part.parentId || !isArtifactTool(part.name)) continue;
9847
- const input = asRecord3(part.input);
10084
+ const input = asRecord4(part.input);
9848
10085
  const content = toolContent(input);
9849
10086
  const hint = artifactHint(content);
9850
10087
  if (hint && hints.has(hint)) continue;
@@ -9894,121 +10131,6 @@ function pickArtifactMatch(items, request) {
9894
10131
  return null;
9895
10132
  }
9896
10133
 
9897
- // src/remote/phone-panes.ts
9898
- var PHONE_CHANGE_SCOPES = ["last_turn", "uncommitted", "staged", "unstaged", "commits"];
9899
- var PHONE_CHANGE_SCOPE_DEFAULT = "commits";
9900
- function phoneChangeScope(value) {
9901
- return typeof value === "string" && PHONE_CHANGE_SCOPES.includes(value) ? value : void 0;
9902
- }
9903
- function phoneCommitSha(value) {
9904
- return typeof value === "string" && /^[0-9a-f]{4,40}$/i.test(value) ? value : void 0;
9905
- }
9906
- var MAX_ROWS = 200;
9907
- var MAX_QUERY = 200;
9908
- function parsePhonePane(value, chatId) {
9909
- const pane = value && typeof value === "object" ? value.pane : void 0;
9910
- if (pane !== "files" && pane !== "changes" && pane !== "checks") return "invalid";
9911
- const scope = phoneChangeScope(value.scope);
9912
- const commitSha = phoneCommitSha(value.commitSha);
9913
- const queryRaw = value.query;
9914
- const query = typeof queryRaw === "string" ? queryRaw.trim().slice(0, MAX_QUERY) : "";
9915
- return {
9916
- op: "pane",
9917
- chatId,
9918
- pane,
9919
- ...scope && pane === "changes" ? { scope } : {},
9920
- ...commitSha && pane === "changes" && scope === "commits" ? { commitSha } : {},
9921
- ...query && pane === "files" ? { query } : {}
9922
- };
9923
- }
9924
- function filterPhoneFiles(paths, query) {
9925
- const q = query.trim().toLowerCase();
9926
- if (!q) return paths;
9927
- return paths.filter((path2) => path2.toLowerCase().includes(q));
9928
- }
9929
- async function readPhonePane(thread, pane, scope, query, commitSha) {
9930
- try {
9931
- if (pane === "files") return await readFiles(thread.id, query);
9932
- if (pane === "changes") return await readChanges(thread.id, scope ?? PHONE_CHANGE_SCOPE_DEFAULT, commitSha);
9933
- return await readChecks(thread.id);
9934
- } catch (err) {
9935
- const message = err instanceof Error ? err.message : String(err);
9936
- return { rows: [], note: message };
9937
- }
9938
- }
9939
- async function readFiles(threadId, query) {
9940
- const paths = filterPhoneFiles(await getOrchestrator().listFiles(threadId), query ?? "");
9941
- const shown = paths.slice(0, MAX_ROWS);
9942
- const extra = paths.length - shown.length;
9943
- const note = extra > 0 ? query?.trim() ? `${extra} more matching files` : `${extra} more files on the Mac` : void 0;
9944
- return {
9945
- rows: shown.map((path2) => ({ label: path2, path: path2 })),
9946
- ...note ? { note } : {}
9947
- };
9948
- }
9949
- async function readChanges(threadId, scope, commitSha) {
9950
- const diff = await getOrchestrator().diff(threadId, {
9951
- includePatches: false,
9952
- scope,
9953
- commitSha: scope === "commits" ? commitSha ?? null : null
9954
- });
9955
- const shown = diff.files.slice(0, MAX_ROWS);
9956
- const selected = diff.commits.find((commit) => commit.sha === commitSha || commit.shortSha === commitSha);
9957
- return {
9958
- rows: shown.map((file) => {
9959
- const detail = changeDetail(file.additions, file.deletions);
9960
- return {
9961
- label: file.path,
9962
- path: file.path,
9963
- ...file.status ? { status: file.status } : {},
9964
- ...detail ? { detail } : {}
9965
- };
9966
- }),
9967
- ...diff.files.length > shown.length ? { note: `${diff.files.length - shown.length} more changed files` } : {},
9968
- scope: diff.scope,
9969
- commitSha: selected?.sha ?? null,
9970
- ...diff.base ? { base: diff.base } : {},
9971
- hasLastTurnBase: diff.hasLastTurnBase,
9972
- commits: diff.commits.slice(0, 40),
9973
- scopeStats: diff.scopeStats
9974
- };
9975
- }
9976
- function changeDetail(additions, deletions) {
9977
- const add = additions ?? 0;
9978
- const del = deletions ?? 0;
9979
- if (add === 0 && del === 0) return void 0;
9980
- return `+${add} \u2212${del}`;
9981
- }
9982
- async function readPhoneChangePatch(threadId, path2, scope = PHONE_CHANGE_SCOPE_DEFAULT, commitSha) {
9983
- const diff = await getOrchestrator().diff(threadId, {
9984
- path: path2,
9985
- scope,
9986
- commitSha: scope === "commits" ? commitSha ?? null : null,
9987
- includePatches: true,
9988
- includeMeta: false,
9989
- includeUntracked: scope === "uncommitted" || scope === "unstaged" || scope === "last_turn" || scope === "commits" && !commitSha
9990
- });
9991
- const file = diff.files.find((row2) => row2.path === path2) ?? diff.files[0];
9992
- if (!file) return null;
9993
- const content = file.patch ?? "";
9994
- return { content, truncated: content.includes("\u2026 truncated") };
9995
- }
9996
- async function readChecks(threadId) {
9997
- const checks = await getOrchestrator().getPrChecks(threadId);
9998
- if (!checks) return { rows: [], note: "No pull request on this branch." };
9999
- const shown = checks.slice(0, MAX_ROWS);
10000
- return {
10001
- rows: shown.map((check) => ({
10002
- label: check.name,
10003
- detail: check.bucket || check.state,
10004
- ...check.link ? { href: check.link } : {}
10005
- }))
10006
- };
10007
- }
10008
-
10009
- // src/remote/phone-chats.ts
10010
- import { randomUUID as randomUUID7 } from "crypto";
10011
-
10012
10134
  // src/remote/phone-header.ts
10013
10135
  var PHONE_HEADER_GIT_ACTIONS = [
10014
10136
  "commit-push",
@@ -10170,6 +10292,118 @@ function parsePhoneCodeRefs(value) {
10170
10292
  return refs;
10171
10293
  }
10172
10294
 
10295
+ // src/remote/phone-panes.ts
10296
+ var PHONE_CHANGE_SCOPES = ["last_turn", "uncommitted", "staged", "unstaged", "commits"];
10297
+ var PHONE_CHANGE_SCOPE_DEFAULT = "commits";
10298
+ function phoneChangeScope(value) {
10299
+ return typeof value === "string" && PHONE_CHANGE_SCOPES.includes(value) ? value : void 0;
10300
+ }
10301
+ function phoneCommitSha(value) {
10302
+ return typeof value === "string" && /^[0-9a-f]{4,40}$/i.test(value) ? value : void 0;
10303
+ }
10304
+ var MAX_ROWS = 200;
10305
+ var MAX_QUERY = 200;
10306
+ function parsePhonePane(value, chatId) {
10307
+ const pane = value && typeof value === "object" ? value.pane : void 0;
10308
+ if (pane !== "files" && pane !== "changes" && pane !== "checks") return "invalid";
10309
+ const scope = phoneChangeScope(value.scope);
10310
+ const commitSha = phoneCommitSha(value.commitSha);
10311
+ const queryRaw = value.query;
10312
+ const query = typeof queryRaw === "string" ? queryRaw.trim().slice(0, MAX_QUERY) : "";
10313
+ return {
10314
+ op: "pane",
10315
+ chatId,
10316
+ pane,
10317
+ ...scope && pane === "changes" ? { scope } : {},
10318
+ ...commitSha && pane === "changes" && scope === "commits" ? { commitSha } : {},
10319
+ ...query && pane === "files" ? { query } : {}
10320
+ };
10321
+ }
10322
+ function filterPhoneFiles(paths, query) {
10323
+ const q = query.trim().toLowerCase();
10324
+ if (!q) return paths;
10325
+ return paths.filter((path2) => path2.toLowerCase().includes(q));
10326
+ }
10327
+ async function readPhonePane(thread, pane, scope, query, commitSha) {
10328
+ try {
10329
+ if (pane === "files") return await readFiles(thread.id, query);
10330
+ if (pane === "changes") return await readChanges(thread.id, scope ?? PHONE_CHANGE_SCOPE_DEFAULT, commitSha);
10331
+ return await readChecks(thread.id);
10332
+ } catch (err) {
10333
+ const message = err instanceof Error ? err.message : String(err);
10334
+ return { rows: [], note: message };
10335
+ }
10336
+ }
10337
+ async function readFiles(threadId, query) {
10338
+ const paths = filterPhoneFiles(await getOrchestrator().listFiles(threadId), query ?? "");
10339
+ const shown = paths.slice(0, MAX_ROWS);
10340
+ const extra = paths.length - shown.length;
10341
+ const note = extra > 0 ? query?.trim() ? `${extra} more matching files` : `${extra} more files on the Mac` : void 0;
10342
+ return {
10343
+ rows: shown.map((path2) => ({ label: path2, path: path2 })),
10344
+ ...note ? { note } : {}
10345
+ };
10346
+ }
10347
+ async function readChanges(threadId, scope, commitSha) {
10348
+ const diff = await getOrchestrator().diff(threadId, {
10349
+ includePatches: false,
10350
+ scope,
10351
+ commitSha: scope === "commits" ? commitSha ?? null : null
10352
+ });
10353
+ const shown = diff.files.slice(0, MAX_ROWS);
10354
+ const selected = diff.commits.find((commit) => commit.sha === commitSha || commit.shortSha === commitSha);
10355
+ return {
10356
+ rows: shown.map((file) => {
10357
+ const detail = changeDetail(file.additions, file.deletions);
10358
+ return {
10359
+ label: file.path,
10360
+ path: file.path,
10361
+ ...file.status ? { status: file.status } : {},
10362
+ ...detail ? { detail } : {}
10363
+ };
10364
+ }),
10365
+ ...diff.files.length > shown.length ? { note: `${diff.files.length - shown.length} more changed files` } : {},
10366
+ scope: diff.scope,
10367
+ commitSha: selected?.sha ?? null,
10368
+ ...diff.base ? { base: diff.base } : {},
10369
+ hasLastTurnBase: diff.hasLastTurnBase,
10370
+ commits: diff.commits.slice(0, 40),
10371
+ scopeStats: diff.scopeStats
10372
+ };
10373
+ }
10374
+ function changeDetail(additions, deletions) {
10375
+ const add = additions ?? 0;
10376
+ const del = deletions ?? 0;
10377
+ if (add === 0 && del === 0) return void 0;
10378
+ return `+${add} \u2212${del}`;
10379
+ }
10380
+ async function readPhoneChangePatch(threadId, path2, scope = PHONE_CHANGE_SCOPE_DEFAULT, commitSha) {
10381
+ const diff = await getOrchestrator().diff(threadId, {
10382
+ path: path2,
10383
+ scope,
10384
+ commitSha: scope === "commits" ? commitSha ?? null : null,
10385
+ includePatches: true,
10386
+ includeMeta: false,
10387
+ includeUntracked: scope === "uncommitted" || scope === "unstaged" || scope === "last_turn" || scope === "commits" && !commitSha
10388
+ });
10389
+ const file = diff.files.find((row2) => row2.path === path2) ?? diff.files[0];
10390
+ if (!file) return null;
10391
+ const content = file.patch ?? "";
10392
+ return { content, truncated: content.includes("\u2026 truncated") };
10393
+ }
10394
+ async function readChecks(threadId) {
10395
+ const checks = await getOrchestrator().getPrChecks(threadId);
10396
+ if (!checks) return { rows: [], note: "No pull request on this branch." };
10397
+ const shown = checks.slice(0, MAX_ROWS);
10398
+ return {
10399
+ rows: shown.map((check) => ({
10400
+ label: check.name,
10401
+ detail: check.bucket || check.state,
10402
+ ...check.link ? { href: check.link } : {}
10403
+ }))
10404
+ };
10405
+ }
10406
+
10173
10407
  // src/remote/phone-defaults.ts
10174
10408
  function row(resolved) {
10175
10409
  return {
@@ -10209,6 +10443,63 @@ function refreshPhoneViewerLogin(force = false) {
10209
10443
  return run2;
10210
10444
  }
10211
10445
 
10446
+ // src/remote/phone-mcp-ui.ts
10447
+ var MAX_APPS = 4;
10448
+ function phoneMcpApps(agent, parts) {
10449
+ if (agent !== "claude" || !parts?.length) return [];
10450
+ const out = [];
10451
+ for (const part of parts) {
10452
+ if (part.type !== "tool" || part.parentId || part.status === "running") continue;
10453
+ if (!part.name.startsWith("mcp__") || part.name.startsWith("mcp__sideboard")) continue;
10454
+ out.push({ id: part.id, tool: part.name });
10455
+ if (out.length >= MAX_APPS) break;
10456
+ }
10457
+ return out;
10458
+ }
10459
+ function controlText(value, key) {
10460
+ const raw = value[key];
10461
+ return typeof raw === "string" ? raw.trim() : "";
10462
+ }
10463
+ function parsePhoneMcpUi(value, op, chatId) {
10464
+ if (op === "mcp-ui") {
10465
+ const tool2 = controlText(value, "tool");
10466
+ const callId = controlText(value, "callId");
10467
+ const reqId2 = controlText(value, "reqId");
10468
+ if (!tool2.startsWith("mcp__") || tool2.length > 200 || !callId || !reqId2 || reqId2.length > 40) {
10469
+ return "invalid";
10470
+ }
10471
+ return { op: "mcp-ui", chatId, tool: tool2, callId, reqId: reqId2 };
10472
+ }
10473
+ if (op !== "mcp-ui-call" && op !== "mcp-ui-read") return "invalid";
10474
+ const reqId = controlText(value, "reqId");
10475
+ const server = controlText(value, "server");
10476
+ if (!reqId || reqId.length > 40 || !server || server.length > 120) return "invalid";
10477
+ if (op === "mcp-ui-read") {
10478
+ const uri = controlText(value, "uri");
10479
+ if (!uri.startsWith("ui://") || uri.length > 500) return "invalid";
10480
+ return { op, chatId, reqId, server, uri };
10481
+ }
10482
+ const tool = controlText(value, "tool");
10483
+ const args = value.args;
10484
+ if (!tool || tool.length > 200 || !args || typeof args !== "object" || Array.isArray(args)) {
10485
+ return "invalid";
10486
+ }
10487
+ return { op, chatId, reqId, server, tool, args };
10488
+ }
10489
+ function phoneMcpToolPayload(parts, tool, callId) {
10490
+ if (!parts) return null;
10491
+ const matches = parts.filter(
10492
+ (part2) => part2.type === "tool" && part2.name === tool && !part2.parentId
10493
+ );
10494
+ const part = matches.find((row2) => row2.id === callId) ?? matches[matches.length - 1];
10495
+ if (!part) return null;
10496
+ return {
10497
+ input: part.input ?? {},
10498
+ resultText: part.result ?? "",
10499
+ isError: part.status === "error"
10500
+ };
10501
+ }
10502
+
10212
10503
  // src/remote/phone-stream.ts
10213
10504
  var PHONE_STREAM_FLUSH_MS = 80;
10214
10505
  function phoneStreamFrame(parts) {
@@ -10267,7 +10558,8 @@ function listPhoneMessages(thread) {
10267
10558
  const visible = [phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n");
10268
10559
  const text5 = message.role === "agent" ? phoneArtifactText(visible, message.parts, MESSAGE_LIMIT) : clip(visible, MESSAGE_LIMIT);
10269
10560
  if (!text5) continue;
10270
- out.push({ role: message.role, text: text5 });
10561
+ const apps = message.role === "agent" ? phoneMcpApps(thread.agent, message.parts) : [];
10562
+ out.push({ role: message.role, text: text5, ...apps.length ? { apps } : {} });
10271
10563
  }
10272
10564
  const live = withLiveTail(thread, out);
10273
10565
  for (const prompt of thread.queue) {
@@ -10394,6 +10686,7 @@ function parsePhoneControl(value) {
10394
10686
  }
10395
10687
  if (op === "pane") return parsePhonePane(value, chatId);
10396
10688
  if (op === "open-artifact") return parseOpenArtifact(value, chatId);
10689
+ if (op === "mcp-ui" || op === "mcp-ui-call" || op === "mcp-ui-read") return parsePhoneMcpUi(value, op, chatId);
10397
10690
  if (op === "open-file" || op === "media") {
10398
10691
  const path2 = textField2(value, "path");
10399
10692
  if (!path2 || path2.length > 2e3) return "invalid";
@@ -10809,108 +11102,6 @@ function archivePhoneChat(chatId) {
10809
11102
  return true;
10810
11103
  }
10811
11104
 
10812
- // src/remote/phone-workspace-ops.ts
10813
- function dispatchPhoneWorkspace(cmd, emit, publishOpened) {
10814
- if (cmd.op === "pane") {
10815
- const thread = readPhoneThread(cmd.chatId);
10816
- if (!thread) {
10817
- emit({ op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
10818
- return;
10819
- }
10820
- void readPhonePane(thread, cmd.pane, cmd.scope, cmd.query, cmd.commitSha).then((body) => {
10821
- emit({
10822
- op: "pane",
10823
- chatId: cmd.chatId,
10824
- pane: cmd.pane,
10825
- rows: body.rows,
10826
- ...body.note ? { note: body.note } : {},
10827
- ...cmd.pane === "files" ? { query: cmd.query ?? "" } : {},
10828
- ...cmd.pane === "changes" ? {
10829
- scope: body.scope,
10830
- commitSha: body.commitSha ?? null,
10831
- ...body.base ? { base: body.base } : {},
10832
- hasLastTurnBase: body.hasLastTurnBase,
10833
- commits: body.commits ?? [],
10834
- ...body.scopeStats ? { scopeStats: body.scopeStats } : {}
10835
- } : {}
10836
- });
10837
- });
10838
- return;
10839
- }
10840
- if (cmd.op === "header") {
10841
- const thread = readPhoneThread(cmd.chatId);
10842
- if (!thread) {
10843
- emit({ op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
10844
- return;
10845
- }
10846
- void readPhoneHeader(thread).then((header) => {
10847
- emit({ op: "header", chatId: cmd.chatId, ...header });
10848
- });
10849
- return;
10850
- }
10851
- if (cmd.op === "git") {
10852
- void getOrchestrator().askGit(cmd.chatId, cmd.action).then((tab) => publishOpened(tab.id)).catch((err) => {
10853
- emit({
10854
- op: "error",
10855
- chatId: cmd.chatId,
10856
- message: err instanceof Error ? err.message : String(err)
10857
- });
10858
- });
10859
- return;
10860
- }
10861
- void getOrchestrator().requestReview(cmd.chatId).then((tab) => publishOpened(tab.id)).catch((err) => {
10862
- emit({
10863
- op: "error",
10864
- chatId: cmd.chatId,
10865
- message: err instanceof Error ? err.message : String(err)
10866
- });
10867
- });
10868
- }
10869
-
10870
- // src/remote/phone-sources.ts
10871
- function sourceWarning(label, err) {
10872
- const message = err instanceof Error ? err.message : String(err);
10873
- return message ? `${label}: ${message}` : `${label} failed.`;
10874
- }
10875
- async function listPhoneSources(repoPath, query) {
10876
- const path2 = assertKnownProject(repoPath);
10877
- const q = query?.trim() ?? "";
10878
- const warnings = [];
10879
- const [prs, branches, issues] = await Promise.all([
10880
- listPrs(path2, { state: "open", limit: 40, ...q ? { query: q } : {} }).catch((err) => {
10881
- warnings.push(sourceWarning("Pull requests", err));
10882
- return [];
10883
- }),
10884
- listBranches(path2).catch((err) => {
10885
- warnings.push(sourceWarning("Branches", err));
10886
- return [];
10887
- }),
10888
- listIssues(path2, { limit: 40, ...q ? { query: q } : {} }).catch((err) => {
10889
- warnings.push(sourceWarning("Issues", err));
10890
- return { issues: [] };
10891
- })
10892
- ]);
10893
- const needle = q.toLowerCase();
10894
- const branchRows = branches.filter((branch) => !needle || branch.name.toLowerCase().includes(needle)).slice(0, 50).map((branch) => ({
10895
- ref: branch.name,
10896
- title: branch.current ? `${branch.name} (current)` : branch.name
10897
- }));
10898
- return {
10899
- prs: prs.slice(0, 40).map((pr) => ({
10900
- ref: String(pr.number),
10901
- title: clip(pr.title || `PR #${pr.number}`, 140),
10902
- ...pr.url ? { url: pr.url } : {}
10903
- })),
10904
- branches: branchRows,
10905
- issues: (issues.issues ?? []).slice(0, 40).map((issue) => ({
10906
- ref: issue.identifier,
10907
- title: clip(issue.title || issue.identifier, 140),
10908
- ...issue.url ? { url: issue.url } : {}
10909
- })),
10910
- ...warnings.length ? { warnings } : {}
10911
- };
10912
- }
10913
-
10914
11105
  // src/remote/phone-live.ts
10915
11106
  var REMOTE_STOPPED_REPLY = "Sideboard stopped the in-progress turn.";
10916
11107
  var chatChains = /* @__PURE__ */ new Map();
@@ -11037,10 +11228,12 @@ function emitChatTurn(opts, chatId, threadId) {
11037
11228
  failed: (detail) => detail ? `Sideboard failed: ${detail}` : "Sideboard failed before producing a result."
11038
11229
  });
11039
11230
  const agent = [...thread.messages].reverse().find((m) => m.role === "agent");
11231
+ const apps = phoneMcpApps(thread.agent, agent?.parts);
11040
11232
  emitControl(opts, {
11041
11233
  op: "assistant",
11042
11234
  chatId,
11043
- text: phoneArtifactText(reply.trim(), agent?.parts)
11235
+ text: phoneArtifactText(reply.trim(), agent?.parts),
11236
+ ...apps.length ? { apps } : {}
11044
11237
  });
11045
11238
  }
11046
11239
  function phoneError(err) {
@@ -11073,6 +11266,279 @@ function deliverPhoneTurn(opts, chatId) {
11073
11266
  });
11074
11267
  }
11075
11268
 
11269
+ // src/remote/phone-mcp-ui-listen.ts
11270
+ var PHONE_HTML_CAP = 7e5;
11271
+ function dispatchPhoneMcpUi(cmd, opts) {
11272
+ if (cmd.op !== "mcp-ui" && cmd.op !== "mcp-ui-call" && cmd.op !== "mcp-ui-read") return false;
11273
+ const thread = readPhoneThread(cmd.chatId);
11274
+ const reqId = cmd.reqId;
11275
+ const chatId = cmd.chatId;
11276
+ if (!thread || thread.agent !== "claude") {
11277
+ if (cmd.op === "mcp-ui") emitControl(opts, { op: "mcp-ui", chatId, reqId, view: null });
11278
+ else emitControl(opts, { op: cmd.op, chatId, reqId, error: "This chat is not using Claude." });
11279
+ return true;
11280
+ }
11281
+ if (cmd.op === "mcp-ui-call") {
11282
+ void getMcpUiHost().callTool(cmd.server, cmd.tool, cmd.args).then((result) => emitControl(opts, { op: "mcp-ui-call", chatId, reqId, result })).catch(
11283
+ (err) => emitControl(opts, { op: "mcp-ui-call", chatId, reqId, error: phoneError(err) })
11284
+ );
11285
+ return true;
11286
+ }
11287
+ if (cmd.op === "mcp-ui-read") {
11288
+ void getMcpUiHost().readResource(cmd.server, cmd.uri).then((result) => emitControl(opts, { op: "mcp-ui-read", chatId, reqId, result })).catch(
11289
+ (err) => emitControl(opts, { op: "mcp-ui-read", chatId, reqId, error: phoneError(err) })
11290
+ );
11291
+ return true;
11292
+ }
11293
+ const agentMessage = [...thread.messages].reverse().find((message) => message.role === "agent");
11294
+ const payload = phoneMcpToolPayload(agentMessage?.parts, cmd.tool, cmd.callId);
11295
+ void getMcpUiHost().viewForClaudeTool(cmd.tool).then((view) => {
11296
+ if (!view || view.html.length > PHONE_HTML_CAP) {
11297
+ emitControl(opts, { op: "mcp-ui", chatId, reqId, view: null });
11298
+ return;
11299
+ }
11300
+ emitControl(opts, {
11301
+ op: "mcp-ui",
11302
+ chatId,
11303
+ reqId,
11304
+ view: {
11305
+ server: view.server,
11306
+ tool: view.tool,
11307
+ html: view.html,
11308
+ csp: view.csp,
11309
+ uri: view.uri,
11310
+ input: payload?.input ?? {},
11311
+ resultText: payload?.resultText ?? "",
11312
+ isError: payload?.isError ?? false
11313
+ }
11314
+ });
11315
+ }).catch((err) => emitControl(opts, { op: "error", chatId, message: phoneError(err) }));
11316
+ return true;
11317
+ }
11318
+
11319
+ // src/remote/phone-media.ts
11320
+ import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
11321
+ import { join as join6 } from "path";
11322
+ var PHONE_IMAGE_MAX_BYTES = 15e5;
11323
+ var PHONE_TEXT_MAX_BYTES = 18e4;
11324
+ var PHONE_DIR_MAX = 200;
11325
+ var IMAGE_MIME = {
11326
+ png: "image/png",
11327
+ jpg: "image/jpeg",
11328
+ jpeg: "image/jpeg",
11329
+ gif: "image/gif",
11330
+ webp: "image/webp",
11331
+ bmp: "image/bmp"
11332
+ };
11333
+ function phoneWorktreeRelative(worktreePath, raw) {
11334
+ let text5 = raw.trim();
11335
+ if (!text5) return null;
11336
+ if (/^file:\/\//i.test(text5)) {
11337
+ try {
11338
+ text5 = decodeURIComponent(new URL(text5).pathname);
11339
+ } catch {
11340
+ return null;
11341
+ }
11342
+ }
11343
+ text5 = (text5.split("#")[0] ?? text5).replace(/\\/g, "/");
11344
+ if (!text5 || text5.includes("\0") || text5.split("/").includes("..")) return null;
11345
+ const root = worktreePath.replace(/\/+$/, "");
11346
+ if (!root || root === "/") return null;
11347
+ if (text5.startsWith("/")) {
11348
+ if (text5 !== root && !text5.startsWith(`${root}/`)) return null;
11349
+ text5 = text5 === root ? "" : text5.slice(root.length + 1);
11350
+ }
11351
+ text5 = text5.replace(/^\.\//, "").replace(/\/+$/, "");
11352
+ if (!text5 || text5 === "." || text5.startsWith("/")) return null;
11353
+ return text5;
11354
+ }
11355
+ function phoneImageMime(relativePath) {
11356
+ const ext = relativePath.split(".").pop()?.toLowerCase() ?? "";
11357
+ return IMAGE_MIME[ext] ?? null;
11358
+ }
11359
+ function phoneImageDataUrl(worktreePath, raw) {
11360
+ const relative = phoneWorktreeRelative(worktreePath, raw);
11361
+ if (!relative) return null;
11362
+ const mime = phoneImageMime(relative);
11363
+ if (!mime) return null;
11364
+ try {
11365
+ const file = readWorktreeFileForUpload(worktreePath, relative, { maxBytes: PHONE_IMAGE_MAX_BYTES });
11366
+ return `data:${mime};base64,${file.contentBase64}`;
11367
+ } catch {
11368
+ return null;
11369
+ }
11370
+ }
11371
+ function phoneDirectoryListing(worktreePath, relative) {
11372
+ try {
11373
+ const entries = readdirSync2(join6(worktreePath, relative), { withFileTypes: true });
11374
+ const dirs = new Set(entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name));
11375
+ const names = entries.map((entry) => entry.name).filter((name) => name && !name.includes("/") && !name.includes("\0")).sort((a, b) => a.localeCompare(b));
11376
+ const shown = names.slice(0, PHONE_DIR_MAX);
11377
+ const lines = shown.map((name) => dirs.has(name) ? `${name}/` : name);
11378
+ const truncated = names.length > shown.length;
11379
+ if (truncated) lines.push(`\u2026 ${names.length - shown.length} more`);
11380
+ return { content: lines.join("\n"), truncated };
11381
+ } catch {
11382
+ return null;
11383
+ }
11384
+ }
11385
+ function phoneFilePreview(worktreePath, relative, kind) {
11386
+ if (kind === "dir") {
11387
+ const listing = phoneDirectoryListing(worktreePath, relative);
11388
+ if (!listing) return null;
11389
+ return {
11390
+ content: listing.content,
11391
+ encoding: "utf8",
11392
+ binary: false,
11393
+ directory: true,
11394
+ truncated: listing.truncated
11395
+ };
11396
+ }
11397
+ if (relative.toLowerCase().endsWith(".svg")) {
11398
+ try {
11399
+ const buf = readFileSync5(join6(worktreePath, relative));
11400
+ const truncated = buf.length > PHONE_TEXT_MAX_BYTES;
11401
+ return {
11402
+ content: buf.subarray(0, PHONE_TEXT_MAX_BYTES).toString("utf8"),
11403
+ encoding: "utf8",
11404
+ binary: false,
11405
+ directory: false,
11406
+ truncated,
11407
+ mime: "image/svg+xml"
11408
+ };
11409
+ } catch {
11410
+ return null;
11411
+ }
11412
+ }
11413
+ const mime = phoneImageMime(relative);
11414
+ if (mime) {
11415
+ const dataUrl = phoneImageDataUrl(worktreePath, relative);
11416
+ if (!dataUrl) return null;
11417
+ return {
11418
+ content: dataUrl,
11419
+ encoding: "base64",
11420
+ binary: true,
11421
+ directory: false,
11422
+ truncated: false,
11423
+ mime
11424
+ };
11425
+ }
11426
+ try {
11427
+ const file = readWorktreeFile(worktreePath, relative, { maxBytes: PHONE_TEXT_MAX_BYTES });
11428
+ return {
11429
+ content: file.content,
11430
+ encoding: file.encoding,
11431
+ binary: file.binary,
11432
+ directory: false,
11433
+ truncated: file.truncated
11434
+ };
11435
+ } catch {
11436
+ return null;
11437
+ }
11438
+ }
11439
+
11440
+ // src/remote/phone-workspace-ops.ts
11441
+ function dispatchPhoneWorkspace(cmd, emit, publishOpened) {
11442
+ if (cmd.op === "pane") {
11443
+ const thread = readPhoneThread(cmd.chatId);
11444
+ if (!thread) {
11445
+ emit({ op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
11446
+ return;
11447
+ }
11448
+ void readPhonePane(thread, cmd.pane, cmd.scope, cmd.query, cmd.commitSha).then((body) => {
11449
+ emit({
11450
+ op: "pane",
11451
+ chatId: cmd.chatId,
11452
+ pane: cmd.pane,
11453
+ rows: body.rows,
11454
+ ...body.note ? { note: body.note } : {},
11455
+ ...cmd.pane === "files" ? { query: cmd.query ?? "" } : {},
11456
+ ...cmd.pane === "changes" ? {
11457
+ scope: body.scope,
11458
+ commitSha: body.commitSha ?? null,
11459
+ ...body.base ? { base: body.base } : {},
11460
+ hasLastTurnBase: body.hasLastTurnBase,
11461
+ commits: body.commits ?? [],
11462
+ ...body.scopeStats ? { scopeStats: body.scopeStats } : {}
11463
+ } : {}
11464
+ });
11465
+ });
11466
+ return;
11467
+ }
11468
+ if (cmd.op === "header") {
11469
+ const thread = readPhoneThread(cmd.chatId);
11470
+ if (!thread) {
11471
+ emit({ op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
11472
+ return;
11473
+ }
11474
+ void readPhoneHeader(thread).then((header) => {
11475
+ emit({ op: "header", chatId: cmd.chatId, ...header });
11476
+ });
11477
+ return;
11478
+ }
11479
+ if (cmd.op === "git") {
11480
+ void getOrchestrator().askGit(cmd.chatId, cmd.action).then((tab) => publishOpened(tab.id)).catch((err) => {
11481
+ emit({
11482
+ op: "error",
11483
+ chatId: cmd.chatId,
11484
+ message: err instanceof Error ? err.message : String(err)
11485
+ });
11486
+ });
11487
+ return;
11488
+ }
11489
+ void getOrchestrator().requestReview(cmd.chatId).then((tab) => publishOpened(tab.id)).catch((err) => {
11490
+ emit({
11491
+ op: "error",
11492
+ chatId: cmd.chatId,
11493
+ message: err instanceof Error ? err.message : String(err)
11494
+ });
11495
+ });
11496
+ }
11497
+
11498
+ // src/remote/phone-sources.ts
11499
+ function sourceWarning(label, err) {
11500
+ const message = err instanceof Error ? err.message : String(err);
11501
+ return message ? `${label}: ${message}` : `${label} failed.`;
11502
+ }
11503
+ async function listPhoneSources(repoPath, query) {
11504
+ const path2 = assertKnownProject(repoPath);
11505
+ const q = query?.trim() ?? "";
11506
+ const warnings = [];
11507
+ const [prs, branches, issues] = await Promise.all([
11508
+ listPrs(path2, { state: "open", limit: 40, ...q ? { query: q } : {} }).catch((err) => {
11509
+ warnings.push(sourceWarning("Pull requests", err));
11510
+ return [];
11511
+ }),
11512
+ listBranches(path2).catch((err) => {
11513
+ warnings.push(sourceWarning("Branches", err));
11514
+ return [];
11515
+ }),
11516
+ listIssues(path2, { limit: 40, ...q ? { query: q } : {} }).catch((err) => {
11517
+ warnings.push(sourceWarning("Issues", err));
11518
+ return { issues: [] };
11519
+ })
11520
+ ]);
11521
+ const needle = q.toLowerCase();
11522
+ const branchRows = branches.filter((branch) => !needle || branch.name.toLowerCase().includes(needle)).slice(0, 50).map((branch) => ({
11523
+ ref: branch.name,
11524
+ title: branch.current ? `${branch.name} (current)` : branch.name
11525
+ }));
11526
+ return {
11527
+ prs: prs.slice(0, 40).map((pr) => ({
11528
+ ref: String(pr.number),
11529
+ title: clip(pr.title || `PR #${pr.number}`, 140),
11530
+ ...pr.url ? { url: pr.url } : {}
11531
+ })),
11532
+ branches: branchRows,
11533
+ issues: (issues.issues ?? []).slice(0, 40).map((issue) => ({
11534
+ ref: issue.identifier,
11535
+ title: clip(issue.title || issue.identifier, 140),
11536
+ ...issue.url ? { url: issue.url } : {}
11537
+ })),
11538
+ ...warnings.length ? { warnings } : {}
11539
+ };
11540
+ }
11541
+
11076
11542
  // src/remote/phone-turn.ts
11077
11543
  function formatRemotePrompt(text5) {
11078
11544
  return `Phone
@@ -11421,6 +11887,7 @@ function handlePhoneControl(cmd, opts) {
11421
11887
  });
11422
11888
  return;
11423
11889
  }
11890
+ if (dispatchPhoneMcpUi(cmd, opts)) return;
11424
11891
  if (cmd.op === "open-file") {
11425
11892
  const thread = readPhoneThread(cmd.chatId);
11426
11893
  const relative = thread ? phoneWorktreeRelative(thread.worktreePath, cmd.path) : null;
@@ -13551,7 +14018,7 @@ async function continueSourceThread(threadId, prompt) {
13551
14018
  await continueOnReply(threadId, prompt);
13552
14019
  return;
13553
14020
  }
13554
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-JEF7JC3S.js");
14021
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-CZPRDLHV.js");
13555
14022
  await getOrchestrator2().send(threadId, prompt);
13556
14023
  } catch {
13557
14024
  }
@@ -14083,6 +14550,7 @@ export {
14083
14550
  clipAgentEventForPaint,
14084
14551
  clipThinkingForStore,
14085
14552
  cloneRepoIntoSideboard,
14553
+ closeMcpUiHost,
14086
14554
  codeRefRangeLabel,
14087
14555
  codexAdapter,
14088
14556
  codexSandboxWritableRootsArgs,
@@ -14305,6 +14773,7 @@ export {
14305
14773
  getLinearApiKey,
14306
14774
  getLinearAuthToken,
14307
14775
  getLinearIssue,
14776
+ getMcpUiHost,
14308
14777
  getOrchestrator,
14309
14778
  getPr,
14310
14779
  getPrChecks,
@@ -14370,6 +14839,7 @@ export {
14370
14839
  isBrightsyConnected,
14371
14840
  isBrightsyNdjsonLine,
14372
14841
  isChatTextScale,
14842
+ isClaudeAiConnectorMcp,
14373
14843
  isCloudCoordinatorThread,
14374
14844
  isCompactToolName,
14375
14845
  isConductorBundledCli,
@@ -14524,6 +14994,7 @@ export {
14524
14994
  mcpServerIdForPermission,
14525
14995
  mcpServerPermissionUpdate,
14526
14996
  mcpStatusThinkingFromClaudeInit,
14997
+ mcpUiContentSecurityPolicy,
14527
14998
  mentionsPrGoal,
14528
14999
  mergeAgentGitAuthEnv,
14529
15000
  mergePr,