@sideboard-ai/core 0.1.288 → 0.1.290

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 (35) hide show
  1. package/dist/{agents-GZLTMGSH.js → agents-4BBCKITL.js} +4 -4
  2. package/dist/{agents-UQMHFZSM.js → agents-XAKSP653.js} +4 -4
  3. package/dist/{chunk-HNB2HTCV.js → chunk-2IXKTAIT.js} +1 -1
  4. package/dist/{chunk-442O6OXM.js → chunk-4VXWLY3L.js} +13 -12
  5. package/dist/{chunk-3W5SL5EW.js → chunk-5HPMKLJ7.js} +1 -1
  6. package/dist/{chunk-4NM72HJZ.js → chunk-G74TSBJ3.js} +4 -4
  7. package/dist/{chunk-WQSIUZEB.js → chunk-GBAACE3C.js} +4 -4
  8. package/dist/{chunk-2N77CLRQ.js → chunk-LUQ7MIVD.js} +3 -3
  9. package/dist/{chunk-IA4MPG7F.js → chunk-O4324MXE.js} +2 -2
  10. package/dist/{chunk-WW5XGEMY.js → chunk-QOUTSISS.js} +14 -13
  11. package/dist/{chunk-DBOFDEQ7.js → chunk-V3BSO2O6.js} +2 -2
  12. package/dist/{chunk-6NVQNTJO.js → chunk-VNCPRMIK.js} +3 -3
  13. package/dist/{conductor-MCU63Z5U.js → conductor-5MNGC25D.js} +4 -4
  14. package/dist/{conductor-ZLYZVIAV.js → conductor-UFJXJPTF.js} +4 -4
  15. package/dist/{coordinator-prompt-KHCXXDRO.js → coordinator-prompt-5Q66Y5KW.js} +3 -3
  16. package/dist/{coordinator-prompt-UV6THLMD.js → coordinator-prompt-BYPEW6DE.js} +3 -3
  17. package/dist/{global-workspace-GDHZSFP6.js → global-workspace-BTH2YGDZ.js} +3 -3
  18. package/dist/{global-workspace-AXTSYVAE.js → global-workspace-YKOYXSB2.js} +3 -3
  19. package/dist/index.cjs +695 -145
  20. package/dist/index.d.cts +18 -2
  21. package/dist/index.d.ts +18 -2
  22. package/dist/index.js +641 -102
  23. package/dist/mcp/run-stdio.cjs +2 -1
  24. package/dist/mcp/run-stdio.js +6 -6
  25. package/dist/{orchestrator-5ZBHR6BJ.js → orchestrator-EQSK2RMI.js} +6 -6
  26. package/dist/{orchestrator-SDH6W6ZL.js → orchestrator-GSOW3RPA.js} +6 -6
  27. package/dist/{thread-store-7XBOUQ5K.js → thread-store-DPKESDFR.js} +1 -1
  28. package/dist/{thread-store-ZQZDREVH.js → thread-store-X6S2GIR5.js} +1 -1
  29. package/dist/{workspaces-Q2R2XGO3.js → workspaces-ELO2YVTU.js} +3 -3
  30. package/dist/{workspaces-L2UR4KC2.js → workspaces-RFD6I4FM.js} +3 -3
  31. package/dist/{worktree-2GOK4VYT.js → worktree-ADVQ6X6C.js} +2 -2
  32. package/dist/{worktree-YEUTJ7MZ.js → worktree-TN7DZDJZ.js} +2 -2
  33. package/package.json +1 -1
  34. /package/dist/{chunk-N5UGX3JL.js → chunk-QB5MDOHL.js} +0 -0
  35. /package/dist/{chunk-JPH7PYSK.js → chunk-YRXV3E7J.js} +0 -0
