@sideboard-ai/core 0.1.289 → 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.
package/dist/index.cjs CHANGED
@@ -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({
@@ -40026,10 +40027,16 @@ init_stage_files();
40026
40027
  init_orchestrator();
40027
40028
  init_app_settings();
40028
40029
  init_implied_first_prompt();
40030
+ init_message_parts();
40031
+ init_turn_live();
40029
40032
 
40030
40033
  // src/remote/phone-media.ts
40034
+ var import_node_fs71 = require("fs");
40035
+ var import_node_path69 = require("path");
40031
40036
  init_diff();
40032
40037
  var PHONE_IMAGE_MAX_BYTES = 15e5;
40038
+ var PHONE_TEXT_MAX_BYTES = 18e4;
40039
+ var PHONE_DIR_MAX = 200;
40033
40040
  var IMAGE_MIME = {
40034
40041
  png: "image/png",
40035
40042
  jpg: "image/jpeg",
@@ -40076,21 +40083,74 @@ function phoneImageDataUrl(worktreePath, raw) {
40076
40083
  return null;
40077
40084
  }
40078
40085
  }
40079
-
40080
- // src/remote/phone-chats.ts
40081
- var import_node_crypto26 = require("crypto");
40082
- init_list_models();
40083
- init_home_board();
40084
- init_stage_files();
40085
- init_worktree_labels();
40086
- init_thinking_effort();
40087
- init_app_settings();
40088
- init_global_workspace();
40089
- init_thread_store();
40090
- init_workspaces();
40091
- init_chat_tabs();
40092
- init_create();
40093
- 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
+ }
40094
40154
 
40095
40155
  // src/remote/phone-artifact.ts
40096
40156
  var FENCE_RE = /```([a-zA-Z0-9_+-]*)[^\n]*\n([\s\S]*?)(?:```|$)/g;
@@ -40227,6 +40287,281 @@ function parseOpenArtifact(value, chatId) {
40227
40287
  const hint = oneLine(str6(record.hint), 200);
40228
40288
  return { op: "open-artifact", chatId, title, ...hint ? { hint } : {} };
40229
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
+ }
40230
40565
 
40231
40566
  // src/remote/phone-defaults.ts
40232
40567
  init_app_settings();
@@ -40332,10 +40667,63 @@ function listPhoneMessages(thread) {
40332
40667
  return live.slice(-TRANSCRIPT_LIMIT);
40333
40668
  }
40334
40669
 
40335
- // src/remote/phone-chats.ts
40336
- var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
40670
+ // src/remote/phone-history.ts
40671
+ init_global_workspace();
40672
+ init_thread_store();
40337
40673
  var PREVIEW_LIMIT = 90;
40338
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;
40339
40727
  var AGENT_KINDS = /* @__PURE__ */ new Set(["claude", "codex", "opencode", "brightsy", "cursor"]);
40340
40728
  var MAX_PHONE_FILES = 8;
40341
40729
  var MAX_PHONE_FILE_CHARS = Math.ceil(8 * 1024 * 1024 * 4 / 3);
@@ -40365,7 +40753,7 @@ function parsePhoneControl(value) {
40365
40753
  }
40366
40754
  if (op === "sources") {
40367
40755
  const repoPath = textField(value, "repoPath");
40368
- if (!repoPath || !isProjectPath(repoPath)) return "invalid";
40756
+ if (!repoPath || !isProjectPath2(repoPath)) return "invalid";
40369
40757
  const query = textField(value, "query");
40370
40758
  return { op: "sources", repoPath, ...query ? { query } : {} };
40371
40759
  }
@@ -40383,7 +40771,15 @@ function parsePhoneControl(value) {
40383
40771
  if (op === "create") return parsePhoneCreate(value);
40384
40772
  const chatId = typeof value.chatId === "string" ? value.chatId.trim() : "";
40385
40773
  if (!chatId) return "invalid";
40386
- 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);
40387
40783
  if (op === "open-artifact") return parseOpenArtifact(value, chatId);
