@sideboard-ai/core 0.1.278 → 0.1.279

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 (34) hide show
  1. package/dist/agents/cursor-runner.cjs +3 -3
  2. package/dist/agents/cursor-runner.js +5 -5
  3. package/dist/{agents-2FOWVJCN.js → agents-DYCFLDNL.js} +5 -5
  4. package/dist/{agents-JBVJRDH3.js → agents-RXP5NZBC.js} +3 -3
  5. package/dist/{chunk-UKSJO36E.js → chunk-CPA43FBK.js} +4 -4
  6. package/dist/{chunk-FELFTNH2.js → chunk-CPGJ5ICZ.js} +261 -19
  7. package/dist/{chunk-NIB2IZ6J.js → chunk-HNNS25SG.js} +5 -5
  8. package/dist/{chunk-MGS5T2A3.js → chunk-HQXBBUQX.js} +161 -9
  9. package/dist/{chunk-FIBZEFKA.js → chunk-IFURHRAR.js} +1 -1
  10. package/dist/{chunk-5N4Q46SG.js → chunk-K2S33ZCB.js} +5 -5
  11. package/dist/{chunk-NT7RIWRR.js → chunk-RK5PIWCW.js} +3 -3
  12. package/dist/{chunk-A2G3ZBJS.js → chunk-SKSOCIJT.js} +8 -8
  13. package/dist/{chunk-QTZO4LPU.js → chunk-TZ5JHIYK.js} +8 -8
  14. package/dist/{chunk-27Z4MEQ5.js → chunk-UZUXWBXK.js} +1 -1
  15. package/dist/{chunk-SQOAFR2U.js → chunk-V5DMHB6Y.js} +270 -23
  16. package/dist/{chunk-FAGK33E2.js → chunk-Y7JPL4WA.js} +1 -1
  17. package/dist/{chunk-HOMZKYHF.js → chunk-YDXNCBA2.js} +107 -8
  18. package/dist/{conductor-YYQKZAND.js → conductor-J6WDOXWM.js} +3 -3
  19. package/dist/{conductor-GBCE37K4.js → conductor-OW6NAK3N.js} +4 -4
  20. package/dist/{coordinator-prompt-SFZCZ3HR.js → coordinator-prompt-ERTHREGJ.js} +1 -1
  21. package/dist/{coordinator-prompt-B3GLVXPP.js → coordinator-prompt-Z2D32HWK.js} +1 -1
  22. package/dist/{global-workspace-7XEXTIHL.js → global-workspace-5PJXO45Z.js} +1 -1
  23. package/dist/{global-workspace-N2YYJKA5.js → global-workspace-AK5PSPQ2.js} +1 -1
  24. package/dist/index.cjs +798 -445
  25. package/dist/index.d.cts +52 -15
  26. package/dist/index.d.ts +52 -15
  27. package/dist/index.js +251 -290
  28. package/dist/mcp/run-stdio.cjs +529 -257
  29. package/dist/mcp/run-stdio.js +40 -124
  30. package/dist/{orchestrator-6QJLM6Y4.js → orchestrator-E5WK57DA.js} +5 -5
  31. package/dist/{orchestrator-QMKVXVUR.js → orchestrator-KY2KBBE2.js} +7 -7
  32. package/dist/{workspaces-PBB6HY2D.js → workspaces-UP6DQ3WL.js} +1 -1
  33. package/dist/{workspaces-BJCHMGVG.js → workspaces-WU2RNLDV.js} +1 -1
  34. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  RUN_SCRIPT_DESKTOP_ADOPT_MS,
19
19
  addStackLayerFromThread,
20
20
  adoptThread,
21
+ agentBlockFields,
21
22
  agentGitPrompt,
22
23
  alignQueueAttachments,
23
24
  appendQueuedItem,