package/dist/index.cjs CHANGED
@@ -1160,9 +1160,9 @@ function sumOptional(a, b) {
1160
1160
  function fromInclusiveInputUsage(opts) {
1161
1161
  const totalInput = Number(opts.inputTokens) || 0;
1162
1162
  const outputTokens = Number(opts.outputTokens) || 0;
1163
- const cached = Number(opts.cachedInputTokens) || 0;
1163
+ const cached2 = Number(opts.cachedInputTokens) || 0;
1164
1164
  if (!totalInput && !outputTokens) return null;
1165
- const cacheReadTokens = cached > 0 ? Math.min(cached, totalInput) : 0;
1165
+ const cacheReadTokens = cached2 > 0 ? Math.min(cached2, totalInput) : 0;
1166
1166
  return {
1167
1167
  inputTokens: Math.max(0, totalInput - cacheReadTokens),
1168
1168
  outputTokens,
@@ -4702,8 +4702,8 @@ function readThread(id) {
4702
4702
  return null;
4703
4703
  }
4704
4704
  const cache2 = cacheForDir();
4705
- const cached = cache2.byId.get(id);
4706
- if (cached && cache2.mtimeMs.get(id) === mtimeMs) return cached;
4705
+ const cached2 = cache2.byId.get(id);
4706
+ if (cached2 && cache2.mtimeMs.get(id) === mtimeMs) return cached2;
4707
4707
  const raw = (0, import_node_fs12.readFileSync)(path2, "utf8");
4708
4708
  const thread = normalizeThread(JSON.parse(raw));
4709
4709
  rememberThread(thread, mtimeMs);
@@ -6922,15 +6922,15 @@ function loadFromDisk(threadId) {
6922
6922
  }
6923
6923
  }
6924
6924
  function memFor(threadId) {
6925
- const cached = memory.get(threadId);
6926
- if (cached) return cached;
6925
+ const cached2 = memory.get(threadId);
6926
+ if (cached2) return cached2;
6927
6927
  const mem = fromSnapshot(loadFromDisk(threadId) ?? emptySetupLog());
6928
6928
  memory.set(threadId, mem);
6929
6929
  return mem;
6930
6930
  }
6931
6931
  function readSetupLog(threadId) {
6932
- const cached = memory.get(threadId);
6933
- if (cached) return materialize(cached);
6932
+ const cached2 = memory.get(threadId);
6933
+ if (cached2) return materialize(cached2);
6934
6934
  const disk = loadFromDisk(threadId);
6935
6935
  if (disk) {
6936
6936
  memory.set(threadId, fromSnapshot(disk));
@@ -7076,15 +7076,15 @@ function loadFromDisk2(key) {
7076
7076
  }
7077
7077
  }
7078
7078
  function memFor2(key) {
7079
- const cached = memory2.get(key);
7080
- if (cached) return cached;
7079
+ const cached2 = memory2.get(key);
7080
+ if (cached2) return cached2;
7081
7081
  const mem = fromSnapshot2(loadFromDisk2(key) ?? emptyRunLog());
7082
7082
  memory2.set(key, mem);
7083
7083
  return mem;
7084
7084
  }
7085
7085
  function readRunLog(key) {
7086
- const cached = memory2.get(key);
7087
- if (cached) return materialize2(cached);
7086
+ const cached2 = memory2.get(key);
7087
+ if (cached2) return materialize2(cached2);
7088
7088
  const disk = loadFromDisk2(key);
7089
7089
  if (disk) {
7090
7090
  memory2.set(key, fromSnapshot2(disk));
@@ -15128,8 +15128,8 @@ var init_claude = __esm({
15128
15128
  return { type: "stdout", data: line };
15129
15129
  }
15130
15130
  },
15131
- async resolveSessionId(_worktreePath, cached) {
15132
- return cached;
15131
+ async resolveSessionId(_worktreePath, cached2) {
15132
+ return cached2;
15133
15133
  },
15134
15134
  async buildAttach(thread) {
15135
15135
  const claude = resolveClaudeExecutable();
@@ -15513,8 +15513,8 @@ var init_codex = __esm({
15513
15513
  return { type: "stdout", data: line };
15514
15514
  }
15515
15515
  },
15516
- async resolveSessionId(_worktreePath, cached) {
15517
- return cached;
15516
+ async resolveSessionId(_worktreePath, cached2) {
15517
+ return cached2;
15518
15518
  },
15519
15519
  async buildAttach(thread) {
15520
15520
  const codex = resolveAgentExecutable("codex");
@@ -16150,8 +16150,8 @@ var init_cursor = __esm({
16150
16150
  parseEvent(line) {
16151
16151
  return parseCursorRunnerLine(line);
16152
16152
  },
16153
- async resolveSessionId(_worktreePath, cached) {
16154
- return cached;
16153
+ async resolveSessionId(_worktreePath, cached2) {
16154
+ return cached2;
16155
16155
  },
16156
16156
  async buildAttach(thread) {
16157
16157
  const which = await run("which", ["cursor"], { reject: false });
@@ -16633,8 +16633,8 @@ var init_opencode = __esm({
16633
16633
  return { type: "stdout", data: line };
16634
16634
  }
16635
16635
  },
16636
- async resolveSessionId(worktreePath, cached) {
16637
- const cachedId = cached?.trim() || null;
16636
+ async resolveSessionId(worktreePath, cached2) {
16637
+ const cachedId = cached2?.trim() || null;
16638
16638
  const listed = await run(
16639
16639
  resolveAgentExecutable("opencode"),
16640
16640
  ["session", "list", "--format", "json"],
@@ -16961,7 +16961,7 @@ function numberOrUndef(value) {
16961
16961
  if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
16962
16962
  return value;
16963
16963
  }
16964
- function parseClaudeUsagePayload(raw, fetchedAt = (/* @__PURE__ */ new Date()).toISOString()) {
16964
+ function parseClaudeUsagePayload(raw, fetchedAt2 = (/* @__PURE__ */ new Date()).toISOString()) {
16965
16965
  if (!raw || typeof raw !== "object") return null;
16966
16966
  const obj = raw;
16967
16967
  const source = obj.rate_limits && typeof obj.rate_limits === "object" ? obj.rate_limits : obj;
@@ -16975,7 +16975,7 @@ function parseClaudeUsagePayload(raw, fetchedAt = (/* @__PURE__ */ new Date()).t
16975
16975
  return {
16976
16976
  windows,
16977
16977
  ...extra ? { extraUsage: extra } : {},
16978
- fetchedAt
16978
+ fetchedAt: fetchedAt2
16979
16979
  };
16980
16980
  }
16981
16981
  function hottestClaudeWindow(usage) {
@@ -17018,15 +17018,15 @@ function formatClaudeUsageResetClock(resetsAt) {
17018
17018
  return at.toISOString();
17019
17019
  }
17020
17020
  }
17021
- function formatClaudeUsageFetchedAt(fetchedAt, now = /* @__PURE__ */ new Date()) {
17022
- const at = new Date(fetchedAt);
17021
+ function formatClaudeUsageFetchedAt(fetchedAt2, now = /* @__PURE__ */ new Date()) {
17022
+ const at = new Date(fetchedAt2);
17023
17023
  if (Number.isNaN(at.getTime())) return "";
17024
17024
  const minutes = Math.max(0, Math.round((now.getTime() - at.getTime()) / 6e4));
17025
17025
  if (minutes < 1) return "Updated just now";
17026
17026
  if (minutes < 60) return `Updated ${minutes}m ago`;
17027
17027
  const hours = Math.round(minutes / 60);
17028
17028
  if (hours < 36) return `Updated ${hours}h ago`;
17029
- return `Updated ${formatClaudeUsageResetClock(fetchedAt)}`;
17029
+ return `Updated ${formatClaudeUsageResetClock(fetchedAt2)}`;
17030
17030
  }
17031
17031
  function formatClaudeExtraUsageDetail(extra) {
17032
17032
  if (!extra.enabled) return "Extra usage off";
@@ -26376,12 +26376,13 @@ function formatArtifactDirective() {
26376
26376
  "Sideboard side column (desktop UI) \u2014 Sideboard MCP tools present_artifact, present_schema, present_files, ask_user (see their descriptions). claude.ai\u2019s \u201CArtifact\u201D tool does not exist here; never say artifacts, the CMS UI, or the Files column are unavailable.",
26377
26377
  "- Standalone documents: a fenced block tagged `html` (preferred), `svg`, or `markdown` containing the FULL document opens the side column by itself. Use that or present_artifact \u2014 never both for the same body.",
26378
26378
  "- present_artifact type=log appends: same artifact_id, content = new lines only (plus status/phase). Do not resend the full log or wrap it in HTML.",
26379
+ "- Chat links open the side column and files: `[label](sideboard://artifact/<artifact_id or title>)` opens that pane; `[label](sideboard://file/<path>#L12-L40)` opens the file (line range optional). A repo-relative markdown link opens the file too.",
26379
26380
  "- Data: a markdown table is enough to read. Call present_schema only when the user needs to filter/edit/publish/persist rows \u2014 including when they ask for an editable table after you already showed markdown. Never re-present rows you already wrote just to display them.",
26380
26381
  "- ask_user only when work is blocked on a few concrete options (approach fork, which API, auth vs cookies): first a short chat message explaining the decision and each option, then the call (description on every option), then stop and wait. Not for greetings, check-ins, an invented menu of next tasks, or after a review \u2014 reply in chat. If one option is the obvious default, proceed. ask_user already notifies the parent orchestrator. Call notify_orchestrator only when blocked without a picker."
26381
26382
  ].join("\n");
26382
26383
  }
26383
26384
  function formatUiReminder() {
26384
- return "Sideboard UI: markdown table is enough to read data; present_schema if they ask to edit/filter (even after markdown); present_files for the file manager. html fence or present_artifact, not both for the same document. type=log appends (same artifact_id, new lines only). ask_user only for a real multiple-choice (not hellos, \u201Cwhat next?\u201D, or after a review) \u2014 reply in chat. ask_user wakes the parent orchestrator; notify_orchestrator if blocked without a picker. Do not say artifacts/CMS UI are unavailable. Dev / preview: run_dev_script (same as the Dev button; pass name \u2014 several scripts can run at once) returns {url, port, preview} (SIDEBOARD_PORT \u2014 never guess :3000). preview=url \u2192 open url; preview=window \u2192 Electron window is the app, url is renderer/HMR only. list_run_scripts active[].url if already running; get_run_log for that script\u2019s Run-tab terminal; stop_dev_script to stop one.";
26385
+ return "Sideboard UI: markdown table is enough to read data; present_schema if they ask to edit/filter (even after markdown); present_files for the file manager. html fence or present_artifact, not both for the same document. type=log appends (same artifact_id, new lines only). Chat links: `[name](sideboard://artifact/<id-or-title>)` opens that pane; `[name](sideboard://file/path#L12-L40)` opens a file. ask_user only for a real multiple-choice (not hellos, \u201Cwhat next?\u201D, or after a review) \u2014 reply in chat. ask_user wakes the parent orchestrator; notify_orchestrator if blocked without a picker. Do not say artifacts/CMS UI are unavailable. Dev / preview: run_dev_script (same as the Dev button; pass name \u2014 several scripts can run at once) returns {url, port, preview} (SIDEBOARD_PORT \u2014 never guess :3000). preview=url \u2192 open url; preview=window \u2192 Electron window is the app, url is renderer/HMR only. list_run_scripts active[].url if already running; get_run_log for that script\u2019s Run-tab terminal; stop_dev_script to stop one.";
26385
26386
  }
26386
26387
  var import_node_fs63, import_node_path60, PR_GOAL_RE, GITHUB_TICKET_REF, KEYED_TICKET_REF;
26387
26388
  var init_instructions = __esm({
@@ -36106,15 +36107,15 @@ async function getHomeBoardInputs(workspaces, opts) {
36106
36107
  }
36107
36108
  const promise = (async () => {
36108
36109
  const inputs = await loadHomeBoardInputs(workspaces);
36109
- const fetchedAt = opts?.now ?? Date.now();
36110
+ const fetchedAt2 = opts?.now ?? Date.now();
36110
36111
  const pins = syncBoardPins(listBoardPins(), inputs.issues, inputs.prs);
36111
36112
  replaceBoardPins(pins);
36112
- const loaded = { ...inputs, fetchedAt, fromCache: false, pins };
36113
+ const loaded = { ...inputs, fetchedAt: fetchedAt2, fromCache: false, pins };
36113
36114
  if (shouldCacheHomeBoardInputs(inputs)) {
36114
36115
  const entry = {
36115
36116
  version: CACHE_VERSION,
36116
36117
  workspaceKey: key,
36117
- fetchedAt,
36118
+ fetchedAt: fetchedAt2,
36118
36119
  inputs
36119
36120
  };
36120
36121
  memory3 = entry;
@@ -39883,14 +39884,46 @@ var RemoteHub = class {
39883
39884
  this.codes.delete(code);
39884
39885
  this.attachPhone(socket, host);
39885
39886
  }
39887
+ /** In-memory session, with the live Mac's label when that socket is up. */
39888
+ memorySession(sessionToken) {
39889
+ const cached2 = this.phonesByToken.get(sessionToken);
39890
+ if (!cached2) return null;
39891
+ const live = this.hosts.get(cached2.deviceId);
39892
+ const remembered = this.remembered.get(cached2.deviceId);
39893
+ const current = live?.sessionToken ?? remembered?.sessionToken ?? null;
39894
+ if (current && current !== sessionToken) {
39895
+ this.phonesByToken.delete(sessionToken);
39896
+ return null;
39897
+ }
39898
+ return {
39899
+ deviceId: cached2.deviceId,
39900
+ deviceLabel: live?.deviceLabel || remembered?.deviceLabel || cached2.deviceLabel
39901
+ };
39902
+ }
39903
+ knownSession(sessionToken) {
39904
+ const cached2 = this.memorySession(sessionToken);
39905
+ if (cached2) return cached2;
39906
+ const stored = this.opts.phoneSessions?.find(sessionToken) ?? null;
39907
+ if (!stored) return null;
39908
+ const live = this.hosts.get(stored.deviceId);
39909
+ const remembered = this.remembered.get(stored.deviceId);
39910
+ const current = live?.sessionToken ?? remembered?.sessionToken ?? null;
39911
+ if (current && current !== sessionToken) return null;
39912
+ this.phonesByToken.set(sessionToken, stored);
39913
+ if (remembered && !remembered.sessionToken) remembered.sessionToken = sessionToken;
39914
+ if (live && !live.sessionToken) live.sessionToken = sessionToken;
39915
+ return {
39916
+ deviceId: stored.deviceId,
39917
+ deviceLabel: live?.deviceLabel || remembered?.deviceLabel || stored.deviceLabel
39918
+ };
39919
+ }
39886
39920
  resumePhone(socket, sessionToken) {
39887
- const deviceId = this.phonesByToken.get(sessionToken);
39888
- const remembered = deviceId ? this.remembered.get(deviceId) : void 0;
39889
- if (!deviceId || !remembered || remembered.sessionToken !== sessionToken) {
39921
+ const known = this.knownSession(sessionToken);
39922
+ if (!known) {
39890
39923
  send2(socket, { type: "error", message: "phone session expired \u2014 pair again" });
39891
39924
  return;
39892
39925
  }
39893
- const host = this.hosts.get(deviceId);
39926
+ const host = this.hosts.get(known.deviceId);
39894
39927
  if (!host) {
39895
39928
  send2(socket, { type: "host_offline" });
39896
39929
  return;
@@ -39899,17 +39932,13 @@ var RemoteHub = class {
39899
39932
  }
39900
39933
  listHosts(socket, sessionTokens) {
39901
39934
  const hosts = sessionTokens.map((sessionToken) => {
39902
- const deviceId = this.phonesByToken.get(sessionToken);
39903
- const remembered = deviceId ? this.remembered.get(deviceId) : void 0;
39904
- const valid = Boolean(deviceId && remembered && remembered.sessionToken === sessionToken);
39905
- if (!valid || !deviceId || !remembered) {
39906
- return { sessionToken, deviceId: "", deviceLabel: "", online: false };
39907
- }
39908
- const host = this.hosts.get(deviceId);
39935
+ const known = this.knownSession(sessionToken);
39936
+ if (!known) return { sessionToken, deviceId: "", deviceLabel: "", online: false };
39937
+ const host = this.hosts.get(known.deviceId);
39909
39938
  return {
39910
39939
  sessionToken,
39911
- deviceId,
39912
- deviceLabel: host?.deviceLabel || remembered.deviceLabel,
39940
+ deviceId: known.deviceId,
39941
+ deviceLabel: host?.deviceLabel || known.deviceLabel,
39913
39942
  online: Boolean(host)
39914
39943
  };
39915
39944
  });
@@ -39940,18 +39969,20 @@ var RemoteHub = class {
39940
39969
  }
39941
39970
  host.phone = null;
39942
39971
  }
39972
+ const created = sessionToken == null;
39943
39973
  const token = sessionToken ?? (0, import_node_crypto25.randomBytes)(24).toString("hex");
39944
39974
  if (host.sessionToken && host.sessionToken !== token) {
39945
39975
  this.phonesByToken.delete(host.sessionToken);
39946
39976
  }
39947
39977
  host.sessionToken = token;
39948
39978
  host.phone = socket;
39949
- this.phonesByToken.set(token, host.deviceId);
39979
+ this.phonesByToken.set(token, { deviceId: host.deviceId, deviceLabel: host.deviceLabel });
39950
39980
  this.remembered.set(host.deviceId, {
39951
39981
  hostSecret: host.hostSecret,
39952
39982
  sessionToken: token,
39953
39983
  deviceLabel: host.deviceLabel
39954
39984
  });
39985
+ if (created) this.opts.phoneSessions?.save(host.deviceId, token);
39955
39986
  this.bySocket.set(socket, { role: "phone", deviceId: host.deviceId });
39956
39987
  this.opts.onAuthorized?.(socket);
39957
39988
  send2(socket, {
@@ -39996,10 +40027,16 @@ init_stage_files();
39996
40027
  init_orchestrator();
39997
40028
  init_app_settings();
39998
40029
  init_implied_first_prompt();
40030
+ init_message_parts();
40031
+ init_turn_live();
39999
40032
 
40000
40033
  // src/remote/phone-media.ts
40034
+ var import_node_fs71 = require("fs");
40035
+ var import_node_path69 = require("path");
40001
40036
  init_diff();
40002
40037
  var PHONE_IMAGE_MAX_BYTES = 15e5;
40038
+ var PHONE_TEXT_MAX_BYTES = 18e4;
40039
+ var PHONE_DIR_MAX = 200;
40003
40040
  var IMAGE_MIME = {
40004
40041
  png: "image/png",
40005
40042
  jpg: "image/jpeg",
@@ -40046,21 +40083,74 @@ function phoneImageDataUrl(worktreePath, raw) {
40046
40083
  return null;
40047
40084
  }
40048
40085
  }
40049
-
40050
- // src/remote/phone-chats.ts
40051
- var import_node_crypto26 = require("crypto");
40052
- init_list_models();
40053
- init_home_board();
40054
- init_stage_files();
40055
- init_worktree_labels();
40056
- init_thinking_effort();
40057
- init_app_settings();
40058
- init_global_workspace();
40059
- init_thread_store();
40060
- init_workspaces();
40061
- init_chat_tabs();
40062
- init_create();
40063
- init_orchestrator();
40086
+ function phoneDirectoryListing(worktreePath, relative) {
40087
+ try {
40088
+ const entries = (0, import_node_fs71.readdirSync)((0, import_node_path69.join)(worktreePath, relative), { withFileTypes: true });
40089
+ const dirs = new Set(entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name));
40090
+ const names = entries.map((entry) => entry.name).filter((name) => name && !name.includes("/") && !name.includes("\0")).sort((a, b) => a.localeCompare(b));
40091
+ const shown = names.slice(0, PHONE_DIR_MAX);
40092
+ const lines = shown.map((name) => dirs.has(name) ? `${name}/` : name);
40093
+ const truncated = names.length > shown.length;
40094
+ if (truncated) lines.push(`\u2026 ${names.length - shown.length} more`);
40095
+ return { content: lines.join("\n"), truncated };
40096
+ } catch {
40097
+ return null;
40098
+ }
40099
+ }
40100
+ function phoneFilePreview(worktreePath, relative, kind) {
40101
+ if (kind === "dir") {
40102
+ const listing = phoneDirectoryListing(worktreePath, relative);
40103
+ if (!listing) return null;
40104
+ return {
40105
+ content: listing.content,
40106
+ encoding: "utf8",
40107
+ binary: false,
40108
+ directory: true,
40109
+ truncated: listing.truncated
40110
+ };
40111
+ }
40112
+ if (relative.toLowerCase().endsWith(".svg")) {
40113
+ try {
40114
+ const buf = (0, import_node_fs71.readFileSync)((0, import_node_path69.join)(worktreePath, relative));
40115
+ const truncated = buf.length > PHONE_TEXT_MAX_BYTES;
40116
+ return {
40117
+ content: buf.subarray(0, PHONE_TEXT_MAX_BYTES).toString("utf8"),
40118
+ encoding: "utf8",
40119
+ binary: false,
40120
+ directory: false,
40121
+ truncated,
40122
+ mime: "image/svg+xml"
40123
+ };
40124
+ } catch {
40125
+ return null;
40126
+ }
40127
+ }
40128
+ const mime = phoneImageMime(relative);
40129
+ if (mime) {
40130
+ const dataUrl = phoneImageDataUrl(worktreePath, relative);
40131
+ if (!dataUrl) return null;
40132
+ return {
40133
+ content: dataUrl,
40134
+ encoding: "base64",
40135
+ binary: true,
40136
+ directory: false,
40137
+ truncated: false,
40138
+ mime
40139
+ };
40140
+ }
40141
+ try {
40142
+ const file = readWorktreeFile(worktreePath, relative, { maxBytes: PHONE_TEXT_MAX_BYTES });
40143
+ return {
40144
+ content: file.content,
40145
+ encoding: file.encoding,
40146
+ binary: file.binary,
40147
+ directory: false,
40148
+ truncated: file.truncated
40149
+ };
40150
+ } catch {
40151
+ return null;
40152
+ }
40153
+ }
40064
40154
 
40065
40155
  // src/remote/phone-artifact.ts
40066
40156
  var FENCE_RE = /```([a-zA-Z0-9_+-]*)[^\n]*\n([\s\S]*?)(?:```|$)/g;
@@ -40197,6 +40287,281 @@ function parseOpenArtifact(value, chatId) {
40197
40287
  const hint = oneLine(str6(record.hint), 200);
40198
40288
  return { op: "open-artifact", chatId, title, ...hint ? { hint } : {} };
40199
40289
  }
40290
+ var PHONE_ARTIFACT_MAX_CHARS = 18e4;
40291
+ function phoneArtifactDocuments(sources) {
40292
+ const docs = [];
40293
+ for (const source of sources) {
40294
+ const hints = /* @__PURE__ */ new Set();
40295
+ for (const match of source.text.matchAll(new RegExp(FENCE_RE.source, "g"))) {
40296
+ const language = match[1] ?? "";
40297
+ const content = match[2] ?? "";
40298
+ const kind = kindFor(language, content);
40299
+ if (!kind) continue;
40300
+ const hint = artifactHint(content);
40301
+ if (hint) hints.add(hint);
40302
+ docs.push({ title: titleFor(kind, content), content, kind });
40303
+ }
40304
+ for (const part of source.parts ?? []) {
40305
+ if (part.type !== "tool" || part.parentId || !isArtifactTool(part.name)) continue;
40306
+ const input = asRecord14(part.input);
40307
+ const content = toolContent(input);
40308
+ const hint = artifactHint(content);
40309
+ if (hint && hints.has(hint)) continue;
40310
+ const typed = kindFor(str6(input?.type), content) ?? kindFor("", content);
40311
+ const title = oneLine(str6(input?.title), 80) || (typed ? titleFor(typed, content) : "Artifact");
40312
+ if (!str6(input?.title) && content.trim().length < MIN_FENCE_CHARS) continue;
40313
+ if (hint) hints.add(hint);
40314
+ docs.push({ title, content, kind: typed ?? "text" });
40315
+ }
40316
+ }
40317
+ return docs;
40318
+ }
40319
+ function phoneArtifactReply(docs, request) {
40320
+ const match = pickArtifactMatch(docs, request);
40321
+ if (!match) return null;
40322
+ const truncated = match.content.length > PHONE_ARTIFACT_MAX_CHARS;
40323
+ return {
40324
+ title: match.title,
40325
+ kind: match.kind,
40326
+ content: truncated ? match.content.slice(0, PHONE_ARTIFACT_MAX_CHARS) : match.content,
40327
+ truncated
40328
+ };
40329
+ }
40330
+ function newest(items) {
40331
+ return items.length ? items[items.length - 1] ?? null : null;
40332
+ }
40333
+ function pickArtifactMatch(items, request) {
40334
+ const hint = request.hint?.trim();
40335
+ const title = request.title.trim();
40336
+ if (hint) {
40337
+ const exact = items.filter((item) => item.content && artifactHint(item.content) === hint);
40338
+ const exactTitled = title ? exact.filter((item) => item.title === title) : [];
40339
+ const exactMatch = newest(exactTitled.length ? exactTitled : exact);
40340
+ if (exactMatch) return exactMatch;
40341
+ if (title) {
40342
+ for (let i = items.length - 1; i >= 0; i--) {
40343
+ const item = items[i];
40344
+ if (!item?.content || item.title !== title) continue;
40345
+ if (normalizedBody(item.content).startsWith(hint)) return item;
40346
+ }
40347
+ }
40348
+ }
40349
+ if (!title) return null;
40350
+ for (let i = items.length - 1; i >= 0; i--) {
40351
+ if (items[i]?.title === title) return items[i] ?? null;
40352
+ }
40353
+ return null;
40354
+ }
40355
+
40356
+ // src/remote/phone-workspace-ops.ts
40357
+ init_orchestrator();
40358
+
40359
+ // src/remote/phone-chats.ts
40360
+ var import_node_crypto26 = require("crypto");
40361
+ init_list_models();
40362
+ init_home_board();
40363
+ init_stage_files();
40364
+ init_worktree_labels();
40365
+ init_thinking_effort();
40366
+ init_app_settings();
40367
+ init_global_workspace();
40368
+ init_thread_store();
40369
+ init_workspaces();
40370
+ init_chat_tabs();
40371
+ init_create();
40372
+ init_orchestrator();
40373
+
40374
+ // src/remote/phone-header.ts
40375
+ init_orchestrator();
40376
+ init_global_workspace();
40377
+ var PHONE_HEADER_GIT_ACTIONS = [
40378
+ "commit-push",
40379
+ "create-draft",
40380
+ "resolve-conflicts",
40381
+ "ready-for-review",
40382
+ "merge"
40383
+ ];
40384
+ var HIDDEN = {
40385
+ visible: false,
40386
+ branch: "",
40387
+ additions: 0,
40388
+ deletions: 0,
40389
+ unpushed: 0,
40390
+ dirty: false,
40391
+ ready: false,
40392
+ title: ""
40393
+ };
40394
+ function phoneHeaderFrom(thread, diff) {
40395
+ if (isGlobalThread(thread)) return HIDDEN;
40396
+ const branch = thread.branchName.trim().replace(/^thread\//, "");
40397
+ if (!branch) return HIDDEN;
40398
+ const ready = diff != null;
40399
+ const uncommitted = diff?.scopeStats?.uncommitted;
40400
+ const additions = ready ? Math.max(0, Math.trunc(uncommitted?.additions ?? 0)) : 0;
40401
+ const deletions = ready ? Math.max(0, Math.trunc(uncommitted?.deletions ?? 0)) : 0;
40402
+ const unpushed = ready ? Math.max(0, Math.trunc(diff?.unpushed ?? 0)) : 0;
40403
+ const dirty = ready && (Boolean(diff?.dirty) || additions > 0 || deletions > 0);
40404
+ const pr = prSnapshot(thread);
40405
+ const action = ready ? headerAction(pr, { dirty: Boolean(diff?.dirty), unpushed }) : null;
40406
+ const lines = [branch];
40407
+ if (!ready) lines.push("Loading git status\u2026");
40408
+ else if (dirty) lines.push(`Uncommitted +${additions} \u2212${deletions}`);
40409
+ else lines.push("Working tree clean");
40410
+ if (unpushed > 0) lines.push(`${unpushed} unpushed commit${unpushed === 1 ? "" : "s"}`);
40411
+ return {
40412
+ visible: true,
40413
+ branch,
40414
+ additions,
40415
+ deletions,
40416
+ unpushed,
40417
+ dirty,
40418
+ ready,
40419
+ title: lines.join("\n"),
40420
+ ...pr.url ? { prUrl: pr.url } : {},
40421
+ ...pr.number ? { prNumber: pr.number } : {},
40422
+ ...pr.status ? { prStatus: pr.status } : {},
40423
+ ...action ? { action: action.id, actionLabel: action.label } : {}
40424
+ };
40425
+ }
40426
+ function phonePrPill(thread) {
40427
+ const pr = prSnapshot(thread);
40428
+ if (!pr.url) return null;
40429
+ return { url: pr.url, number: pr.number, status: pr.status };
40430
+ }
40431
+ async function readPhoneHeader(thread) {
40432
+ const pending = phoneHeaderFrom(thread, null);
40433
+ if (!pending.visible) return pending;
40434
+ try {
40435
+ const diff = await getOrchestrator().diff(thread.id, { includePatches: false });
40436
+ return phoneHeaderFrom(thread, diff);
40437
+ } catch {
40438
+ return pending;
40439
+ }
40440
+ }
40441
+ function prSnapshot(thread) {
40442
+ const url = thread.prUrl?.trim() || "";
40443
+ const number = url.match(/\/pull\/(\d+)/)?.[1] ?? "";
40444
+ const state = (thread.prState ?? "").toUpperCase();
40445
+ const merged = state === "MERGED";
40446
+ const closed = state === "CLOSED";
40447
+ const open = Boolean(url) && !merged && !closed;
40448
+ const draft = open && Boolean(thread.prIsDraft);
40449
+ const inMergeQueue = open && Boolean(thread.prIsInMergeQueue);
40450
+ const mergeable = (thread.prMergeable ?? "").toUpperCase();
40451
+ const mergeState = (thread.prMergeStateStatus ?? "").toUpperCase();
40452
+ const mergeConflicts = open && !inMergeQueue && (mergeable === "CONFLICTING" || mergeState === "DIRTY");
40453
+ const branchBehind = open && !inMergeQueue && !mergeConflicts && mergeState === "BEHIND";
40454
+ const checksFailed = Boolean(thread.prChecksFailed);
40455
+ const review = (open && !draft ? thread.prReviewDecision : "") ?? "";
40456
+ return {
40457
+ url,
40458
+ number,
40459
+ status: pillStatus({ url, merged, closed, draft, inMergeQueue, mergeConflicts, branchBehind, checksFailed, review }),
40460
+ merged,
40461
+ closed,
40462
+ open,
40463
+ draft,
40464
+ cowboy: Boolean(thread.cowboy),
40465
+ inMergeQueue,
40466
+ mergeConflicts,
40467
+ branchBehind,
40468
+ checksFailed,
40469
+ review: review.toUpperCase()
40470
+ };
40471
+ }
40472
+ function pillStatus(pr) {
40473
+ if (!pr.url) return "";
40474
+ if (pr.merged) return "Merged";
40475
+ if (pr.closed) return "Closed";
40476
+ if (pr.inMergeQueue) return "Queued";
40477
+ if (pr.mergeConflicts) return "Merge conflicts";
40478
+ if (pr.branchBehind) return "Behind base";
40479
+ if (pr.draft) return "Draft";
40480
+ if (pr.checksFailed) return "Checks failing";
40481
+ if (pr.review === "CHANGES_REQUESTED") return "Changes requested";
40482
+ if (pr.review !== "APPROVED") return "Needs approval";
40483
+ return "Approved";
40484
+ }
40485
+ function headerAction(pr, diff) {
40486
+ const hasLocalChanges = diff.dirty || diff.unpushed > 0;
40487
+ const originInSync = !hasLocalChanges;
40488
+ if (pr.merged) return null;
40489
+ if (pr.closed) return { id: "open-pr", label: "Closed" };
40490
+ if (pr.cowboy) {
40491
+ return hasLocalChanges ? { id: "commit-push", label: "Commit & push" } : { id: "commit-push", label: "Push" };
40492
+ }
40493
+ if (!pr.url) return { id: "create-draft", label: "Create PR" };
40494
+ if (pr.inMergeQueue) return { id: "open-pr", label: "Queued" };
40495
+ if (pr.mergeConflicts) return { id: "resolve-conflicts", label: "Resolve" };
40496
+ if (pr.branchBehind) return { id: "resolve-conflicts", label: "Update" };
40497
+ if (hasLocalChanges) return { id: "commit-push", label: "Commit & push" };
40498
+ if (pr.draft) {
40499
+ return originInSync ? { id: "ready-for-review", label: "Ready for review" } : { id: "commit-push", label: "Commit & push" };
40500
+ }
40501
+ if (pr.checksFailed) return { id: "open-checks", label: "Checks failing" };
40502
+ if (pr.review === "CHANGES_REQUESTED") return { id: "open-pr", label: "Changes requested" };
40503
+ if (pr.review !== "APPROVED") return { id: "open-pr", label: "Needs approval" };
40504
+ return { id: "merge", label: "Merge" };
40505
+ }
40506
+
40507
+ // src/remote/phone-panes.ts
40508
+ init_orchestrator();
40509
+ var MAX_ROWS = 200;
40510
+ function parsePhonePane(value, chatId) {
40511
+ const pane = value && typeof value === "object" ? value.pane : void 0;
40512
+ if (pane !== "files" && pane !== "changes" && pane !== "checks") return "invalid";
40513
+ const scopeRaw = value.scope;
40514
+ const scope = scopeRaw === "uncommitted" || scopeRaw === "commits" ? scopeRaw : void 0;
40515
+ return { op: "pane", chatId, pane, ...scope && pane === "changes" ? { scope } : {} };
40516
+ }
40517
+ async function readPhonePane(thread, pane, scope) {
40518
+ try {
40519
+ if (pane === "files") return await readFiles(thread.id);
40520
+ if (pane === "changes") return await readChanges(thread.id, scope ?? "commits");
40521
+ return await readChecks(thread.id);
40522
+ } catch (err) {
40523
+ const message = err instanceof Error ? err.message : String(err);
40524
+ return { rows: [], note: message };
40525
+ }
40526
+ }
40527
+ async function readFiles(threadId) {
40528
+ const paths = await getOrchestrator().listFiles(threadId);
40529
+ const shown = paths.slice(0, MAX_ROWS);
40530
+ return {
40531
+ rows: shown.map((path2) => ({ label: path2, path: path2 })),
40532
+ ...paths.length > shown.length ? { note: `${paths.length - shown.length} more files on the Mac` } : {}
40533
+ };
40534
+ }
40535
+ async function readChanges(threadId, scope) {
40536
+ const diff = await getOrchestrator().diff(threadId, { includePatches: false, scope });
40537
+ const shown = diff.files.slice(0, MAX_ROWS);
40538
+ return {
40539
+ rows: shown.map((file) => ({
40540
+ label: file.path,
40541
+ path: file.path,
40542
+ detail: changeDetail(file.additions, file.deletions)
40543
+ })),
40544
+ ...diff.files.length > shown.length ? { note: `${diff.files.length - shown.length} more changed files` } : {}
40545
+ };
40546
+ }
40547
+ function changeDetail(additions, deletions) {
40548
+ const add = additions ?? 0;
40549
+ const del = deletions ?? 0;
40550
+ if (add === 0 && del === 0) return void 0;
40551
+ return `+${add} \u2212${del}`;
40552
+ }
40553
+ async function readChecks(threadId) {
40554
+ const checks = await getOrchestrator().getPrChecks(threadId);
40555
+ if (!checks) return { rows: [], note: "No pull request on this branch." };
40556
+ const shown = checks.slice(0, MAX_ROWS);
40557
+ return {
40558
+ rows: shown.map((check) => ({
40559
+ label: check.name,
40560
+ detail: check.bucket || check.state,
40561
+ ...check.link ? { href: check.link } : {}
40562
+ }))
40563
+ };
40564
+ }
40200
40565
 
40201
40566
  // src/remote/phone-defaults.ts
40202
40567
  init_app_settings();
@@ -40215,6 +40580,29 @@ function phoneAccountDefaults() {
40215
40580
  };
40216
40581
  }
40217
40582
 
40583
+ // src/remote/phone-viewer-login.ts
40584
+ var TTL_MS = 5 * 6e4;
40585
+ var cached = "";
40586
+ var fetchedAt = 0;
40587
+ var inflight3 = null;
40588
+ function phoneViewerLogin() {
40589
+ return cached;
40590
+ }
40591
+ function refreshPhoneViewerLogin(force = false) {
40592
+ const now = Date.now();
40593
+ if (!force && fetchedAt && now - fetchedAt < TTL_MS) return Promise.resolve(cached);
40594
+ if (inflight3) return inflight3;
40595
+ const run2 = getGitHubStatus().then((status) => {
40596
+ cached = status.login?.trim() ?? "";
40597
+ fetchedAt = Date.now();
40598
+ return cached;
40599
+ }).catch(() => cached).finally(() => {
40600
+ if (inflight3 === run2) inflight3 = null;
40601
+ });
40602
+ inflight3 = run2;
40603
+ return run2;
40604
+ }
40605
+
40218
40606
  // src/remote/phone-transcript.ts
40219
40607
  init_turn_live();
40220
40608
 
@@ -40279,10 +40667,63 @@ function listPhoneMessages(thread) {
40279
40667
  return live.slice(-TRANSCRIPT_LIMIT);
40280
40668
  }
40281
40669
 
40282
- // src/remote/phone-chats.ts
40283
- var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
40670
+ // src/remote/phone-history.ts
40671
+ init_global_workspace();
40672
+ init_thread_store();
40284
40673
  var PREVIEW_LIMIT = 90;
40285
40674
  var HISTORY_PAGE_SIZE = 40;
40675
+ function previewOf(thread) {
40676
+ const messages = listPhoneMessages(thread);
40677
+ const last = messages[messages.length - 1];
40678
+ if (!last) return "";
40679
+ return clip(last.text.replace(/\s+/g, " ").trim(), PREVIEW_LIMIT);
40680
+ }
40681
+ function repoName(repoPath) {
40682
+ const parts = repoPath.replace(/\/$/, "").split("/");
40683
+ return parts[parts.length - 1] || repoPath;
40684
+ }
40685
+ function isProjectPath(path2) {
40686
+ if (!path2 || path2 === "/" || path2 === ".") return false;
40687
+ return !isGlobalThread({ repoPath: path2 });
40688
+ }
40689
+ function historyStamp(thread) {
40690
+ return thread.archivedAt || thread.updatedAt;
40691
+ }
40692
+ function historyCursor(chat) {
40693
+ return `${chat.updatedAt} ${chat.id}`;
40694
+ }
40695
+ function listPhoneHistory(opts) {
40696
+ const query = opts?.query?.trim().toLowerCase() ?? "";
40697
+ const after = opts?.after?.trim() ?? "";
40698
+ const all = listThreads({ includeArchived: true }).filter((thread) => thread.status === "archived").filter((thread) => isGlobalThread(thread) || isProjectPath(thread.repoPath)).sort((a, b) => historyStamp(a) < historyStamp(b) ? 1 : historyStamp(a) > historyStamp(b) ? -1 : 0).map((thread) => ({
40699
+ id: thread.id,
40700
+ title: thread.title?.trim() || "Untitled",
40701
+ preview: previewOf(thread),
40702
+ updatedAt: historyStamp(thread),
40703
+ where: isGlobalThread(thread) ? "Orchestration" : repoName(thread.repoPath),
40704
+ agent: thread.agent
40705
+ })).filter((chat) => {
40706
+ if (!query) return true;
40707
+ return `${chat.title} ${chat.where} ${chat.agent} ${chat.preview}`.toLowerCase().includes(query);
40708
+ });
40709
+ let start = 0;
40710
+ if (after) {
40711
+ const index = all.findIndex((chat) => historyCursor(chat) === after);
40712
+ start = index >= 0 ? index + 1 : all.length;
40713
+ }
40714
+ const chats = all.slice(start, start + HISTORY_PAGE_SIZE);
40715
+ const last = chats.at(-1);
40716
+ const more = start + chats.length < all.length;
40717
+ return {
40718
+ chats,
40719
+ total: all.length,
40720
+ ...more && last ? { next: historyCursor(last) } : {}
40721
+ };
40722
+ }
40723
+
40724
+ // src/remote/phone-chats.ts
40725
+ var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
40726
+ var PREVIEW_LIMIT2 = 90;
40286
40727
  var AGENT_KINDS = /* @__PURE__ */ new Set(["claude", "codex", "opencode", "brightsy", "cursor"]);
40287
40728
  var MAX_PHONE_FILES = 8;
40288
40729
  var MAX_PHONE_FILE_CHARS = Math.ceil(8 * 1024 * 1024 * 4 / 3);
@@ -40312,7 +40753,7 @@ function parsePhoneControl(value) {
40312
40753
  }
40313
40754
  if (op === "sources") {
40314
40755
  const repoPath = textField(value, "repoPath");
40315
- if (!repoPath || !isProjectPath(repoPath)) return "invalid";
40756
+ if (!repoPath || !isProjectPath2(repoPath)) return "invalid";
40316
40757
  const query = textField(value, "query");
40317
40758
  return { op: "sources", repoPath, ...query ? { query } : {} };
40318
40759
  }
@@ -40330,7 +40771,15 @@ function parsePhoneControl(value) {
40330
40771
  if (op === "create") return parsePhoneCreate(value);
40331
40772
  const chatId = typeof value.chatId === "string" ? value.chatId.trim() : "";
40332
40773
  if (!chatId) return "invalid";
40333
- if (op === "open" || op === "archive" || op === "restore" || op === "stop") return { op, chatId };
40774
+ if (op === "open" || op === "archive" || op === "restore" || op === "stop" || op === "review" || op === "header") {
40775
+ return { op, chatId };
40776
+ }
40777
+ if (op === "git") {
40778
+ const action = textField(value, "action");
40779
+ if (!PHONE_HEADER_GIT_ACTIONS.includes(action)) return "invalid";
40780
+ return { op: "git", chatId, action };
40781
+ }
40782
+ if (op === "pane") return parsePhonePane(value, chatId);
40334
40783
  if (op === "open-artifact") return parseOpenArtifact(value, chatId);
40335
40784
  if (op === "open-file" || op === "media") {
40336
40785
  const path2 = textField(value, "path");
@@ -40470,7 +40919,7 @@ function parsePhoneCreate(value) {
40470
40919
  }
40471
40920
  if (where === "project") {
40472
40921
  const repoPath = textField(value, "repoPath");
40473
- if (!repoPath || !isProjectPath(repoPath)) return "invalid";
40922
+ if (!repoPath || !isProjectPath2(repoPath)) return "invalid";
40474
40923
  const sourceRaw = textField(value, "sourceType");
40475
40924
  let sourceType;
40476
40925
  if (sourceRaw === "branch" || sourceRaw === "pr" || sourceRaw === "ticket") sourceType = sourceRaw;
@@ -40493,94 +40942,70 @@ function parsePhoneCreate(value) {
40493
40942
  return "invalid";
40494
40943
  }
40495
40944
  function summarize(thread) {
40945
+ const pr = phonePrPill(thread);
40496
40946
  return {
40497
40947
  id: thread.id,
40498
40948
  title: thread.title?.trim() || "Untitled",
40499
40949
  status: thread.status,
40500
- preview: previewOf(thread),
40950
+ preview: previewOf2(thread),
40501
40951
  updatedAt: thread.updatedAt,
40502
- agent: thread.agent
40952
+ agent: thread.agent,
40953
+ ...pr?.url ? { prUrl: pr.url } : {},
40954
+ ...pr?.number ? { prNumber: pr.number } : {},
40955
+ ...pr?.status ? { prStatus: pr.status } : {}
40503
40956
  };
40504
40957
  }
40505
- function previewOf(thread) {
40958
+ function previewOf2(thread) {
40506
40959
  const messages = listPhoneMessages(thread);
40507
40960
  const last = messages[messages.length - 1];
40508
40961
  if (!last) return "";
40509
- return clip(last.text.replace(/\s+/g, " ").trim(), PREVIEW_LIMIT);
40962
+ return clip(last.text.replace(/\s+/g, " ").trim(), PREVIEW_LIMIT2);
40510
40963
  }
40511
- function repoName(repoPath) {
40964
+ function repoName2(repoPath) {
40512
40965
  const parts = repoPath.replace(/\/$/, "").split("/");
40513
40966
  return parts[parts.length - 1] || repoPath;
40514
40967
  }
40515
- function isProjectPath(path2) {
40968
+ function isProjectPath2(path2) {
40516
40969
  if (!path2 || path2 === "/" || path2 === ".") return false;
40517
40970
  return !isGlobalThread({ repoPath: path2 });
40518
40971
  }
40519
40972
  function listPhoneChats() {
40520
40973
  return listGlobalThreads().slice().sort((a, b) => a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0).map(summarize);
40521
40974
  }
40522
- function listPhoneSidebar() {
40975
+ function listPhoneSidebar(viewerLogin = phoneViewerLogin()) {
40976
+ const settings = loadAppSettings();
40523
40977
  const threads = listThreads();
40524
40978
  const byRepo = /* @__PURE__ */ new Map();
40525
40979
  for (const thread of threads) {
40526
- if (!isProjectPath(thread.repoPath) || isRemovedWorkspace(thread.repoPath)) continue;
40980
+ if (!isProjectPath2(thread.repoPath) || isRemovedWorkspace(thread.repoPath)) continue;
40527
40981
  const list = byRepo.get(thread.repoPath) ?? [];
40528
40982
  list.push(thread);
40529
40983
  byRepo.set(thread.repoPath, list);
40530
40984
  }
40531
40985
  for (const workspace of listWorkspaces()) {
40532
- if (!isProjectPath(workspace.path) || byRepo.has(workspace.path)) continue;
40986
+ if (!isProjectPath2(workspace.path) || byRepo.has(workspace.path)) continue;
40533
40987
  byRepo.set(workspace.path, []);
40534
40988
  }
40535
- const projects = [...byRepo.entries()].sort(([a], [b]) => repoName(a).localeCompare(repoName(b))).map(([path2, repoThreads]) => ({
40536
- name: repoName(path2),
40989
+ const projects = [...byRepo.entries()].sort(([a], [b]) => repoName2(a).localeCompare(repoName2(b))).map(([path2, repoThreads]) => ({
40990
+ name: repoName2(path2),
40537
40991
  path: path2,
40538
40992
  worktrees: groupHomeBoardWorktrees(repoThreads).map((group) => {
40539
40993
  const tags = workspaceTagsFromGroup(group);
40994
+ const badge = classifyWorktreeListBadges(
40995
+ group,
40996
+ viewerLogin,
40997
+ resolveProjectReviewLabel(settings, path2)
40998
+ )[0];
40540
40999
  return {
40541
41000
  label: worktreeDisplayLabelForGroup(group),
40542
41001
  chats: group.map(summarize),
40543
- ...tags.length > 0 ? { tags } : {}
41002
+ ...tags.length > 0 ? { tags } : {},
41003
+ ...badge ? { badge: { id: badge.id, label: badge.label, title: badge.title } } : {}
40544
41004
  };
40545
41005
  })
40546
41006
  }));
40547
41007
  return { orchestration: listPhoneChats(), projects, defaults: phoneAccountDefaults() };
40548
41008
  }
40549
- function historyStamp(thread) {
40550
- return thread.archivedAt || thread.updatedAt;
40551
- }
40552
- function historyCursor(chat) {
40553
- return `${chat.updatedAt} ${chat.id}`;
40554
- }
40555
- function historyQueryMatch(chat, query) {
40556
- if (!query) return true;
40557
- return `${chat.title} ${chat.where} ${chat.agent} ${chat.preview}`.toLowerCase().includes(query);
40558
- }
40559
- function listPhoneHistory(opts) {
40560
- const query = opts?.query?.trim().toLowerCase() ?? "";
40561
- const after = opts?.after?.trim() ?? "";
40562
- const all = listThreads({ includeArchived: true }).filter((thread) => thread.status === "archived").filter((thread) => isGlobalThread(thread) || isProjectPath(thread.repoPath)).sort((a, b) => historyStamp(a) < historyStamp(b) ? 1 : historyStamp(a) > historyStamp(b) ? -1 : 0).map((thread) => ({
40563
- id: thread.id,
40564
- title: thread.title?.trim() || "Untitled",
40565
- preview: previewOf(thread),
40566
- updatedAt: historyStamp(thread),
40567
- where: isGlobalThread(thread) ? "Orchestration" : repoName(thread.repoPath),
40568
- agent: thread.agent
40569
- })).filter((chat) => historyQueryMatch(chat, query));
40570
- let start = 0;
40571
- if (after) {
40572
- const index = all.findIndex((chat) => historyCursor(chat) === after);
40573
- start = index >= 0 ? index + 1 : all.length;
40574
- }
40575
- const chats = all.slice(start, start + HISTORY_PAGE_SIZE);
40576
- const last = chats.at(-1);
40577
- const more = start + chats.length < all.length;
40578
- return {
40579
- chats,
40580
- total: all.length,
40581
- ...more && last ? { next: historyCursor(last) } : {}
40582
- };
40583
- }
40584
41009
  function matchPhoneThread(chatId) {
40585
41010
  const id = chatId.trim();
40586
41011
  if (!id) return null;
@@ -40593,7 +41018,7 @@ function matchPhoneThread(chatId) {
40593
41018
  }
40594
41019
  function visiblePhoneThread(thread) {
40595
41020
  if (isGlobalThread(thread)) return true;
40596
- return isProjectPath(thread.repoPath);
41021
+ return isProjectPath2(thread.repoPath);
40597
41022
  }
40598
41023
  function findPhoneChat(chatId) {
40599
41024
  const thread = matchPhoneThread(chatId);
@@ -40659,7 +41084,7 @@ function projectPlace(thread) {
40659
41084
  }
40660
41085
  function assertKnownProject(repoPath) {
40661
41086
  const path2 = repoPath.trim();
40662
- if (!isProjectPath(path2)) throw new Error("Pick a project on this Mac.");
41087
+ if (!isProjectPath2(path2)) throw new Error("Pick a project on this Mac.");
40663
41088
  const known = listWorkspaces().some((workspace) => sameRepoPath3(workspace.path, path2)) || listThreads().some(
40664
41089
  (thread) => thread.status !== "archived" && sameRepoPath3(thread.repoPath, path2)
40665
41090
  );
@@ -40762,6 +41187,55 @@ function archivePhoneChat(chatId) {
40762
41187
  return true;
40763
41188
  }
40764
41189
 
41190
+ // src/remote/phone-workspace-ops.ts
41191
+ function dispatchPhoneWorkspace(cmd, emit, publishOpened) {
41192
+ if (cmd.op === "pane") {
41193
+ const thread = readPhoneThread(cmd.chatId);
41194
+ if (!thread) {
41195
+ emit({ op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
41196
+ return;
41197
+ }
41198
+ void readPhonePane(thread, cmd.pane, cmd.scope).then((body) => {
41199
+ emit({
41200
+ op: "pane",
41201
+ chatId: cmd.chatId,
41202
+ pane: cmd.pane,
41203
+ rows: body.rows,
41204
+ ...body.note ? { note: body.note } : {}
41205
+ });
41206
+ });
41207
+ return;
41208
+ }
41209
+ if (cmd.op === "header") {
41210
+ const thread = readPhoneThread(cmd.chatId);
41211
+ if (!thread) {
41212
+ emit({ op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
41213
+ return;
41214
+ }
41215
+ void readPhoneHeader(thread).then((header) => {
41216
+ emit({ op: "header", chatId: cmd.chatId, ...header });
41217
+ });
41218
+ return;
41219
+ }
41220
+ if (cmd.op === "git") {
41221
+ void getOrchestrator().askGit(cmd.chatId, cmd.action).then((tab) => publishOpened(tab.id)).catch((err) => {
41222
+ emit({
41223
+ op: "error",
41224
+ chatId: cmd.chatId,
41225
+ message: err instanceof Error ? err.message : String(err)
41226
+ });
41227
+ });
41228
+ return;
41229
+ }
41230
+ void getOrchestrator().requestReview(cmd.chatId).then((tab) => publishOpened(tab.id)).catch((err) => {
41231
+ emit({
41232
+ op: "error",
41233
+ chatId: cmd.chatId,
41234
+ message: err instanceof Error ? err.message : String(err)
41235
+ });
41236
+ });
41237
+ }
41238
+
40765
41239
  // src/remote/phone-sources.ts
40766
41240
  init_worktree();
40767
41241
  function sourceWarning(label, err) {
@@ -41338,10 +41812,25 @@ function handlePhoneControl(cmd, opts) {
41338
41812
  emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
41339
41813
  return;
41340
41814
  }
41341
- opts.onOpenWorktreeFile?.({
41342
- threadId: thread.id,
41815
+ const preview = phoneFilePreview(thread.worktreePath, relative, kind === "dir" ? "dir" : "file");
41816
+ if (!preview) {
41817
+ emitControl(opts, {
41818
+ op: "error",
41819
+ chatId: cmd.chatId,
41820
+ message: "That file could not be opened on the phone."
41821
+ });
41822
+ return;
41823
+ }
41824
+ emitControl(opts, {
41825
+ op: "file",
41826
+ chatId: cmd.chatId,
41343
41827
  path: relative,
41344
- ...kind === "dir" ? { directory: true } : {},
41828
+ content: preview.content,
41829
+ encoding: preview.encoding,
41830
+ ...preview.binary ? { binary: true } : {},
41831
+ ...preview.directory ? { directory: true } : {},
41832
+ ...preview.truncated ? { truncated: true } : {},
41833
+ ...preview.mime ? { mime: preview.mime } : {},
41345
41834
  ...cmd.startLine != null ? { startLine: cmd.startLine } : {},
41346
41835
  ...cmd.endLine != null ? { endLine: cmd.endLine } : {}
41347
41836
  });
@@ -41356,13 +41845,34 @@ function handlePhoneControl(cmd, opts) {
41356
41845
  emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
41357
41846
  return;
41358
41847
  }
41359
- opts.onOpenArtifact?.({
41360
- threadId: thread.id,
41361
- title: cmd.title,
41362
- ...cmd.hint ? { hint: cmd.hint } : {}
41848
+ const liveParts = readTurnLiveParts(thread.id);
41849
+ const docs = phoneArtifactDocuments([
41850
+ ...(thread.messages ?? []).filter((message) => message.role === "agent").map((message) => ({ text: message.text.trim(), parts: message.parts })),
41851
+ { text: visibleAssistantText(void 0, liveParts), parts: liveParts }
41852
+ ]);
41853
+ const body = phoneArtifactReply(docs, { title: cmd.title, ...cmd.hint ? { hint: cmd.hint } : {} });
41854
+ if (!body) {
41855
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That artifact is not in this chat." });
41856
+ return;
41857
+ }
41858
+ emitControl(opts, {
41859
+ op: "artifact",
41860
+ chatId: cmd.chatId,
41861
+ title: body.title,
41862
+ kind: body.kind,
41863
+ content: body.content,
41864
+ ...body.truncated ? { truncated: true } : {}
41363
41865
  });
41364
41866
  return;
41365
41867
  }
41868
+ if (cmd.op === "pane" || cmd.op === "header" || cmd.op === "git" || cmd.op === "review") {
41869
+ dispatchPhoneWorkspace(
41870
+ cmd,
41871
+ (reply) => emitControl(opts, reply),
41872
+ (chatId2) => publishOpenedChat(opts, chatId2)
41873
+ );
41874
+ return;
41875
+ }
41366
41876
  if (cmd.op === "stop") {
41367
41877
  if (!readPhoneThread(cmd.chatId)) {
41368
41878
  emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
@@ -41441,7 +41951,7 @@ function handleRemoteInbound(text7, opts) {
41441
41951
  }
41442
41952
 
41443
41953
  // src/remote/phone-board-sync.ts
41444
- var import_node_path69 = require("path");
41954
+ var import_node_path70 = require("path");
41445
41955
  var import_chokidar = require("chokidar");
41446
41956
  init_orchestrator();
41447
41957
  init_paths();
@@ -41453,7 +41963,9 @@ function chatKey(row2) {
41453
41963
  function phoneBoardKey(sidebar) {
41454
41964
  const orchestration = sidebar.orchestration.map(chatKey).join("\n");
41455
41965
  const projects = sidebar.projects.map((project) => {
41456
- const trees = project.worktrees.map((tree) => `${tree.label} ${(tree.tags ?? []).join(",")} ${tree.chats.map(chatKey).join("\n")}`).join("\n");
41966
+ const trees = project.worktrees.map(
41967
+ (tree) => `${tree.label} ${tree.badge?.id ?? ""} ${(tree.tags ?? []).join(",")} ${tree.chats.map(chatKey).join("\n")}`
41968
+ ).join("\n");
41457
41969
  return `${project.path} ${project.name}
41458
41970
  ${trees}`;
41459
41971
  }).join("\n---\n");
@@ -41484,7 +41996,7 @@ function subscribePhoneBoard(notify) {
41484
41996
  threads.on("error", () => void 0);
41485
41997
  const data = (0, import_chokidar.watch)(appDataDir(), { ignoreInitial: true, depth: 0 });
41486
41998
  const onData = (changed) => {
41487
- const name = (0, import_node_path69.basename)(changed);
41999
+ const name = (0, import_node_path70.basename)(changed);
41488
42000
  if (name !== "workspaces.json" && name !== "removed-workspaces.json" && name !== "settings.json") return;
41489
42001
  notify();
41490
42002
  };
@@ -41531,10 +42043,23 @@ function startPhoneBoardSync(opts) {
41531
42043
  const unsubscribe = (opts.subscribe ?? subscribePhoneBoard)(schedule);
41532
42044
  const offEvents = (opts.events ?? ((listener) => getOrchestrator().on(listener)))(handle);
41533
42045
  push();
42046
+ let loginTimer = null;
42047
+ const refreshViewerLogin = opts.refreshViewerLogin;
42048
+ if (refreshViewerLogin) {
42049
+ const afterLogin = () => {
42050
+ void refreshViewerLogin().then(() => {
42051
+ if (!stopped) push();
42052
+ });
42053
+ };
42054
+ afterLogin();
42055
+ loginTimer = setInterval(afterLogin, opts.viewerLoginIntervalMs ?? 5 * 6e4);
42056
+ }
41534
42057
  return () => {
41535
42058
  stopped = true;
41536
42059
  if (timer) clearTimeout(timer);
41537
42060
  timer = null;
42061
+ if (loginTimer) clearInterval(loginTimer);
42062
+ loginTimer = null;
41538
42063
  unsubscribe();
41539
42064
  offEvents();
41540
42065
  };
@@ -41601,7 +42126,8 @@ function runRemoteHost(opts) {
41601
42126
  },
41602
42127
  onRunningChat: (chatId) => {
41603
42128
  deliverPhoneTurn({ onOutbound: emit }, chatId);
41604
- }
42129
+ },
42130
+ refreshViewerLogin: () => refreshPhoneViewerLogin(true)
41605
42131
  });
41606
42132
  };
41607
42133
  const send3 = (msg) => {
@@ -41781,14 +42307,14 @@ init_fetch();
41781
42307
 
41782
42308
  // src/relay/accounts.ts
41783
42309
  var import_node_crypto27 = require("crypto");
41784
- var import_node_fs71 = require("fs");
42310
+ var import_node_fs72 = require("fs");
41785
42311
  var import_node_module7 = require("module");
41786
- var import_node_path70 = require("path");
42312
+ var import_node_path71 = require("path");
41787
42313
  var GITHUB_ACCOUNT_ISSUER = "https://github.com";
41788
42314
  var RELAY_DEVICE_CAP = 3;
41789
42315
  function thisModuleFile3() {
41790
42316
  const cjsFile = typeof __filename !== "undefined" ? __filename : "";
41791
- return cjsFile || process.argv[1] || (0, import_node_path70.join)(process.cwd(), "package.json");
42317
+ return cjsFile || process.argv[1] || (0, import_node_path71.join)(process.cwd(), "package.json");
41792
42318
  }
41793
42319
  function openDatabase(filePath) {
41794
42320
  const req = (0, import_node_module7.createRequire)(thisModuleFile3());
@@ -41825,8 +42351,8 @@ function assertRelayAccountsDurable(filePath) {
41825
42351
  if (process.env.SIDEBOARD_RELAY_ACCOUNTS_EPHEMERAL === "1") return;
41826
42352
  if (process.env.NODE_ENV !== "production") return;
41827
42353
  if (process.platform !== "linux") return;
41828
- const dir = (0, import_node_fs71.realpathSync)((0, import_node_path70.dirname)(filePath));
41829
- const mounts = (0, import_node_fs71.readFileSync)("/proc/mounts", "utf8");
42354
+ const dir = (0, import_node_fs72.realpathSync)((0, import_node_path71.dirname)(filePath));
42355
+ const mounts = (0, import_node_fs72.readFileSync)("/proc/mounts", "utf8");
41830
42356
  const point = longestMountPoint(dir, mounts);
41831
42357
  if (point === "/") {
41832
42358
  throw new Error(
@@ -41837,7 +42363,7 @@ function assertRelayAccountsDurable(filePath) {
41837
42363
  var RelayAccountStore = class {
41838
42364
  db;
41839
42365
  constructor(filePath) {
41840
- (0, import_node_fs71.mkdirSync)((0, import_node_path70.dirname)(filePath), { recursive: true });
42366
+ (0, import_node_fs72.mkdirSync)((0, import_node_path71.dirname)(filePath), { recursive: true });
41841
42367
  this.db = openDatabase(filePath);
41842
42368
  this.db.exec(`
41843
42369
  CREATE TABLE IF NOT EXISTS accounts (
@@ -41864,9 +42390,15 @@ var RelayAccountStore = class {
41864
42390
  account_id TEXT NOT NULL,
41865
42391
  host_secret_hash TEXT NOT NULL,
41866
42392
  device_label TEXT NOT NULL,
41867
- created_at INTEGER NOT NULL
42393
+ created_at INTEGER NOT NULL,
42394
+ session_token_hash TEXT
41868
42395
  );
41869
42396
  `);
42397
+ const cols = this.db.prepare("PRAGMA table_info(devices)").all();
42398
+ if (!cols.some((col) => col.name === "session_token_hash")) {
42399
+ this.db.exec("ALTER TABLE devices ADD COLUMN session_token_hash TEXT");
42400
+ }
42401
+ this.db.exec("CREATE INDEX IF NOT EXISTS devices_session ON devices(session_token_hash)");
41870
42402
  }
41871
42403
  close() {
41872
42404
  this.db.close();
@@ -41995,6 +42527,20 @@ var RelayAccountStore = class {
41995
42527
  throw err;
41996
42528
  }
41997
42529
  }
42530
+ /** Store a phone pairing when it is created. A later process looks it up on a memory miss. */
42531
+ savePhoneSession(deviceId, sessionToken) {
42532
+ const id = deviceId.trim();
42533
+ const token = sessionToken.trim();
42534
+ if (!id || !token) return;
42535
+ this.db.prepare("UPDATE devices SET session_token_hash = ? WHERE device_id = ?").run(sha256(token), id);
42536
+ }
42537
+ findPhoneSession(sessionToken) {
42538
+ const token = sessionToken.trim();
42539
+ if (!token) return null;
42540
+ const row2 = this.db.prepare("SELECT device_id, device_label FROM devices WHERE session_token_hash = ?").get(sha256(token));
42541
+ if (!row2) return null;
42542
+ return { deviceId: row2.device_id, deviceLabel: row2.device_label };
42543
+ }
41998
42544
  requireAccount(accountId) {
41999
42545
  const row2 = this.db.prepare("SELECT id FROM accounts WHERE id = ?").get(accountId);
42000
42546
  if (!row2) throw new Error("That Sideboard account is no longer on the relay.");
@@ -42851,9 +43397,9 @@ var import_node_http5 = require("http");
42851
43397
  var import_ws6 = require("ws");
42852
43398
 
42853
43399
  // src/relay/static.ts
42854
- var import_node_fs72 = require("fs");
43400
+ var import_node_fs73 = require("fs");
42855
43401
  var import_promises = require("fs/promises");
42856
- var import_node_path71 = __toESM(require("path"), 1);
43402
+ var import_node_path72 = __toESM(require("path"), 1);
42857
43403
  var TYPES = {
42858
43404
  ".css": "text/css; charset=utf-8",
42859
43405
  ".html": "text/html; charset=utf-8",
@@ -42884,9 +43430,9 @@ function resolveStaticPath(root, requestUrl) {
42884
43430
  return null;
42885
43431
  }
42886
43432
  if (!pathname.startsWith("/") || pathname.includes("\0")) return null;
42887
- const rootResolved = import_node_path71.default.resolve(root);
42888
- const candidate = import_node_path71.default.resolve(rootResolved, `.${pathname}`);
42889
- if (candidate !== rootResolved && !candidate.startsWith(rootResolved + import_node_path71.default.sep)) {
43433
+ const rootResolved = import_node_path72.default.resolve(root);
43434
+ const candidate = import_node_path72.default.resolve(rootResolved, `.${pathname}`);
43435
+ if (candidate !== rootResolved && !candidate.startsWith(rootResolved + import_node_path72.default.sep)) {
42890
43436
  return null;
42891
43437
  }
42892
43438
  return candidate;
@@ -42900,7 +43446,7 @@ async function fileSize(file) {
42900
43446
  }
42901
43447
  }
42902
43448
  function sendFile(req, res, file, size) {
42903
- const ext = import_node_path71.default.extname(file).toLowerCase();
43449
+ const ext = import_node_path72.default.extname(file).toLowerCase();
42904
43450
  res.writeHead(200, {
42905
43451
  "Content-Type": TYPES[ext] ?? "application/octet-stream",
42906
43452
  "Content-Length": size,
@@ -42910,7 +43456,7 @@ function sendFile(req, res, file, size) {
42910
43456
  res.end();
42911
43457
  return true;
42912
43458
  }
42913
- (0, import_node_fs72.createReadStream)(file).pipe(res);
43459
+ (0, import_node_fs73.createReadStream)(file).pipe(res);
42914
43460
  return true;
42915
43461
  }
42916
43462
  async function tryServeStatic(req, res, root) {
@@ -42919,7 +43465,7 @@ async function tryServeStatic(req, res, root) {
42919
43465
  if (!candidate) return false;
42920
43466
  const direct = await fileSize(candidate);
42921
43467
  if (direct != null) return sendFile(req, res, candidate, direct);
42922
- const asIndex = import_node_path71.default.join(candidate, "index.html");
43468
+ const asIndex = import_node_path72.default.join(candidate, "index.html");
42923
43469
  const indexSize = await fileSize(asIndex);
42924
43470
  if (indexSize != null) return sendFile(req, res, asIndex, indexSize);
42925
43471
  return false;
@@ -42985,6 +43531,10 @@ async function startRelayServer(opts) {
42985
43531
  const decision = accountStore.authorizeHost(input);
42986
43532
  return decision.ok ? { ok: true } : { ok: false, message: decision.message };
42987
43533
  } : void 0,
43534
+ phoneSessions: accountStore ? {
43535
+ find: (sessionToken) => accountStore.findPhoneSession(sessionToken),
43536
+ save: (deviceId, sessionToken) => accountStore.savePhoneSession(deviceId, sessionToken)
43537
+ } : void 0,
42988
43538
  onAuthorized: (socket) => authMarks.get(socket)?.()
42989
43539
  });
42990
43540
  const handleCallback = async (reqUrl, res) => {
@@ -43280,8 +43830,8 @@ async function startRelayServer(opts) {
43280
43830
  }
43281
43831
 
43282
43832
  // src/slack/outbound-watch.ts
43283
- var import_node_fs73 = require("fs");
43284
- var import_node_path72 = require("path");
43833
+ var import_node_fs74 = require("fs");
43834
+ var import_node_path73 = require("path");
43285
43835
  init_paths();
43286
43836
  init_private_file();
43287
43837
  init_secure_file();
@@ -43294,7 +43844,7 @@ var POLL_INTERVAL_MS = 12e3;
43294
43844
  var lastPollMs = 0;
43295
43845
  var nameCache = /* @__PURE__ */ new Map();
43296
43846
  function storePath4() {
43297
- return (0, import_node_path72.join)(appDataDir(), "slack-outbound-watch.json");
43847
+ return (0, import_node_path73.join)(appDataDir(), "slack-outbound-watch.json");
43298
43848
  }
43299
43849
  function watchId(teamId, channelId, ts) {
43300
43850
  return `${teamId}:${channelId}:${ts}`;
@@ -43307,9 +43857,9 @@ function tsNewer(a, b) {
43307
43857
  }
43308
43858
  function readStore4() {
43309
43859
  const path2 = storePath4();
43310
- if (!(0, import_node_fs73.existsSync)(path2)) return [];
43860
+ if (!(0, import_node_fs74.existsSync)(path2)) return [];
43311
43861
  try {
43312
- const parsed = isSecureFileEncrypted(path2) ? readSecureJson(path2) : JSON.parse((0, import_node_fs73.readFileSync)(path2, "utf8"));
43862
+ const parsed = isSecureFileEncrypted(path2) ? readSecureJson(path2) : JSON.parse((0, import_node_fs74.readFileSync)(path2, "utf8"));
43313
43863
  return Array.isArray(parsed?.watches) ? parsed.watches : [];
43314
43864
  } catch {
43315
43865
  return [];
@@ -43470,8 +44020,8 @@ function displayName(user) {
43470
44020
  return fromProfile || user.real_name?.trim() || user.name?.trim() || "";
43471
44021
  }
43472
44022
  async function resolveUserName(token, userId, fallback, fetchImpl) {
43473
- const cached = nameCache.get(userId);
43474
- if (cached) return cached;
44023
+ const cached2 = nameCache.get(userId);
44024
+ if (cached2) return cached2;
43475
44025
  try {
43476
44026
  const data = await slackApi(token, "users.info", { user: userId }, fetchImpl);
43477
44027
  const name = displayName(data.user ?? {}) || fallback;