40388
40784
  if (op === "open-file" || op === "media") {
40389
40785
  const path2 = textField(value, "path");
@@ -40523,7 +40919,7 @@ function parsePhoneCreate(value) {
40523
40919
  }
40524
40920
  if (where === "project") {
40525
40921
  const repoPath = textField(value, "repoPath");
40526
- if (!repoPath || !isProjectPath(repoPath)) return "invalid";
40922
+ if (!repoPath || !isProjectPath2(repoPath)) return "invalid";
40527
40923
  const sourceRaw = textField(value, "sourceType");
40528
40924
  let sourceType;
40529
40925
  if (sourceRaw === "branch" || sourceRaw === "pr" || sourceRaw === "ticket") sourceType = sourceRaw;
@@ -40546,26 +40942,30 @@ function parsePhoneCreate(value) {
40546
40942
  return "invalid";
40547
40943
  }
40548
40944
  function summarize(thread) {
40945
+ const pr = phonePrPill(thread);
40549
40946
  return {
40550
40947
  id: thread.id,
40551
40948
  title: thread.title?.trim() || "Untitled",
40552
40949
  status: thread.status,
40553
- preview: previewOf(thread),
40950
+ preview: previewOf2(thread),
40554
40951
  updatedAt: thread.updatedAt,
40555
- 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 } : {}
40556
40956
  };
40557
40957
  }
40558
- function previewOf(thread) {
40958
+ function previewOf2(thread) {
40559
40959
  const messages = listPhoneMessages(thread);
40560
40960
  const last = messages[messages.length - 1];
40561
40961
  if (!last) return "";
40562
- return clip(last.text.replace(/\s+/g, " ").trim(), PREVIEW_LIMIT);
40962
+ return clip(last.text.replace(/\s+/g, " ").trim(), PREVIEW_LIMIT2);
40563
40963
  }
40564
- function repoName(repoPath) {
40964
+ function repoName2(repoPath) {
40565
40965
  const parts = repoPath.replace(/\/$/, "").split("/");
40566
40966
  return parts[parts.length - 1] || repoPath;
40567
40967
  }
40568
- function isProjectPath(path2) {
40968
+ function isProjectPath2(path2) {
40569
40969
  if (!path2 || path2 === "/" || path2 === ".") return false;
40570
40970
  return !isGlobalThread({ repoPath: path2 });
40571
40971
  }
@@ -40577,17 +40977,17 @@ function listPhoneSidebar(viewerLogin = phoneViewerLogin()) {
40577
40977
  const threads = listThreads();
40578
40978
  const byRepo = /* @__PURE__ */ new Map();
40579
40979
  for (const thread of threads) {
40580
- if (!isProjectPath(thread.repoPath) || isRemovedWorkspace(thread.repoPath)) continue;
40980
+ if (!isProjectPath2(thread.repoPath) || isRemovedWorkspace(thread.repoPath)) continue;
40581
40981
  const list = byRepo.get(thread.repoPath) ?? [];
40582
40982
  list.push(thread);
40583
40983
  byRepo.set(thread.repoPath, list);
40584
40984
  }
40585
40985
  for (const workspace of listWorkspaces()) {
40586
- if (!isProjectPath(workspace.path) || byRepo.has(workspace.path)) continue;
40986
+ if (!isProjectPath2(workspace.path) || byRepo.has(workspace.path)) continue;
40587
40987
  byRepo.set(workspace.path, []);
40588
40988
  }
40589
- const projects = [...byRepo.entries()].sort(([a], [b]) => repoName(a).localeCompare(repoName(b))).map(([path2, repoThreads]) => ({
40590
- name: repoName(path2),
40989
+ const projects = [...byRepo.entries()].sort(([a], [b]) => repoName2(a).localeCompare(repoName2(b))).map(([path2, repoThreads]) => ({
40990
+ name: repoName2(path2),
40591
40991
  path: path2,
40592
40992
  worktrees: groupHomeBoardWorktrees(repoThreads).map((group) => {
40593
40993
  const tags = workspaceTagsFromGroup(group);
@@ -40606,41 +41006,6 @@ function listPhoneSidebar(viewerLogin = phoneViewerLogin()) {
40606
41006
  }));
40607
41007
  return { orchestration: listPhoneChats(), projects, defaults: phoneAccountDefaults() };
40608
41008
  }
40609
- function historyStamp(thread) {
40610
- return thread.archivedAt || thread.updatedAt;
40611
- }
40612
- function historyCursor(chat) {
40613
- return `${chat.updatedAt} ${chat.id}`;
40614
- }
40615
- function historyQueryMatch(chat, query) {
40616
- if (!query) return true;
40617
- return `${chat.title} ${chat.where} ${chat.agent} ${chat.preview}`.toLowerCase().includes(query);
40618
- }
40619
- function listPhoneHistory(opts) {
40620
- const query = opts?.query?.trim().toLowerCase() ?? "";
40621
- const after = opts?.after?.trim() ?? "";
40622
- 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) => ({
40623
- id: thread.id,
40624
- title: thread.title?.trim() || "Untitled",
40625
- preview: previewOf(thread),
40626
- updatedAt: historyStamp(thread),
40627
- where: isGlobalThread(thread) ? "Orchestration" : repoName(thread.repoPath),
40628
- agent: thread.agent
40629
- })).filter((chat) => historyQueryMatch(chat, query));
40630
- let start = 0;
40631
- if (after) {
40632
- const index = all.findIndex((chat) => historyCursor(chat) === after);
40633
- start = index >= 0 ? index + 1 : all.length;
40634
- }
40635
- const chats = all.slice(start, start + HISTORY_PAGE_SIZE);
40636
- const last = chats.at(-1);
40637
- const more = start + chats.length < all.length;
40638
- return {
40639
- chats,
40640
- total: all.length,
40641
- ...more && last ? { next: historyCursor(last) } : {}
40642
- };
40643
- }
40644
41009
  function matchPhoneThread(chatId) {
40645
41010
  const id = chatId.trim();
40646
41011
  if (!id) return null;
@@ -40653,7 +41018,7 @@ function matchPhoneThread(chatId) {
40653
41018
  }
40654
41019
  function visiblePhoneThread(thread) {
40655
41020
  if (isGlobalThread(thread)) return true;
40656
- return isProjectPath(thread.repoPath);
41021
+ return isProjectPath2(thread.repoPath);
40657
41022
  }
40658
41023
  function findPhoneChat(chatId) {
40659
41024
  const thread = matchPhoneThread(chatId);
@@ -40719,7 +41084,7 @@ function projectPlace(thread) {
40719
41084
  }
40720
41085
  function assertKnownProject(repoPath) {
40721
41086
  const path2 = repoPath.trim();
40722
- 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.");
40723
41088
  const known = listWorkspaces().some((workspace) => sameRepoPath3(workspace.path, path2)) || listThreads().some(
40724
41089
  (thread) => thread.status !== "archived" && sameRepoPath3(thread.repoPath, path2)
40725
41090
  );
@@ -40822,6 +41187,55 @@ function archivePhoneChat(chatId) {
40822
41187
  return true;
40823
41188
  }
40824
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
+
40825
41239
  // src/remote/phone-sources.ts
40826
41240
  init_worktree();
40827
41241
  function sourceWarning(label, err) {
@@ -41398,10 +41812,25 @@ function handlePhoneControl(cmd, opts) {
41398
41812
  emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
41399
41813
  return;
41400
41814
  }
41401
- opts.onOpenWorktreeFile?.({
41402
- 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,
41403
41827
  path: relative,
41404
- ...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 } : {},
41405
41834
  ...cmd.startLine != null ? { startLine: cmd.startLine } : {},
41406
41835
  ...cmd.endLine != null ? { endLine: cmd.endLine } : {}
41407
41836
  });
@@ -41416,13 +41845,34 @@ function handlePhoneControl(cmd, opts) {
41416
41845
  emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
41417
41846
  return;
41418
41847
  }
41419
- opts.onOpenArtifact?.({
41420
- threadId: thread.id,
41421
- title: cmd.title,
41422
- ...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 } : {}
41423
41865
  });
41424
41866
  return;
41425
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
+ }
41426
41876
  if (cmd.op === "stop") {
41427
41877
  if (!readPhoneThread(cmd.chatId)) {
41428
41878
  emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
@@ -41501,7 +41951,7 @@ function handleRemoteInbound(text7, opts) {
41501
41951
  }
41502
41952
 
41503
41953
  // src/remote/phone-board-sync.ts
41504
- var import_node_path69 = require("path");
41954
+ var import_node_path70 = require("path");
41505
41955
  var import_chokidar = require("chokidar");
41506
41956
  init_orchestrator();
41507
41957
  init_paths();
@@ -41546,7 +41996,7 @@ function subscribePhoneBoard(notify) {
41546
41996
  threads.on("error", () => void 0);
41547
41997
  const data = (0, import_chokidar.watch)(appDataDir(), { ignoreInitial: true, depth: 0 });
41548
41998
  const onData = (changed) => {
41549
- const name = (0, import_node_path69.basename)(changed);
41999
+ const name = (0, import_node_path70.basename)(changed);
41550
42000
  if (name !== "workspaces.json" && name !== "removed-workspaces.json" && name !== "settings.json") return;
41551
42001
  notify();
41552
42002
  };
@@ -41857,14 +42307,14 @@ init_fetch();
41857
42307
 
41858
42308
  // src/relay/accounts.ts
41859
42309
  var import_node_crypto27 = require("crypto");
41860
- var import_node_fs71 = require("fs");
42310
+ var import_node_fs72 = require("fs");
41861
42311
  var import_node_module7 = require("module");
41862
- var import_node_path70 = require("path");
42312
+ var import_node_path71 = require("path");
41863
42313
  var GITHUB_ACCOUNT_ISSUER = "https://github.com";
41864
42314
  var RELAY_DEVICE_CAP = 3;
41865
42315
  function thisModuleFile3() {
41866
42316
  const cjsFile = typeof __filename !== "undefined" ? __filename : "";
41867
- 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");
41868
42318
  }
41869
42319
  function openDatabase(filePath) {
41870
42320
  const req = (0, import_node_module7.createRequire)(thisModuleFile3());
@@ -41901,8 +42351,8 @@ function assertRelayAccountsDurable(filePath) {
41901
42351
  if (process.env.SIDEBOARD_RELAY_ACCOUNTS_EPHEMERAL === "1") return;
41902
42352
  if (process.env.NODE_ENV !== "production") return;
41903
42353
  if (process.platform !== "linux") return;
41904
- const dir = (0, import_node_fs71.realpathSync)((0, import_node_path70.dirname)(filePath));
41905
- 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");
41906
42356
  const point = longestMountPoint(dir, mounts);
41907
42357
  if (point === "/") {
41908
42358
  throw new Error(
@@ -41913,7 +42363,7 @@ function assertRelayAccountsDurable(filePath) {
41913
42363
  var RelayAccountStore = class {
41914
42364
  db;
41915
42365
  constructor(filePath) {
41916
- (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 });
41917
42367
  this.db = openDatabase(filePath);
41918
42368
  this.db.exec(`
41919
42369
  CREATE TABLE IF NOT EXISTS accounts (
@@ -42947,9 +43397,9 @@ var import_node_http5 = require("http");
42947
43397
  var import_ws6 = require("ws");
42948
43398
 
42949
43399
  // src/relay/static.ts
42950
- var import_node_fs72 = require("fs");
43400
+ var import_node_fs73 = require("fs");
42951
43401
  var import_promises = require("fs/promises");
42952
- var import_node_path71 = __toESM(require("path"), 1);
43402
+ var import_node_path72 = __toESM(require("path"), 1);
42953
43403
  var TYPES = {
42954
43404
  ".css": "text/css; charset=utf-8",
42955
43405
  ".html": "text/html; charset=utf-8",
@@ -42980,9 +43430,9 @@ function resolveStaticPath(root, requestUrl) {
42980
43430
  return null;
42981
43431
  }
42982
43432
  if (!pathname.startsWith("/") || pathname.includes("\0")) return null;
42983
- const rootResolved = import_node_path71.default.resolve(root);
42984
- const candidate = import_node_path71.default.resolve(rootResolved, `.${pathname}`);
42985
- 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)) {
42986
43436
  return null;
42987
43437
  }
42988
43438
  return candidate;
@@ -42996,7 +43446,7 @@ async function fileSize(file) {
42996
43446
  }
42997
43447
  }
42998
43448
  function sendFile(req, res, file, size) {
42999
- const ext = import_node_path71.default.extname(file).toLowerCase();
43449
+ const ext = import_node_path72.default.extname(file).toLowerCase();
43000
43450
  res.writeHead(200, {
43001
43451
  "Content-Type": TYPES[ext] ?? "application/octet-stream",
43002
43452
  "Content-Length": size,
@@ -43006,7 +43456,7 @@ function sendFile(req, res, file, size) {
43006
43456
  res.end();
43007
43457
  return true;
43008
43458
  }
43009
- (0, import_node_fs72.createReadStream)(file).pipe(res);
43459
+ (0, import_node_fs73.createReadStream)(file).pipe(res);
43010
43460
  return true;
43011
43461
  }
43012
43462
  async function tryServeStatic(req, res, root) {
@@ -43015,7 +43465,7 @@ async function tryServeStatic(req, res, root) {
43015
43465
  if (!candidate) return false;
43016
43466
  const direct = await fileSize(candidate);
43017
43467
  if (direct != null) return sendFile(req, res, candidate, direct);
43018
- const asIndex = import_node_path71.default.join(candidate, "index.html");
43468
+ const asIndex = import_node_path72.default.join(candidate, "index.html");
43019
43469
  const indexSize = await fileSize(asIndex);
43020
43470
  if (indexSize != null) return sendFile(req, res, asIndex, indexSize);
43021
43471
  return false;
@@ -43380,8 +43830,8 @@ async function startRelayServer(opts) {
43380
43830
  }
43381
43831
 
43382
43832
  // src/slack/outbound-watch.ts
43383
- var import_node_fs73 = require("fs");
43384
- var import_node_path72 = require("path");
43833
+ var import_node_fs74 = require("fs");
43834
+ var import_node_path73 = require("path");
43385
43835
  init_paths();
43386
43836
  init_private_file();
43387
43837
  init_secure_file();
@@ -43394,7 +43844,7 @@ var POLL_INTERVAL_MS = 12e3;
43394
43844
  var lastPollMs = 0;
43395
43845
  var nameCache = /* @__PURE__ */ new Map();
43396
43846
  function storePath4() {
43397
- return (0, import_node_path72.join)(appDataDir(), "slack-outbound-watch.json");
43847
+ return (0, import_node_path73.join)(appDataDir(), "slack-outbound-watch.json");
43398
43848
  }
43399
43849
  function watchId(teamId, channelId, ts) {
43400
43850
  return `${teamId}:${channelId}:${ts}`;
@@ -43407,9 +43857,9 @@ function tsNewer(a, b) {
43407
43857
  }
43408
43858
  function readStore4() {
43409
43859
  const path2 = storePath4();
43410
- if (!(0, import_node_fs73.existsSync)(path2)) return [];
43860
+ if (!(0, import_node_fs74.existsSync)(path2)) return [];
43411
43861
  try {
43412
- 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"));
43413
43863
  return Array.isArray(parsed?.watches) ? parsed.watches : [];
43414
43864
  } catch {
43415
43865
  return [];