@@ -27,6 +28,7 @@ import {
27
28
  assembleHomeBoard,
28
29
  boardPinIdentity,
29
30
  captureTurnBaseline,
31
+ chatBlockedSuffix,
30
32
  claimDesktopHost,
31
33
  classifyWorktreeColumn,
32
34
  classifyWorktreeListBadges,
@@ -63,6 +65,7 @@ import {
63
65
  fireSchedule,
64
66
  forkThreadWorktree,
65
67
  formatArtifactDirective,
68
+ formatAskUserNotifyMessage,
66
69
  formatDetachedJobInvoke,
67
70
  formatDevAccessHint,
68
71
  formatInjectedNoticesForTurn,
@@ -72,6 +75,7 @@ import {
72
75
  formatLinearReminder,
73
76
  formatLongRunningDirective,
74
77
  formatLongRunningReminder,
78
+ formatNotifyOrchestratorPrompt,
75
79
  formatOptionalServicesDirective,
76
80
  formatOptionalServicesReminder,
77
81
  formatPrGateDirective,
@@ -124,6 +128,7 @@ import {
124
128
  mergeSetupOutput,
125
129
  moveQueuedItemToFront,
126
130
  normalizeServiceOrigin,
131
+ notifyOrchestrator,
127
132
  openPrStackLayers,
128
133
  openStackLayer,
129
134
  optionalServiceConnected,
@@ -146,6 +151,7 @@ import {
146
151
  resolveConductorCursorAgentId,
147
152
  resolveCreateFirstPrompt,
148
153
  resolveDetachedJobScript,
154
+ resolveNotifyCallerThread,
149
155
  resolveOrchChildFollowUp,
150
156
  resolveScheduleThreadId,
151
157
  resolveSendFollowUp,
@@ -154,6 +160,7 @@ import {
154
160
  scheduleNeedsCaffeinate,
155
161
  schedulesPath,
156
162
  shiftQueuedItem,
163
+ shouldNotifyOrchestrator,
157
164
  shouldRemoveWorktreeOnTeardown,
158
165
  shouldRunHistoryCleanup,
159
166
  shouldRunWorktreeCleanup,
@@ -162,6 +169,9 @@ import {
162
169
  spawnAgentTurn,
163
170
  stackAgentDefaultsFrom,
164
171
  stackIdFrom,
172
+ stampAskUserBlock,
173
+ stampNotifyBlock,
174
+ stampPlanBlock,
165
175
  startOrchestration,
166
176
  statWorktreePath,
167
177
  syncBoardPins,
@@ -169,11 +179,13 @@ import {
169
179
  thisProcessShouldDrainAgentQueues,
170
180
  updateSchedule,
171
181
  verifyOptionalService,
182
+ visibleAgentBlock,
172
183
  waitForPidExit,
184
+ worktreeAgentBlock,
173
185
  worktreeBoardStatus,
174
186
  worktreeCleanupSettings,
175
187
  writeWorktreeFile
176
- } from "./chunk-SQOAFR2U.js";
188
+ } from "./chunk-V5DMHB6Y.js";
177
189
  import {
178
190
  BASE_ALLOWED_TOOLS,
179
191
  BRIGHTSY_MCP_ALLOWED_TOOLS,
@@ -263,7 +275,7 @@ import {
263
275
  whichOnPath,
264
276
  worktreeMcpToolNames,
265
277
  writeInjectedMcpConfig
266
- } from "./chunk-NIB2IZ6J.js";
278
+ } from "./chunk-HNNS25SG.js";
267
279
  import {
268
280
  brightsyMcpServerName,
269
281
  connectBrightsyTeam,
@@ -284,6 +296,7 @@ import {
284
296
  CONTEXT_MIN_MESSAGES,
285
297
  CONTEXT_REVIEW_PATH,
286
298
  LEGACY_REVIEW_REQUEST_PATH,
299
+ PLAN_QUESTION_ANSWERS_PREFIX,
287
300
  PORT_RANGE_SIZE,
288
301
  REPO_REVIEW_NAME,
289
302
  REPO_REVIEW_PATH,
@@ -321,19 +334,24 @@ import {
321
334
  estimateOccupancyTokens,
322
335
  estimateThreadChars,
323
336
  extractBrightsyContextSummary,
337
+ extractPendingPlanQuestions,
324
338
  extractiveSummary,
325
339
  findLastBrightsyContextSummary,
326
340
  forkChatTab,
327
341
  forkMessageSlice,
328
342
  formatMessagesAsTranscript,
343
+ formatPlanQuestionAnswers,
344
+ formatPlanQuestionsForChat,
329
345
  formatTranscriptMarkdown,
330
346
  forwardContextUsage,
331
347
  forwardOccupancyTokens,
332
348
  getDefaultRunScript,
333
349
  getRunMode,
334
350
  getRunScript,
351
+ isAskUserToolName,
335
352
  isIncompleteTaskState,
336
353
  isOtherWorktreeListenerCwd,
354
+ isPlanQuestionAnswersMessage,
337
355
  isTerminalTaskState,
338
356
  isThisWorktreeListenerCwd,
339
357
  killDetachedRunTree,
@@ -341,6 +359,7 @@ import {
341
359
  killStaleWorktreeListeners,
342
360
  killableListenerPids,
343
361
  lastRequestOccupancy,
362
+ latestPendingPlanQuestions,
344
363
  listRunScripts,
345
364
  listenerCwd,
346
365
  loadWorktreeRunPorts,
@@ -351,6 +370,8 @@ import {
351
370
  messagesForCompactArchive,
352
371
  messagesSinceLastBrightsyContextSummary,
353
372
  needsCoordinatorAction,
373
+ parsePlanQuestionsInput,
374
+ planQuestionsSignature,
354
375
  readExistingReviewRequestFile,
355
376
  readWorktreeInclude,
356
377
  requestReview,
@@ -378,9 +399,10 @@ import {
378
399
  tryReservePorts,
379
400
  waitForDetachedJob,
380
401
  waitForTurnToolResult,
402
+ workspaceAgentChatSummaries,
381
403
  worktreeRunPortsPath,
382
404
  wrapReviewSkillMarkdown
383
- } from "./chunk-MGS5T2A3.js";
405
+ } from "./chunk-HQXBBUQX.js";
384
406
  import {
385
407
  STDERR_EVENT_MAX_CHARS,
386
408
  STDOUT_EVENT_MAX_CHARS,
@@ -422,7 +444,7 @@ import {
422
444
  visibleToolRowDetail,
423
445
  withEventParentId,
424
446
  withEventsParentId
425
- } from "./chunk-FAGK33E2.js";
447
+ } from "./chunk-Y7JPL4WA.js";
426
448
  import "./chunk-7H655NEW.js";
427
449
  import {
428
450
  ABLETIME_OAUTH_CALLBACK_PATH,
@@ -557,7 +579,7 @@ import {
557
579
  stageBuffersAsAttachments,
558
580
  syncWorkspacesFromThreads,
559
581
  takenTeamSlugsForOrchestration
560
- } from "./chunk-A2G3ZBJS.js";
582
+ } from "./chunk-SKSOCIJT.js";
561
583
  import {
562
584
  FAMOUS_SOCCER_TEAMS,
563
585
  GITHUB_PR_BODY_MAX_CHARS,
@@ -697,7 +719,7 @@ import {
697
719
  } from "./chunk-N7WZ6NIN.js";
698
720
  import {
699
721
  clipMcpToolResult
700
- } from "./chunk-27Z4MEQ5.js";
722
+ } from "./chunk-UZUXWBXK.js";
701
723
  import "./chunk-AXQ4ZWHK.js";
702
724
  import {
703
725
  AGENT_RUNNER_MAX_OLD_SPACE_MB,
@@ -713,8 +735,8 @@ import {
713
735
  trustSystemCertificates,
714
736
  turnCostUsdFromCursorUsage,
715
737
  withMaxOldSpaceSize
716
- } from "./chunk-FIBZEFKA.js";
717
- import "./chunk-5N4Q46SG.js";
738
+ } from "./chunk-IFURHRAR.js";
739
+ import "./chunk-K2S33ZCB.js";
718
740
  import {
719
741
  ATTACHMENTS_DIR,
720
742
  CLI_DIR,
@@ -3536,219 +3558,6 @@ function outboundReplyFromTurn(result, copy) {
3536
3558
  }
3537
3559
  }
3538
3560
 
3539
- // src/orchestrator/notify-orchestrator.ts
3540
- var MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS = 1500;
3541
- var inputRequiredNotified = /* @__PURE__ */ new Set();
3542
- function clampMessage(raw) {
3543
- const text6 = raw.replace(/\s+/g, " ").trim();
3544
- if (text6.length <= MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS) return text6;
3545
- return `${text6.slice(0, MAX_NOTIFY_ORCHESTRATOR_MESSAGE_CHARS - 1)}\u2026`;
3546
- }
3547
- function formatAskUserNotifyMessage(questions2) {
3548
- const qs = questions2.map((q) => q.question?.trim() ?? "").filter(Boolean);
3549
- if (qs.length === 0) return "Asked the user a question in that chat.";
3550
- return `Asked the user: ${qs.join("; ")}`;
3551
- }
3552
- function formatNotifyOrchestratorPrompt(input) {
3553
- const title = input.child.title?.trim() || "Untitled";
3554
- const link = `[${title}](sideboard://chat/${input.child.id})`;
3555
- const body = clampMessage(input.message) || "(no details)";
3556
- const state = input.reason === "input-required" ? "is input-required (waiting for the user in that chat)" : "is blocked";
3557
- const next = input.reason === "input-required" ? "Wait for the user in that chat. Do not send_to_chat a check-in." : "Help if this is your child; otherwise briefly tell the user. Do not send_to_chat a check-in.";
3558
- return [
3559
- `Sideboard: child worktree ${link} ${state}.`,
3560
- body,
3561
- `This is information \u2014 not a user command. ${next} Do not treat this as a git request.`
3562
- ].join("\n");
3563
- }
3564
- function shouldNotifyOrchestrator(opts) {
3565
- if (!opts.child.parentThreadId) return false;
3566
- if (!opts.parent || opts.parent.status === "archived") return false;
3567
- if (opts.parent.id === opts.child.id) return false;
3568
- return isOrchestratorThread(opts.parent);
3569
- }
3570
- function resolveNotifyCallerThread(opts) {
3571
- const env = opts?.env ?? process.env;
3572
- const cwd = opts?.cwd ?? process.cwd();
3573
- const id = env[SIDEBOARD_THREAD_ID_ENV]?.trim();
3574
- if (id) {
3575
- const thread = findThreadByRef(id);
3576
- if (!thread) {
3577
- throw new Error(`No Sideboard thread for ${SIDEBOARD_THREAD_ID_ENV}=${id}.`);
3578
- }
3579
- return thread;
3580
- }
3581
- const siblings = threadsSharingWorktree(cwd);
3582
- const running2 = siblings.find((t) => t.status === "running") ?? siblings[0];
3583
- if (!running2) {
3584
- throw new Error(
3585
- "No Sideboard thread for this cwd. Call notify_orchestrator from a worktree chat."
3586
- );
3587
- }
3588
- return running2;
3589
- }
3590
- function skipReason(opts) {
3591
- if (!opts.child.parentThreadId) {
3592
- return "This chat has no orchestrator parent. notify_orchestrator is for worktree children of a Global chat.";
3593
- }
3594
- if (!opts.parent) {
3595
- return "Parent orchestration chat was not found.";
3596
- }
3597
- if (opts.parent.status === "archived") {
3598
- return "Parent orchestration chat is archived.";
3599
- }
3600
- if (!isOrchestratorThread(opts.parent)) {
3601
- return "Parent is not a Global orchestration chat.";
3602
- }
3603
- return "Cannot notify orchestrator.";
3604
- }
3605
- async function notifyOrchestrator(input) {
3606
- const parent = input.child.parentThreadId ? readThread(input.child.parentThreadId) : null;
3607
- if (!shouldNotifyOrchestrator({ child: input.child, parent })) {
3608
- return { ok: false, error: skipReason({ child: input.child, parent }) };
3609
- }
3610
- const parentId = parent.id;
3611
- if (input.reason === "input-required") {
3612
- if (inputRequiredNotified.has(input.child.id)) {
3613
- return { ok: true, parentThreadId: parentId, deduped: true };
3614
- }
3615
- inputRequiredNotified.add(input.child.id);
3616
- }
3617
- const prompt = formatNotifyOrchestratorPrompt({
3618
- child: input.child,
3619
- reason: input.reason,
3620
- message: input.message
3621
- });
3622
- try {
3623
- await input.send(parentId, prompt, { followUp: "queue" });
3624
- } catch (err) {
3625
- if (input.reason === "input-required") {
3626
- inputRequiredNotified.delete(input.child.id);
3627
- }
3628
- const message = err instanceof Error ? err.message : String(err);
3629
- return { ok: false, error: message };
3630
- }
3631
- return { ok: true, parentThreadId: parentId };
3632
- }
3633
-
3634
- // src/plan/ask-user.ts
3635
- function planQuestionsSignature(questions2) {
3636
- return questions2.map((q) => {
3637
- const opts = q.options.map((o) => o.label).join("");
3638
- return `${q.question}${opts}`;
3639
- }).join("");
3640
- }
3641
- function asRecord(v) {
3642
- return v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
3643
- }
3644
- function parseOptions(raw) {
3645
- if (!Array.isArray(raw)) return [];
3646
- const out = [];
3647
- for (const item of raw) {
3648
- if (typeof item === "string" && item.trim()) {
3649
- out.push({ label: item.trim() });
3650
- continue;
3651
- }
3652
- const o = asRecord(item);
3653
- if (!o) continue;
3654
- const label = typeof o.label === "string" ? o.label.trim() : typeof o.text === "string" ? o.text.trim() : typeof o.title === "string" ? o.title.trim() : "";
3655
- if (!label) continue;
3656
- const description = typeof o.description === "string" && o.description.trim() ? o.description.trim() : void 0;
3657
- out.push({ label, description });
3658
- }
3659
- return out;
3660
- }
3661
- function parseOneQuestion(raw) {
3662
- const o = asRecord(raw);
3663
- if (!o) return null;
3664
- const question = typeof o.question === "string" ? o.question.trim() : typeof o.text === "string" ? o.text.trim() : typeof o.prompt === "string" ? o.prompt.trim() : "";
3665
- if (!question) return null;
3666
- const options = parseOptions(o.options);
3667
- if (options.length < 1) return null;
3668
- const header = typeof o.header === "string" && o.header.trim() ? o.header.trim() : void 0;
3669
- const multiSelect = Boolean(o.multiSelect ?? o.multi_select);
3670
- return { question, header, multiSelect, options };
3671
- }
3672
- function parsePlanQuestionsInput(input) {
3673
- const root = asRecord(input);
3674
- if (!root) return [];
3675
- const list = Array.isArray(root.questions) ? root.questions : Array.isArray(root.question) ? root.question : root.question || root.prompt ? [root] : [];
3676
- const out = [];
3677
- for (const item of list) {
3678
- const q = parseOneQuestion(item);
3679
- if (q) out.push(q);
3680
- }
3681
- return out;
3682
- }
3683
- var ASK_USER_TOOL_RE = /^(AskUserQuestion|ask_user|mcp__sideboard__ask_user)$/i;
3684
- function isAskUserToolName(name) {
3685
- if (!name) return false;
3686
- const base = name.replace(/^mcp__sideboard__/i, "");
3687
- return ASK_USER_TOOL_RE.test(name) || /^ask_user$/i.test(base);
3688
- }
3689
- function extractPendingPlanQuestions(parts) {
3690
- if (!parts?.length) return null;
3691
- for (let i = parts.length - 1; i >= 0; i--) {
3692
- const p = parts[i];
3693
- if (p.type !== "tool" || !isAskUserToolName(p.name)) continue;
3694
- const questions2 = parsePlanQuestionsInput(p.input);
3695
- if (!questions2.length) continue;
3696
- return {
3697
- id: p.id || `ask-${i}`,
3698
- signature: planQuestionsSignature(questions2),
3699
- questions: questions2,
3700
- source: p.name || "ask_user"
3701
- };
3702
- }
3703
- return null;
3704
- }
3705
- function latestPendingPlanQuestions(input) {
3706
- if (input.userReplied) return null;
3707
- const fromLive = extractPendingPlanQuestions(input.liveParts);
3708
- if (fromLive) return fromLive;
3709
- const last = input.messages[input.messages.length - 1];
3710
- if (last?.role === "user") return null;
3711
- if (last?.role === "agent") return extractPendingPlanQuestions(last.parts);
3712
- return null;
3713
- }
3714
- var PLAN_QUESTION_ANSWERS_PREFIX = "Answers to your questions:";
3715
- function isPlanQuestionAnswersMessage(text6) {
3716
- return text6.startsWith(PLAN_QUESTION_ANSWERS_PREFIX);
3717
- }
3718
- function formatPlanQuestionAnswers(questions2, answers) {
3719
- const lines = [PLAN_QUESTION_ANSWERS_PREFIX, ""];
3720
- for (let i = 0; i < questions2.length; i++) {
3721
- const q = questions2[i];
3722
- const a = answers.find((x) => x.questionIndex === i);
3723
- const header = q.header ? `**${q.header}** \u2014 ` : "";
3724
- const parts = [];
3725
- if (a?.selected.length) parts.push(a.selected.join(", "));
3726
- if (a?.other?.trim()) parts.push(a.other.trim());
3727
- const body = parts.length ? parts.join(" \xB7 ") : "(no answer)";
3728
- lines.push(`${i + 1}. ${header}${q.question} `);
3729
- lines.push(` \u2192 ${body}`);
3730
- }
3731
- return lines.join("\n");
3732
- }
3733
- function formatPlanQuestionsForChat(questions2) {
3734
- const lines = ["### Questions for you", ""];
3735
- for (let i = 0; i < questions2.length; i++) {
3736
- const q = questions2[i];
3737
- const header = q.header ? `**${q.header}** \u2014 ` : "";
3738
- lines.push(`${i + 1}. ${header}${q.question}`);
3739
- for (let oi = 0; oi < q.options.length; oi++) {
3740
- const opt = q.options[oi];
3741
- const desc = opt.description?.trim();
3742
- lines.push(
3743
- desc ? ` - **${opt.label}** \u2014 ${desc}` : ` - **${opt.label}**`
3744
- );
3745
- }
3746
- lines.push("");
3747
- }
3748
- lines.push("_Answer in the composer below._");
3749
- return lines.join("\n").trimEnd();
3750
- }
3751
-
3752
3561
  // src/mcp/server.ts
3753
3562
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3754
3563
 
@@ -3830,7 +3639,8 @@ function childThreadRefs(parentId, threads) {
3830
3639
  title: t.title,
3831
3640
  status: t.status,
3832
3641
  agent: t.agent,
3833
- lastText: lastMessagePreview(t.messages, 120)
3642
+ lastText: lastMessagePreview(t.messages, 120),
3643
+ ...t.agentBlock?.reason ? { blockedReason: t.agentBlock.reason, blockedSource: t.agentBlock.source } : {}
3834
3644
  }));
3835
3645
  }
3836
3646
 
@@ -5060,7 +4870,7 @@ function presentArtifactResult(input) {
5060
4870
  message: input.type === "log" ? "Log accepted. Same artifact_id appends; send only new lines next time." : "Artifact accepted. Sideboard desktop opens it in the side column beside chat."
5061
4871
  };
5062
4872
  }
5063
- function asRecord2(value) {
4873
+ function asRecord(value) {
5064
4874
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
5065
4875
  return value;
5066
4876
  }
@@ -5084,7 +4894,7 @@ async function handleRuntimePing(_params, _ctx) {
5084
4894
  };
5085
4895
  }
5086
4896
  async function handlePresentArtifact(params, _ctx) {
5087
- const rec = asRecord2(params);
4897
+ const rec = asRecord(params);
5088
4898
  const title = str(rec.title).trim();
5089
4899
  const type = str(rec.type).trim();
5090
4900
  const allowed = ["html", "svg", "markdown", "react", "log"];
@@ -5108,7 +4918,7 @@ async function handlePresentArtifact(params, _ctx) {
5108
4918
  });
5109
4919
  }
5110
4920
  async function handleJobWait(params, ctx, waitFn = waitForDetachedJob) {
5111
- const rec = asRecord2(params);
4921
+ const rec = asRecord(params);
5112
4922
  const id = str(rec.id).trim();
5113
4923
  if (!id) throw invalidParams("id is required");
5114
4924
  const cwd = jobCwd(rec);
@@ -5132,14 +4942,14 @@ async function handleJobWait(params, ctx, waitFn = waitForDetachedJob) {
5132
4942
  };
5133
4943
  }
5134
4944
  async function handleJobStop(params, _ctx) {
5135
- const rec = asRecord2(params);
4945
+ const rec = asRecord(params);
5136
4946
  const id = str(rec.id).trim();
5137
4947
  if (!id) throw invalidParams("id is required");
5138
4948
  const reason = str(rec.reason).trim() || void 0;
5139
4949
  return stopDetachedJob(jobCwd(rec), id, reason ? { reason } : void 0);
5140
4950
  }
5141
4951
  async function defaultTurnWait(ref, timeoutMs) {
5142
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-QMKVXVUR.js");
4952
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-KY2KBBE2.js");
5143
4953
  const orch = getOrchestrator2();
5144
4954
  const thread = await orch.waitForTurn(ref, timeoutMs, {
5145
4955
  resolveIfStillRunning: true
@@ -5154,11 +4964,13 @@ async function defaultTurnWait(ref, timeoutMs) {
5154
4964
  stillRunning: result.stillRunning,
5155
4965
  progress: result.progress,
5156
4966
  lastActivityAt: result.lastActivityAt,
5157
- usage: result.usage
4967
+ usage: result.usage,
4968
+ blockedReason: result.blockedReason,
4969
+ blockedSource: result.blockedSource
5158
4970
  });
5159
4971
  }
5160
4972
  async function handleTurnWait(params, ctx, waitFn = defaultTurnWait) {
5161
- const rec = asRecord2(params);
4973
+ const rec = asRecord(params);
5162
4974
  const ref = str(rec.ref).trim();
5163
4975
  if (!ref) throw invalidParams("ref is required");
5164
4976
  const requested = typeof rec.timeoutMs === "number" && Number.isFinite(rec.timeoutMs) ? rec.timeoutMs : void 0;
@@ -5182,7 +4994,9 @@ async function handleTurnWait(params, ctx, waitFn = defaultTurnWait) {
5182
4994
  lastActivityAt: null,
5183
4995
  usage: null,
5184
4996
  hint: "Child is still working after max hold. Call wait_for_turn again. Do not send_to_chat a check-in (that steers / interrupts).",
5185
- incomplete: false
4997
+ incomplete: false,
4998
+ blockedReason: null,
4999
+ blockedSource: null
5186
5000
  };
5187
5001
  }
5188
5002
  function createAgentRpcDispatcher() {
@@ -5607,6 +5421,7 @@ async function createOrchChildThread(orch, args, resolveNewThreadOptions2) {
5607
5421
  link: `sideboard://chat/${thread.id}`,
5608
5422
  parentChatId: thread.parentThreadId,
5609
5423
  parentThreadId: thread.parentThreadId,
5424
+ chats: workspaceAgentChatSummaries(thread.worktreePath),
5610
5425
  ...alreadyStarted ? { alreadyStarted: true } : {},
5611
5426
  ...opts.coercedFrom ? { agentCoercedFrom: opts.coercedFrom } : {},
5612
5427
  ...opts.ignoredAgent ? { agentIgnored: opts.ignoredAgent } : {},
@@ -5693,7 +5508,7 @@ async function startMcpServer() {
5693
5508
  );
5694
5509
  server.tool(
5695
5510
  "list_chats",
5696
- "List chats across all projects (one summary line each \u2014 token-frugal). Includes parent id, last message preview, and live progress so you can see workspace children. Each line ends with sideboard://chat/<id> \u2014 use that URL in markdown links so the UI can open the chat.",
5511
+ "List chats across all projects (one summary line each \u2014 token-frugal). Includes parent id, last message preview, blocked: when an agent is waiting on a person, and live progress so you can see workspace children. Each line ends with sideboard://chat/<id> \u2014 use that URL in markdown links so the UI can open the chat.",
5697
5512
  {},
5698
5513
  async () => {
5699
5514
  const threads = orch.getThreads(true);
@@ -5705,7 +5520,7 @@ async function startMcpServer() {
5705
5520
  const previewBit = preview ? ` ${preview}` : "";
5706
5521
  const err = t.lastError ? ` error:${t.lastError.replace(/\s+/g, " ").slice(0, 60)}` : "";
5707
5522
  const progress = live?.summary && !isInternalAgentStatusText(live.summary) ? ` ${live.summary}` : "";
5708
- return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title}${parent}${previewBit}${err} sideboard://chat/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}${progress}`;
5523
+ return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title}${parent}${previewBit}${chatBlockedSuffix(t)}${err} sideboard://chat/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}${progress}`;
5709
5524
  });
5710
5525
  return {
5711
5526
  content: [{ type: "text", text: lines.join("\n") || "(no chats)" }]
@@ -5714,7 +5529,7 @@ async function startMcpServer() {
5714
5529
  );
5715
5530
  server.tool(
5716
5531
  "list_board",
5717
- "Home Kanban of worktrees (New, Draft, Review, Merged) \u2014 one card per checkout; sibling chat tabs nest as inner cards. Same cards as desktop Home. Path to merge: no PR \u2192 draft PR \u2192 open PR \u2192 merged. Archive removes the card to Settings \u2192 History. Queued/running are activity on the card, not columns. Orchestration chats are not on the board. Filters: query, repoPath, kind (ticket/PR/branch source), ownership (mine = your PRs / WIP; reviewing = someone else's PR), column, limit (default 40). create_workspace adds a workspace (and a Home card), or returns the live one if that ticket/PR/named branch is already checked out.",
5532
+ "Home Kanban of workspaces (New, Draft, Review, Merged) \u2014 one card per checkout; sibling chats on that card are separate agents. Same cards as desktop Home. Path to merge: no PR \u2192 draft PR \u2192 open PR \u2192 merged. Archive removes the card to Settings \u2192 History. Queued/running are activity on the card, not columns. Orchestration chats are not on the board. Filters: query, repoPath, kind (ticket/PR/branch source), ownership (mine = your PRs / WIP; reviewing = someone else's PR), column, limit (default 40). create_workspace adds a workspace (and a Home card), or returns the live checkout if that ticket/PR/named branch is already checked out. That reuse is one checkout, not one agent \u2014 fork_chat to add another.",
5718
5533
  {
5719
5534
  query: z8.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
5720
5535
  repoPath: z8.string().optional().describe("Limit to one project path from list_projects"),
@@ -5754,7 +5569,7 @@ async function startMcpServer() {
5754
5569
  );
5755
5570
  server.tool(
5756
5571
  "get_chat",
5757
- "Get a compact chat summary by id/ref. Includes last message preview, parentChatId, and child workspace chats (status + lastText). While running, includes progress (last tool/thinking) and lastActivityAt. Includes usage (billed token + costUsd totals when providers reported cost) and lastTurnUsage.",
5572
+ "Get a compact chat summary by id/ref. Includes last message preview, parentChatId, and child workspace chats (status + lastText). While running, includes progress (last tool/thinking) and lastActivityAt. Includes usage (billed token + costUsd totals when providers reported cost) and lastTurnUsage. blockedReason is why that agent is waiting on a person (question, plan approval, or a reported block).",
5758
5573
  { ref: z8.string() },
5759
5574
  async ({ ref }) => {
5760
5575
  const t = orch.getThread(ref);
@@ -5784,6 +5599,7 @@ async function startMcpServer() {
5784
5599
  devPort: t.devPort,
5785
5600
  prUrl: t.prUrl,
5786
5601
  lastError: t.lastError ?? null,
5602
+ ...agentBlockFields(t),
5787
5603
  stillRunning,
5788
5604
  progress: liveSummary ?? (stillRunning && t.status === "queued" ? "Queued \u2014 waiting for a concurrency slot" : null),
5789
5605
  lastActivityAt: live?.updatedAt ?? null,
@@ -5845,7 +5661,7 @@ async function startMcpServer() {
5845
5661
  };
5846
5662
  await notifyParent({
5847
5663
  reason: "input-required",
5848
- message: formatAskUserNotifyMessage(questions2)
5664
+ message: stampAskUserBlock(resolveNotifyCallerThread, questions2)
5849
5665
  });
5850
5666
  return mcpJson(payload);
5851
5667
  }
@@ -5877,7 +5693,7 @@ async function startMcpServer() {
5877
5693
  const payload = {
5878
5694
  ok: true,
5879
5695
  path: path2,
5880
- title: title?.trim() || "Plan",
5696
+ title: stampPlanBlock(t?.id, title),
5881
5697
  message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
5882
5698
  };
5883
5699
  return mcpJson(payload);
@@ -6082,7 +5898,7 @@ async function startMcpServer() {
6082
5898
  if (worktreeProfile) {
6083
5899
  server.tool(
6084
5900
  "notify_orchestrator",
6085
- "Wake the parent Global orchestrator with a short status so it does not have to poll wait_for_turn. Use when you are blocked and the parent may have moved on (missing access, waiting on something the coordinator must handle). ask_user already notifies for input-required \u2014 do not call this for the same question. Information only: never a git command. Do not use this to steer other worktrees.",
5901
+ "Wake the parent Global orchestrator with a short status so it does not have to poll wait_for_turn. Use when you are blocked and the parent may have moved on (missing access, waiting on something the coordinator must handle). The message is shown on the sidebar and board until the next user message in this chat. ask_user already notifies for input-required \u2014 do not call this for the same question. Information only: never a git command. Do not use this to steer other worktrees.",
6086
5902
  {
6087
5903
  message: z8.string().min(1).max(1500).describe("Why you are blocked / what the parent should know (short)."),
6088
5904
  reason: z8.enum(["input-required", "blocked"]).optional().describe(
@@ -6094,7 +5910,7 @@ async function startMcpServer() {
6094
5910
  const child = resolveNotifyCallerThread();
6095
5911
  const result = await notifyOrchestrator({
6096
5912
  child,
6097
- reason: reason ?? "blocked",
5913
+ reason: stampNotifyBlock(child.id, reason, message),
6098
5914
  message,
6099
5915
  send: (id, prompt, opts) => orch.send(id, prompt, opts)
6100
5916
  });
@@ -6152,7 +5968,7 @@ async function startMcpServer() {
6152
5968
  );
6153
5969
  server.tool(
6154
5970
  "create_workspace",
6155
- `Create a workspace (isolated git worktree + first chat) from branch, pr, or ticket. A ticket, PR, or named branch may have only one live worktree \u2014 if one already matches, returns it (alreadyStarted=true) instead of a second checkout. Creating from the default branch still opens a new isolated worktree. Pass repoPath from list_projects. cowboy=true uses the project folder on the default branch (no isolated worktree; land is commit+push). From an orchestration chat, omit parentChatId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Omit agent/model \u2014 Sideboard applies ${accountDefaultsHint}. Do not pass your own agent or agent=cursor; those are ignored. Setup (settings.toml, .cursor/worktrees.json, or script/setup) runs in the background in parallel with the first turn (skipped for cowboy). Then use send_to_chat to chat.`,
5971
+ `Create a workspace (isolated git checkout + first chat) from branch, pr, or ticket. A ticket, PR, or named branch has one checkout \u2014 if one already matches, returns it (alreadyStarted=true) instead of a second checkout. That is not one agent: chats lists every agent on that checkout. Add another with fork_chat; send_to_chat only the chat that should do this job. Creating from the default branch still opens a new isolated checkout. Pass repoPath from list_projects. cowboy=true uses the project folder on the default branch (no isolated worktree; land is commit+push). From an orchestration chat, omit parentChatId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Omit agent/model \u2014 Sideboard applies ${accountDefaultsHint}. Do not pass your own agent or agent=cursor; those are ignored. Setup (settings.toml, .cursor/worktrees.json, or script/setup) runs in the background in parallel with the first turn (skipped for cowboy).`,
6156
5972
  {
6157
5973
  sourceType: z8.enum(["branch", "pr", "ticket"]),
6158
5974
  sourceRef: z8.string(),
@@ -6182,7 +5998,7 @@ async function startMcpServer() {
6182
5998
  );
6183
5999
  server.tool(
6184
6000
  "start_board_card",
6185
- "Same as create_workspace for a ticket, PR, or named branch (attaches issue text when Sideboard can resolve it). Does not create a second worktree when one already matches \u2014 returns that thread (alreadyStarted). Then send_to_chat.",
6001
+ "Same as create_workspace for a ticket, PR, or named branch (attaches issue text when Sideboard can resolve it). Does not create a second checkout when one already matches \u2014 returns that workspace (alreadyStarted) and its chats. A workspace can have many agents. fork_chat to add one; send_to_chat only the chat that should do this job.",
6186
6002
  {
6187
6003
  kind: z8.enum(["ticket", "pr", "branch"]),
6188
6004
  ref: z8.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
@@ -6209,7 +6025,8 @@ async function startMcpServer() {
6209
6025
  id: existing.id,
6210
6026
  title: existing.title,
6211
6027
  status: existing.status,
6212
- link: `sideboard://chat/${existing.id}`
6028
+ link: `sideboard://chat/${existing.id}`,
6029
+ chats: workspaceAgentChatSummaries(orch.getThread(existing.id)?.worktreePath)
6213
6030
  });
6214
6031
  }
6215
6032
  const workspaces = orch.listWorkspaces();
@@ -6309,7 +6126,7 @@ async function startMcpServer() {
6309
6126
  if (shouldRegisterMcpWaitForTurn()) {
6310
6127
  server.tool(
6311
6128
  "wait_for_turn",
6312
- "Wait until the chat finishes its current/queued turn, or return early with a live progress snapshot. MCP clients often kill tools around 60s, so this returns within 45s even while the child is still working. taskState is the A2A-style lifecycle: submitted (queued, not started), working, input-required (ask_user), completed, failed, canceled. stillRunning is true only for submitted/working. If stillRunning, text and usage are empty (they would be the previous turn) \u2014 read progress and call wait_for_turn again. Do not send_to_chat a check-in (that steers / interrupts). On failed, lastError/text is the failure. On canceled, the child did not finish \u2014 resume with send_to_chat or tell the user. On input-required, wait for the user in that chat. When finished, text is this turn\u2019s assistant reply and usage is that turn\u2019s tokens + costUsd (this turn, not the Claude session total; sessionCostUsd is the provider session total when present).",
6129
+ "Wait until the chat finishes its current/queued turn, or return early with a live progress snapshot. MCP clients often kill tools around 60s, so this returns within 45s even while the child is still working. taskState is the A2A-style lifecycle: submitted (queued, not started), working, input-required (ask_user), completed, failed, canceled. stillRunning is true only for submitted/working. blockedReason is why the child is waiting on a person (the question, plan approval, or a reported block) \u2014 read it and do not send_to_chat a check-in. If stillRunning, text and usage are empty (they would be the previous turn) \u2014 read progress and call wait_for_turn again. Do not send_to_chat a check-in (that steers / interrupts). On failed, lastError/text is the failure. On canceled, the child did not finish \u2014 resume with send_to_chat or tell the user. On input-required, wait for the user in that chat. When finished, text is this turn\u2019s assistant reply and usage is that turn\u2019s tokens + costUsd (this turn, not the Claude session total; sessionCostUsd is the provider session total when present).",
6313
6130
  {
6314
6131
  ref: z8.string(),
6315
6132
  timeoutMs: z8.number().optional()
@@ -6329,7 +6146,9 @@ async function startMcpServer() {
6329
6146
  stillRunning: result.stillRunning,
6330
6147
  progress: result.progress,
6331
6148
  lastActivityAt: result.lastActivityAt,
6332
- usage: result.usage
6149
+ usage: result.usage,
6150
+ blockedReason: result.blockedReason,
6151
+ blockedSource: result.blockedSource
6333
6152
  })
6334
6153
  );
6335
6154
  }
@@ -6337,15 +6156,16 @@ async function startMcpServer() {
6337
6156
  }
6338
6157
  server.tool(
6339
6158
  "get_turn_result",
6340
- "Assistant message and last-turn usage when the turn finished. While stillRunning (submitted/working), text and usage are empty so a previous reply cannot be mistaken for this turn \u2014 use progress for tools/thinking. Not the full transcript. Includes taskState (A2A lifecycle).",
6159
+ "Assistant message and last-turn usage when the turn finished. While stillRunning (submitted/working), text and usage are empty so a previous reply cannot be mistaken for this turn \u2014 use progress for tools/thinking. Not the full transcript. Includes taskState (A2A lifecycle) and blockedReason (why the agent is waiting on a person).",
6341
6160
  { ref: z8.string() },
6342
6161
  async ({ ref }) => {
6343
6162
  const result = orch.getTurnResult(ref);
6344
- const hint = mcpWaitTaskHint(result.taskState, result.status);
6163
+ const hint = mcpWaitTaskHint(result.taskState, result.status, result.blockedReason);
6164
+ const waiting = Boolean(result.blockedReason) && !result.stillRunning && result.taskState !== "failed" && result.taskState !== "canceled";
6345
6165
  return mcpJson({
6346
6166
  ...result,
6347
6167
  hint,
6348
- incomplete: needsCoordinatorAction(result.taskState)
6168
+ incomplete: needsCoordinatorAction(result.taskState) || waiting
6349
6169
  });
6350
6170
  }
6351
6171
  );
@@ -6559,7 +6379,7 @@ async function startMcpServer() {
6559
6379
  );
6560
6380
  server.tool(
6561
6381
  "fork_chat",
6562
- "Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Slack / Global orchestrators use this to continue an orchestration chat on another agent after session limits. Then send_to_chat / wait_for_turn (loop while stillRunning) on the returned id. Use fork_workspace only for worktree agents that need a new git worktree.",
6382
+ "Add another agent chat on the SAME workspace (same files, Run, and git). Also forks a Global orchestration chat into a new orchestration tab. Pass agent when the new chat should use a different harness. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Use this for a second agent on a checkout (review, another harness, parallel work on the same branch). Do not create_workspace again for that \u2014 it returns the existing checkout. Then send_to_chat / wait_for_turn (loop while stillRunning) on the returned id. Use fork_workspace only when the work needs its own git checkout.",
6563
6383
  {
6564
6384
  ref: z8.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
6565
6385
  through_index: z8.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
@@ -9435,10 +9255,10 @@ var MIN_MD_CHARS = 120;
9435
9255
  function oneLine(value, max) {
9436
9256
  return value.replace(/[`\r\n]/g, " ").replace(/\s+/g, " ").trim().slice(0, max);
9437
9257
  }
9438
- function asRecord3(value) {
9258
+ function asRecord2(value) {
9439
9259
  if (typeof value === "string") {
9440
9260
  try {
9441
- return asRecord3(JSON.parse(value));
9261
+ return asRecord2(JSON.parse(value));
9442
9262
  } catch {
9443
9263
  return void 0;
9444
9264
  }
@@ -9513,7 +9333,7 @@ function toolContent(input) {
9513
9333
  if (!input) return "";
9514
9334
  const direct = input.content;
9515
9335
  if (typeof direct === "string") return direct;
9516
- const nested = asRecord3(direct);
9336
+ const nested = asRecord2(direct);
9517
9337
  if (!nested) return "";
9518
9338
  for (const key of ["html", "markup", "text", "source", "content", "code"]) {
9519
9339
  if (typeof nested[key] === "string") return nested[key];
@@ -9536,7 +9356,7 @@ function phoneArtifactText(text6, parts, limit = Number.POSITIVE_INFINITY) {
9536
9356
  const extras = [];
9537
9357
  for (const part of parts ?? []) {
9538
9358
  if (part.type !== "tool" || part.parentId || !isArtifactTool(part.name)) continue;
9539
- const input = asRecord3(part.input);
9359
+ const input = asRecord2(part.input);
9540
9360
  const content = toolContent(input);
9541
9361
  const hint = artifactHint(content);
9542
9362
  if (hint && hints.has(hint)) continue;
@@ -9555,7 +9375,7 @@ function phoneArtifactText(text6, parts, limit = Number.POSITIVE_INFINITY) {
9555
9375
  ${tail}` : tail;
9556
9376
  }
9557
9377
  function parseOpenArtifact(value, chatId) {
9558
- const record = asRecord3(value);
9378
+ const record = asRecord2(value);
9559
9379
  if (!record) return "invalid";
9560
9380
  const title = oneLine(str3(record.title), 200);
9561
9381
  if (!title) return "invalid";
@@ -10138,6 +9958,13 @@ var REMOTE_STOPPED_REPLY = "Sideboard stopped the in-progress turn.";
10138
9958
  var chatChains = /* @__PURE__ */ new Map();
10139
9959
  var chatGeneration = /* @__PURE__ */ new Map();
10140
9960
  var phoneTurnGen = /* @__PURE__ */ new Map();
9961
+ var phoneOpenChatId = null;
9962
+ function setPhoneOpenChat(id) {
9963
+ phoneOpenChatId = id?.trim() || null;
9964
+ }
9965
+ function getPhoneOpenChat() {
9966
+ return phoneOpenChatId;
9967
+ }
10141
9968
  function bumpChat(chatId) {
10142
9969
  const next = (chatGeneration.get(chatId) ?? 0) + 1;
10143
9970
  chatGeneration.set(chatId, next);
@@ -10319,13 +10146,17 @@ function interruptRemoteCoordinator(deviceId, log = () => void 0) {
10319
10146
  return false;
10320
10147
  }
10321
10148
  }
10149
+ function emitOpened(opts, opened) {
10150
+ setPhoneOpenChat(opened.chat.id);
10151
+ emitControl(opts, { op: "opened", ...opened });
10152
+ }
10322
10153
  function publishOpenedChat(opts, chatId) {
10323
10154
  const opened = openPhoneChat(chatId);
10324
10155
  if (!opened) {
10325
10156
  emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
10326
10157
  return;
10327
10158
  }
10328
- emitControl(opts, { op: "opened", ...opened });
10159
+ emitOpened(opts, opened);
10329
10160
  if (opened.chat.status === "running" || opened.chat.status === "queued") {
10330
10161
  deliverPhoneTurn(opts, opened.chat.id);
10331
10162
  }
@@ -10352,6 +10183,7 @@ function stagePhoneTurnFiles(worktreePath, draft) {
10352
10183
  function handlePhoneControl(cmd, opts) {
10353
10184
  const log = opts.onLog ?? (() => void 0);
10354
10185
  if (cmd.op === "list") {
10186
+ setPhoneOpenChat(null);
10355
10187
  emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
10356
10188
  return;
10357
10189
  }
@@ -10430,7 +10262,7 @@ function handlePhoneControl(cmd, opts) {
10430
10262
  }
10431
10263
  }
10432
10264
  const fresh = openPhoneChat(opened.chat.id) ?? opened;
10433
- emitControl(opts, { op: "opened", ...fresh });
10265
+ emitOpened(opts, fresh);
10434
10266
  if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
10435
10267
  } catch (err) {
10436
10268
  emitControl(opts, { op: "error", message: phoneError(err) });
@@ -10452,7 +10284,7 @@ function handlePhoneControl(cmd, opts) {
10452
10284
  }
10453
10285
  }
10454
10286
  const fresh = openPhoneChat(opened.chat.id) ?? opened;
10455
- emitControl(opts, { op: "opened", ...fresh });
10287
+ emitOpened(opts, fresh);
10456
10288
  if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
10457
10289
  }).catch((err) => {
10458
10290
  emitControl(opts, { op: "error", message: phoneError(err) });
@@ -10462,7 +10294,7 @@ function handlePhoneControl(cmd, opts) {
10462
10294
  const goal = cmd.goal?.trim() || cmd.prompt?.trim() || "";
10463
10295
  if (goal || cmd.files?.length || cmd.links?.length) {
10464
10296
  void createPhoneOrchestration({ ...cmd, goal }).then((opened) => {
10465
- emitControl(opts, { op: "opened", ...opened });
10297
+ emitOpened(opts, opened);
10466
10298
  deliverPhoneTurn(opts, opened.chat.id);
10467
10299
  }).catch((err) => {
10468
10300
  emitControl(opts, { op: "error", message: phoneError(err) });
@@ -10473,7 +10305,7 @@ function handlePhoneControl(cmd, opts) {
10473
10305
  const agent = coerceOrchestratorAgent(
10474
10306
  cmd.agent ?? opts.agent ?? resolveOrchestratorDefaults().agent
10475
10307
  );
10476
- emitControl(opts, { op: "opened", ...createPhoneChat(agent) });
10308
+ emitOpened(opts, createPhoneChat(agent));
10477
10309
  } catch (err) {
10478
10310
  emitControl(opts, { op: "error", message: phoneError(err) });
10479
10311
  }
@@ -10505,6 +10337,7 @@ function handlePhoneControl(cmd, opts) {
10505
10337
  }
10506
10338
  bumpChat(thread.id);
10507
10339
  interruptChat(thread.id, log);
10340
+ if (getPhoneOpenChat() === thread.id) setPhoneOpenChat(null);
10508
10341
  archivePhoneChat(thread.id);
10509
10342
  emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
10510
10343
  return;
@@ -10697,6 +10530,99 @@ function handleRemoteInbound(text6, opts) {
10697
10530
  });
10698
10531
  }
10699
10532
 
10533
+ // src/remote/phone-board-sync.ts
10534
+ import { basename as basename3 } from "path";
10535
+ import { watch } from "chokidar";
10536
+ var PHONE_BOARD_SYNC_MS = 300;
10537
+ function chatKey(row) {
10538
+ return [row.id, row.title, row.status, row.agent, row.preview].join(" ");
10539
+ }
10540
+ function phoneBoardKey(sidebar) {
10541
+ const orchestration = sidebar.orchestration.map(chatKey).join("\n");
10542
+ const projects = sidebar.projects.map((project) => {
10543
+ const trees = project.worktrees.map((tree) => `${tree.label} ${tree.chats.map(chatKey).join("\n")}`).join("\n");
10544
+ return `${project.path} ${project.name}
10545
+ ${trees}`;
10546
+ }).join("\n---\n");
10547
+ return `${orchestration}
10548
+ ===
10549
+ ${projects}`;
10550
+ }
10551
+ function isPhoneBoardEvent(event) {
10552
+ return event.type === "status_changed" || event.type === "turn_started" || event.type === "turn_finished" || event.type === "queue_changed" || event.type === "error" || event.type === "context_compacted" || event.type === "quota_failover";
10553
+ }
10554
+ function isRunningChat(event) {
10555
+ if (event.type === "turn_started") return true;
10556
+ return event.type === "status_changed" && (event.status === "running" || event.status === "queued");
10557
+ }
10558
+ function subscribePhoneBoard(notify) {
10559
+ const threads = watch(threadsDir(), { ignoreInitial: true, depth: 0 });
10560
+ const onThread = (changed) => {
10561
+ if (!isThreadRecordFile(changed)) return;
10562
+ notify();
10563
+ };
10564
+ threads.on("add", onThread);
10565
+ threads.on("change", onThread);
10566
+ threads.on("unlink", onThread);
10567
+ threads.on("error", () => void 0);
10568
+ const data = watch(appDataDir(), { ignoreInitial: true, depth: 0 });
10569
+ const onData = (changed) => {
10570
+ const name = basename3(changed);
10571
+ if (name !== "workspaces.json" && name !== "removed-workspaces.json") return;
10572
+ notify();
10573
+ };
10574
+ data.on("add", onData);
10575
+ data.on("change", onData);
10576
+ data.on("unlink", onData);
10577
+ data.on("error", () => void 0);
10578
+ return () => {
10579
+ void threads.close();
10580
+ void data.close();
10581
+ };
10582
+ }
10583
+ function startPhoneBoardSync(opts) {
10584
+ const list = opts.list ?? listPhoneSidebar;
10585
+ const debounceMs = opts.debounceMs ?? PHONE_BOARD_SYNC_MS;
10586
+ let timer = null;
10587
+ let lastKey = "";
10588
+ let stopped = false;
10589
+ const push = () => {
10590
+ timer = null;
10591
+ if (stopped) return;
10592
+ let sidebar;
10593
+ try {
10594
+ sidebar = list();
10595
+ } catch {
10596
+ return;
10597
+ }
10598
+ const key = phoneBoardKey(sidebar);
10599
+ if (key === lastKey) return;
10600
+ lastKey = key;
10601
+ opts.onSidebar(sidebar);
10602
+ };
10603
+ const schedule = () => {
10604
+ if (stopped) return;
10605
+ if (timer) clearTimeout(timer);
10606
+ timer = setTimeout(push, debounceMs);
10607
+ };
10608
+ const handle = (event) => {
10609
+ if (isPhoneBoardEvent(event)) schedule();
10610
+ if (!isRunningChat(event)) return;
10611
+ const open = opts.openChatId?.();
10612
+ if (open && open === event.threadId) opts.onRunningChat?.(event.threadId);
10613
+ };
10614
+ const unsubscribe = (opts.subscribe ?? subscribePhoneBoard)(schedule);
10615
+ const offEvents = (opts.events ?? ((listener) => getOrchestrator().on(listener)))(handle);
10616
+ push();
10617
+ return () => {
10618
+ stopped = true;
10619
+ if (timer) clearTimeout(timer);
10620
+ timer = null;
10621
+ unsubscribe();
10622
+ offEvents();
10623
+ };
10624
+ }
10625
+
10700
10626
  // src/remote/client.ts
10701
10627
  var BACKOFF_START_MS3 = 1e3;
10702
10628
  var BACKOFF_MAX_MS3 = 3e4;
@@ -10743,6 +10669,24 @@ function runRemoteHost(opts) {
10743
10669
  let wantPair = false;
10744
10670
  let deviceLabel = opts.deviceLabel;
10745
10671
  let keepalive = null;
10672
+ let connectionGen = 0;
10673
+ let stopBoard = null;
10674
+ const stopBoardSync = () => {
10675
+ stopBoard?.();
10676
+ stopBoard = null;
10677
+ };
10678
+ const startBoardSync = () => {
10679
+ stopBoardSync();
10680
+ stopBoard = startPhoneBoardSync({
10681
+ openChatId: getPhoneOpenChat,
10682
+ onSidebar: (sidebar) => {
10683
+ emitControl({ onOutbound: emit }, { op: "sidebar", ...sidebar });
10684
+ },
10685
+ onRunningChat: (chatId) => {
10686
+ deliverPhoneTurn({ onOutbound: emit }, chatId);
10687
+ }
10688
+ });
10689
+ };
10746
10690
  const send3 = (msg) => {
10747
10691
  if (!socket || socket.readyState !== socket.OPEN) return;
10748
10692
  socket.send(JSON.stringify(msg));
@@ -10779,12 +10723,19 @@ function runRemoteHost(opts) {
10779
10723
  }
10780
10724
  };
10781
10725
  const connectOnce = () => new Promise((resolve, reject) => {
10726
+ const gen = ++connectionGen;
10727
+ stopBoardSync();
10728
+ setPhoneOpenChat(null);
10782
10729
  const ws = new WsWebSocket3(opts.url, { lookup: lookupPreferPublicDns });
10783
10730
  socket = ws;
10784
10731
  let settled = false;
10785
10732
  const finish = (err) => {
10786
10733
  if (settled) return;
10787
10734
  settled = true;
10735
+ if (gen === connectionGen) {
10736
+ stopBoardSync();
10737
+ setPhoneOpenChat(null);
10738
+ }
10788
10739
  keepalive?.stop();
10789
10740
  keepalive = null;
10790
10741
  opts.signal?.removeEventListener("abort", onAbort);
@@ -10822,6 +10773,13 @@ function runRemoteHost(opts) {
10822
10773
  const raw = typeof data === "string" ? data : data.toString("utf8");
10823
10774
  const msg = parseRemoteServerMessage(raw);
10824
10775
  if (!msg) return;
10776
+ if (gen === connectionGen) {
10777
+ if (msg.type === "phone_joined") startBoardSync();
10778
+ if (msg.type === "phone_left") {
10779
+ setPhoneOpenChat(null);
10780
+ stopBoardSync();
10781
+ }
10782
+ }
10825
10783
  handleServer(msg);
10826
10784
  });
10827
10785
  ws.on("close", () => finish());
@@ -12450,7 +12408,7 @@ async function continueSourceThread(threadId, prompt) {
12450
12408
  await continueOnReply(threadId, prompt);
12451
12409
  return;
12452
12410
  }
12453
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-QMKVXVUR.js");
12411
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-KY2KBBE2.js");
12454
12412
  await getOrchestrator2().send(threadId, prompt);
12455
12413
  } catch {
12456
12414
  }
@@ -12464,10 +12422,10 @@ function formatOwnerSlackFyi(userName, text6) {
12464
12422
  return `${who} replied in Slack:
12465
12423
  ${body}`;
12466
12424
  }
12467
- function sameSlackConversation(watch, target) {
12468
- if (watch.channelId !== target.channelId) return false;
12469
- const targetThread = target.threadTs?.trim() || watch.threadTs;
12470
- return targetThread === watch.threadTs;
12425
+ function sameSlackConversation(watch2, target) {
12426
+ if (watch2.channelId !== target.channelId) return false;
12427
+ const targetThread = target.threadTs?.trim() || watch2.threadTs;
12428
+ return targetThread === watch2.threadTs;
12471
12429
  }
12472
12430
  async function relayExternalReply(opts) {
12473
12431
  const threadId = opts.watch.sourceThreadId?.trim();
@@ -12543,15 +12501,15 @@ function recordSlackOutboundWatch(input) {
12543
12501
  writeStore2(pruneWatches(watches));
12544
12502
  return next;
12545
12503
  }
12546
- function isHumanReply(msg, watch) {
12504
+ function isHumanReply(msg, watch2) {
12547
12505
  const ts = msg.ts?.trim();
12548
- if (!ts || ts === watch.ts) return false;
12549
- if (!tsNewer(ts, watch.lastSeenTs)) return false;
12506
+ if (!ts || ts === watch2.ts) return false;
12507
+ if (!tsNewer(ts, watch2.lastSeenTs)) return false;
12550
12508
  if (msg.bot_id) return false;
12551
12509
  if (msg.subtype) return false;
12552
12510
  const user = msg.user?.trim();
12553
12511
  if (!user) return false;
12554
- if (watch.ownerUserId && user === watch.ownerUserId) return false;
12512
+ if (watch2.ownerUserId && user === watch2.ownerUserId) return false;
12555
12513
  return true;
12556
12514
  }
12557
12515
  function displayName(user) {
@@ -12583,16 +12541,16 @@ async function resolvePermalink(token, channelId, ts, fetchImpl) {
12583
12541
  }
12584
12542
  return slackArchiveUrl(channelId, ts);
12585
12543
  }
12586
- async function fetchMessages(token, watch, fetchImpl) {
12544
+ async function fetchMessages(token, watch2, fetchImpl) {
12587
12545
  const out = [];
12588
12546
  try {
12589
12547
  const data = await slackApi(
12590
12548
  token,
12591
12549
  "conversations.replies",
12592
12550
  {
12593
- channel: watch.channelId,
12594
- ts: watch.threadTs,
12595
- oldest: watch.lastSeenTs,
12551
+ channel: watch2.channelId,
12552
+ ts: watch2.threadTs,
12553
+ oldest: watch2.lastSeenTs,
12596
12554
  inclusive: false,
12597
12555
  limit: 50
12598
12556
  },
@@ -12601,14 +12559,14 @@ async function fetchMessages(token, watch, fetchImpl) {
12601
12559
  out.push(...data.messages ?? []);
12602
12560
  } catch {
12603
12561
  }
12604
- if (watch.kind === "dm" || watch.channelId.startsWith("D")) {
12562
+ if (watch2.kind === "dm" || watch2.channelId.startsWith("D")) {
12605
12563
  try {
12606
12564
  const data = await slackApi(
12607
12565
  token,
12608
12566
  "conversations.history",
12609
12567
  {
12610
- channel: watch.channelId,
12611
- oldest: watch.lastSeenTs,
12568
+ channel: watch2.channelId,
12569
+ oldest: watch2.lastSeenTs,
12612
12570
  inclusive: false,
12613
12571
  limit: 50
12614
12572
  },
@@ -12628,8 +12586,8 @@ async function pollSlackOutboundWatches(opts) {
12628
12586
  let watches = pruneWatches(existing, now);
12629
12587
  let changed = watches.length !== existing.length;
12630
12588
  for (let i = 0; i < watches.length; i++) {
12631
- const watch = watches[i];
12632
- const ws = getSlackWorkspace(watch.teamId);
12589
+ const watch2 = watches[i];
12590
+ const ws = getSlackWorkspace(watch2.teamId);
12633
12591
  if (!ws) continue;
12634
12592
  let token;
12635
12593
  try {
@@ -12637,19 +12595,19 @@ async function pollSlackOutboundWatches(opts) {
12637
12595
  } catch {
12638
12596
  continue;
12639
12597
  }
12640
- const messages = await fetchMessages(token, watch, opts?.fetchImpl);
12641
- const replies = messages.filter((m) => isHumanReply(m, watch)).sort((a, b) => Number(a.ts) - Number(b.ts));
12598
+ const messages = await fetchMessages(token, watch2, opts?.fetchImpl);
12599
+ const replies = messages.filter((m) => isHumanReply(m, watch2)).sort((a, b) => Number(a.ts) - Number(b.ts));
12642
12600
  if (replies.length === 0) continue;
12643
- const injected = new Set(watch.injectedReplyTs ?? []);
12644
- const collected = [...watch.replies ?? []];
12645
- let lastSeenTs = watch.lastSeenTs;
12601
+ const injected = new Set(watch2.injectedReplyTs ?? []);
12602
+ const collected = [...watch2.replies ?? []];
12603
+ let lastSeenTs = watch2.lastSeenTs;
12646
12604
  let latestName;
12647
- let latestPermalink = watch.permalink;
12605
+ let latestPermalink = watch2.permalink;
12648
12606
  let newlyInjected = 0;
12649
12607
  for (const msg of replies) {
12650
12608
  const ts = msg.ts.trim();
12651
12609
  const user = msg.user.trim();
12652
- const fallback = watch.toUserId === user ? watch.toLabel : user;
12610
+ const fallback = watch2.toUserId === user ? watch2.toLabel : user;
12653
12611
  const replyUserName = await resolveUserName(
12654
12612
  token,
12655
12613
  user,
@@ -12658,7 +12616,7 @@ async function pollSlackOutboundWatches(opts) {
12658
12616
  );
12659
12617
  const permalink = await resolvePermalink(
12660
12618
  token,
12661
- watch.channelId,
12619
+ watch2.channelId,
12662
12620
  ts,
12663
12621
  opts?.fetchImpl
12664
12622
  );
@@ -12676,7 +12634,7 @@ async function pollSlackOutboundWatches(opts) {
12676
12634
  continue;
12677
12635
  }
12678
12636
  const delivered = await relayExternalReply({
12679
- watch,
12637
+ watch: watch2,
12680
12638
  reply,
12681
12639
  permalink,
12682
12640
  fetchImpl: opts?.fetchImpl
@@ -12686,19 +12644,19 @@ async function pollSlackOutboundWatches(opts) {
12686
12644
  injected.add(ts);
12687
12645
  lastSeenTs = ts;
12688
12646
  }
12689
- if (newlyInjected > 0 && watch.sourceThreadId?.trim()) {
12647
+ if (newlyInjected > 0 && watch2.sourceThreadId?.trim()) {
12690
12648
  await continueSourceThread(
12691
- watch.sourceThreadId.trim(),
12649
+ watch2.sourceThreadId.trim(),
12692
12650
  formatSlackReplyContinuePrompt({
12693
- userName: latestName || watch.toLabel,
12694
- kind: watch.kind,
12695
- toLabel: watch.toLabel,
12651
+ userName: latestName || watch2.toLabel,
12652
+ kind: watch2.kind,
12653
+ toLabel: watch2.toLabel,
12696
12654
  count: newlyInjected
12697
12655
  })
12698
12656
  );
12699
12657
  }
12700
12658
  watches[i] = {
12701
- ...watch,
12659
+ ...watch2,
12702
12660
  lastSeenTs,
12703
12661
  permalink: latestPermalink,
12704
12662
  injectedReplyTs: [...injected],
@@ -13728,6 +13686,7 @@ export {
13728
13686
  validateLinearApiKey,
13729
13687
  verifyAbleTimeConnection,
13730
13688
  verifyOptionalService,
13689
+ visibleAgentBlock,
13731
13690
  visibleAssistantText,
13732
13691
  visibleToolRowDetail,
13733
13692
  waitForPidExit,
@@ -13742,7 +13701,9 @@ export {
13742
13701
  withSelectedAccount,
13743
13702
  withThreadLock,
13744
13703
  withTurnScopedCost,
13704
+ workspaceAgentChatSummaries,
13745
13705
  workspaceSettingsSourceLabel,
13706
+ worktreeAgentBlock,
13746
13707
  worktreeBoardStatus,
13747
13708
  worktreeCleanupSettings,
13748
13709
  worktreeDisplayLabel,