@sideboard-ai/core 0.1.32 → 0.1.34

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
@@ -3607,6 +3607,120 @@ var init_brightsy_targets = __esm({
3607
3607
  }
3608
3608
  });
3609
3609
 
3610
+ // src/agents/error-detail.ts
3611
+ function formatUnknownDetail(err) {
3612
+ if (err == null) return "";
3613
+ if (typeof err === "string") return err.trim();
3614
+ if (err instanceof Error) {
3615
+ const base = err.message.trim() || err.name;
3616
+ const code = "code" in err && typeof err.code === "string" ? err.code.trim() : "";
3617
+ return code && !base.includes(code) ? `${base} (${code})` : base;
3618
+ }
3619
+ if (typeof err === "object") {
3620
+ const o = err;
3621
+ const nested = o.error != null && typeof o.error === "object" ? formatUnknownDetail(o.error) : "";
3622
+ const message = typeof o.message === "string" ? o.message.trim() : typeof o.error === "string" ? o.error.trim() : typeof o.result === "string" ? o.result.trim() : nested;
3623
+ const code = typeof o.code === "string" ? o.code.trim() : "";
3624
+ if (message) return code && !message.includes(code) ? `${message} (${code})` : message;
3625
+ try {
3626
+ const json = JSON.stringify(err);
3627
+ if (json && json !== "{}" && json !== "null") return json;
3628
+ } catch {
3629
+ }
3630
+ }
3631
+ const fallback = String(err);
3632
+ return fallback === "[object Object]" ? "" : fallback;
3633
+ }
3634
+ function extractJsonErrorMessage(obj) {
3635
+ const nested = obj.error != null && typeof obj.error === "object" ? obj.error : null;
3636
+ const candidates = [
3637
+ typeof obj.message === "string" ? obj.message : null,
3638
+ typeof obj.error === "string" ? obj.error : null,
3639
+ nested && typeof nested.message === "string" ? nested.message : null,
3640
+ typeof obj.result === "string" ? obj.result : null,
3641
+ typeof obj.detail === "string" ? obj.detail : null
3642
+ ];
3643
+ for (const c of candidates) {
3644
+ const t = c?.trim();
3645
+ if (t) return t;
3646
+ }
3647
+ if (Array.isArray(obj.errors)) {
3648
+ const parts = obj.errors.map((e) => formatUnknownDetail(e)).map((s) => s.trim()).filter(Boolean);
3649
+ if (parts.length) return parts.join("; ");
3650
+ }
3651
+ return null;
3652
+ }
3653
+ function pushTurnStderr(tail, line, maxLines = 12) {
3654
+ const trimmed = line.trim();
3655
+ if (!trimmed) return;
3656
+ if (NODE_VERSION_FOOTER.test(trimmed)) return;
3657
+ if (/^reconnecting\.\.\./i.test(trimmed)) return;
3658
+ tail.push(trimmed);
3659
+ while (tail.length > maxLines) tail.shift();
3660
+ }
3661
+ function summarizeTurnStderr(tail, maxChars = 500) {
3662
+ if (tail.length === 0) return "";
3663
+ const joined = tail.slice(-6).join("\n").trim();
3664
+ if (joined.length <= maxChars) return joined;
3665
+ return joined.slice(joined.length - maxChars);
3666
+ }
3667
+ function looksLikeAgentFailureMessage(text) {
3668
+ const lower = text.trim().toLowerCase();
3669
+ if (!lower) return false;
3670
+ return /you've hit your|hit your (session|weekly|opus) limit|usage limit/.test(lower) || /credit balance is too low|out of credits|insufficient.?quota|quota.?exceeded/.test(lower) || /invalid user api key|invalid api key|not logged in|not authenticated|unauthorized/.test(
3671
+ lower
3672
+ ) || /\b429\b|too many requests|rate.?limit/.test(lower) || /prompt is too long|context.*(too long|exceed)|conversation too long/.test(lower);
3673
+ }
3674
+ function fallbackTurnFailDetail(assistantText) {
3675
+ const t = assistantText.trim();
3676
+ if (!t) return "";
3677
+ if (looksLikeAgentFailureMessage(t)) return t;
3678
+ if (t.length <= 400 && !/\n\n/.test(t)) return t;
3679
+ return "";
3680
+ }
3681
+ function humanizeAgentFailDetail(detail) {
3682
+ const raw = detail.trim();
3683
+ if (!raw) return raw;
3684
+ const lower = raw.toLowerCase();
3685
+ if (/credit balance is too low|out of credits|insufficient.?quota|quota.?exceeded|billing/.test(lower)) {
3686
+ return `${raw} \u2014 add credits or switch auth, then retry.`;
3687
+ }
3688
+ if (/hit your (session|weekly|opus) limit|usage limit|you've hit your/.test(lower)) {
3689
+ return raw.includes("reset") ? raw : `${raw} \u2014 wait for the limit window to reset, then retry.`;
3690
+ }
3691
+ if (/\b429\b|rate.?limit|too many requests/.test(lower)) {
3692
+ return `${raw} \u2014 wait a moment and retry.`;
3693
+ }
3694
+ if (/invalid user api key|invalid api key|not logged in|not authenticated|unauthorized|authentication|please run.*login|codex login|claude auth|cursor api/.test(
3695
+ lower
3696
+ )) {
3697
+ return `${raw} \u2014 check agent login / API key in Settings.`;
3698
+ }
3699
+ if (/model .{0,80}(not found|unavailable|unknown|invalid)/.test(lower)) {
3700
+ return `${raw} \u2014 pick another model in the agent options.`;
3701
+ }
3702
+ if (/context.*(too long|exceed)|prompt is too long|conversation too long/.test(lower)) {
3703
+ return `${raw} \u2014 start a new chat or compact context, then retry.`;
3704
+ }
3705
+ return raw;
3706
+ }
3707
+ function formatTurnExitError(exitCode, stderrSummary) {
3708
+ const code = exitCode ?? 1;
3709
+ const detail = humanizeAgentFailDetail(stderrSummary);
3710
+ if (!detail) {
3711
+ return `exit ${code}: agent exited without details (credits, auth, rate limits, or a CLI error)`;
3712
+ }
3713
+ if (looksLikeAgentFailureMessage(stderrSummary)) return detail;
3714
+ return `exit ${code}: ${detail}`;
3715
+ }
3716
+ var NODE_VERSION_FOOTER;
3717
+ var init_error_detail = __esm({
3718
+ "src/agents/error-detail.ts"() {
3719
+ "use strict";
3720
+ NODE_VERSION_FOOTER = /^Node\.js v\d+/i;
3721
+ }
3722
+ });
3723
+
3610
3724
  // src/agents/turn-input.ts
3611
3725
  function normalizeTurnInput(input) {
3612
3726
  if (typeof input === "string") return { prompt: input };
@@ -3697,7 +3811,7 @@ function parseBrightsyCliLine(line) {
3697
3811
  return { type: "thinking", data: obj.text };
3698
3812
  }
3699
3813
  if (obj.type === "error") {
3700
- const msg = String(obj.error ?? trimmed);
3814
+ const msg = extractJsonErrorMessage(obj) || formatUnknownDetail(obj.error) || formatUnknownDetail(obj.message) || trimmed;
3701
3815
  return [
3702
3816
  { type: "stderr", data: msg },
3703
3817
  { type: "stdout", data: `Error: ${msg}` }
@@ -3734,6 +3848,12 @@ function parseBrightsyCliLine(line) {
3734
3848
  if (trimmed.startsWith("{") && /"type"\s*:\s*"(tool_use|tool_result|tool|text|thinking|usage|done|error)"/.test(trimmed)) {
3735
3849
  return null;
3736
3850
  }
3851
+ if (/error|failed|unauthorized|quota|limit|not logged in/i.test(trimmed)) {
3852
+ return [
3853
+ { type: "stderr", data: trimmed },
3854
+ { type: "stdout", data: `Error: ${trimmed}` }
3855
+ ];
3856
+ }
3737
3857
  return { type: "stdout", data: line };
3738
3858
  }
3739
3859
  }
@@ -3888,6 +4008,7 @@ var init_brightsy = __esm({
3888
4008
  init_connected_teams();
3889
4009
  init_config();
3890
4010
  init_brightsy_targets();
4011
+ init_error_detail();
3891
4012
  init_turn_input();
3892
4013
  init_brightsy_targets();
3893
4014
  brightsyAdapter = {
@@ -4226,6 +4347,30 @@ function usageFromClaude(usage) {
4226
4347
  cacheWriteTokens: usage.cache_creation_input_tokens ? Number(usage.cache_creation_input_tokens) : void 0
4227
4348
  };
4228
4349
  }
4350
+ function claudeResultErrorDetail(obj) {
4351
+ const isError = Boolean(obj.is_error) || typeof obj.subtype === "string" && /^error/i.test(obj.subtype);
4352
+ const fromResult = typeof obj.result === "string" ? obj.result.trim() : "";
4353
+ if (fromResult && (isError || looksLikeAgentFailureMessage(fromResult))) {
4354
+ return fromResult;
4355
+ }
4356
+ if (!isError) return null;
4357
+ const errors = obj.errors;
4358
+ if (Array.isArray(errors)) {
4359
+ const parts = errors.map((e) => {
4360
+ if (typeof e === "string") return e.trim();
4361
+ if (e && typeof e === "object" && typeof e.message === "string") {
4362
+ return e.message.trim();
4363
+ }
4364
+ return "";
4365
+ }).filter(Boolean);
4366
+ if (parts.length) return parts.join("; ");
4367
+ }
4368
+ if (typeof obj.error === "string" && obj.error.trim()) return obj.error.trim();
4369
+ if (typeof obj.subtype === "string" && obj.subtype) {
4370
+ return obj.subtype.replace(/^error[_-]?/i, "").replace(/_/g, " ") || "Claude turn failed";
4371
+ }
4372
+ return "Claude turn failed";
4373
+ }
4229
4374
  function eventsFromContentBlocks(blocks) {
4230
4375
  if (!blocks?.length) return [];
4231
4376
  const out = [];
@@ -4295,6 +4440,7 @@ var init_claude = __esm({
4295
4440
  init_run();
4296
4441
  init_app_settings();
4297
4442
  init_claude_mcp();
4443
+ init_error_detail();
4298
4444
  init_injected_mcp();
4299
4445
  init_turn_input();
4300
4446
  init_types();
@@ -4489,8 +4635,13 @@ var init_claude = __esm({
4489
4635
  }
4490
4636
  if (obj.type === "result") {
4491
4637
  const events = [];
4492
- const text = obj.result;
4493
- if (typeof text === "string" && text) events.push({ type: "stdout", data: text });
4638
+ const errorDetail = claudeResultErrorDetail(obj);
4639
+ if (errorDetail) {
4640
+ events.push({ type: "stderr", data: errorDetail });
4641
+ } else {
4642
+ const text = obj.result;
4643
+ if (typeof text === "string" && text) events.push({ type: "stdout", data: text });
4644
+ }
4494
4645
  const usage = usageFromClaude(obj.usage);
4495
4646
  if (usage) events.push({ type: "usage", data: usage });
4496
4647
  if (events.length === 0) return null;
@@ -4598,6 +4749,7 @@ var init_codex = __esm({
4598
4749
  import_node_os7 = require("os");
4599
4750
  import_node_path12 = require("path");
4600
4751
  init_run();
4752
+ init_error_detail();
4601
4753
  init_turn_input();
4602
4754
  init_types();
4603
4755
  CODEX_PROMPT_ARG_MAX = 2e5;
@@ -4682,23 +4834,49 @@ var init_codex = __esm({
4682
4834
  if (!trimmed) return null;
4683
4835
  try {
4684
4836
  const obj = JSON.parse(trimmed);
4685
- const sid = typeof obj.session_id === "string" && obj.session_id || typeof obj.thread_id === "string" && obj.thread_id || typeof obj.session?.id === "string" && obj.session.id;
4686
- if (sid) return { type: "session_id", data: sid };
4687
- if (obj.type === "turn.completed" || obj.type === "turn_completed") {
4688
- const usage = usageFromCodex(obj.usage);
4689
- return usage ? { type: "usage", data: usage } : null;
4837
+ const type = typeof obj.type === "string" ? obj.type : "";
4838
+ if (type === "turn.failed" || type === "turn_failed") {
4839
+ const detail = extractJsonErrorMessage(obj) || extractJsonErrorMessage(obj.error ?? {}) || "Codex turn failed";
4840
+ return { type: "stderr", data: detail };
4841
+ }
4842
+ if (type === "error") {
4843
+ const detail = extractJsonErrorMessage(obj) || trimmed;
4844
+ if (/^reconnecting\.\.\./i.test(detail)) return null;
4845
+ return { type: "stderr", data: detail };
4690
4846
  }
4691
4847
  if (typeof obj.item === "object" && obj.item !== null) {
4692
4848
  const item = obj.item;
4849
+ if (item.type === "error") {
4850
+ const detail = item.message?.trim() || extractJsonErrorMessage(obj) || "Codex item error";
4851
+ return { type: "stderr", data: detail };
4852
+ }
4693
4853
  if (item.type === "agent_message" && item.text) {
4694
4854
  return { type: "stdout", data: item.text };
4695
4855
  }
4856
+ if (item.status === "failed") {
4857
+ const detail = item.message?.trim() || extractJsonErrorMessage(item) || `Codex ${item.type ?? "item"} failed`;
4858
+ return { type: "stderr", data: detail };
4859
+ }
4860
+ }
4861
+ const sid = typeof obj.session_id === "string" && obj.session_id || typeof obj.thread_id === "string" && obj.thread_id || typeof obj.session?.id === "string" && obj.session.id;
4862
+ if (sid && (type === "thread.started" || type === "session" || !type)) {
4863
+ return { type: "session_id", data: sid };
4864
+ }
4865
+ if (sid && type.endsWith(".started")) {
4866
+ return { type: "session_id", data: sid };
4696
4867
  }
4697
- if (typeof obj.content === "string") {
4868
+ if (type === "turn.completed" || type === "turn_completed") {
4869
+ const usage = usageFromCodex(obj.usage);
4870
+ return usage ? { type: "usage", data: usage } : null;
4871
+ }
4872
+ if (typeof obj.content === "string" && obj.content.trim()) {
4698
4873
  return { type: "stdout", data: obj.content };
4699
4874
  }
4700
- return { type: "stdout", data: trimmed };
4875
+ return null;
4701
4876
  } catch {
4877
+ if (/error|failed|unauthorized|quota|limit/i.test(trimmed)) {
4878
+ return { type: "stderr", data: trimmed };
4879
+ }
4702
4880
  return { type: "stdout", data: line };
4703
4881
  }
4704
4882
  },
@@ -4814,7 +4992,12 @@ function cursorSdkMessageToEvents(msg) {
4814
4992
  if (usage) return [{ type: "usage", data: usage }];
4815
4993
  }
4816
4994
  if (msg.type === "status" && msg.status === "ERROR") {
4817
- const detail = msg.message || msg.text || "Cursor run entered ERROR status";
4995
+ const rawMessage = msg.message;
4996
+ const detail = (typeof rawMessage === "string" ? rawMessage.trim() : "") || extractJsonErrorMessage(msg) || formatUnknownDetail(msg.error) || msg.text || "Cursor run entered ERROR status";
4997
+ return [{ type: "stderr", data: detail }];
4998
+ }
4999
+ if (msg.type === "error") {
5000
+ const detail = extractJsonErrorMessage(msg) || formatUnknownDetail(msg.error) || msg.text || "Cursor run error";
4818
5001
  return [{ type: "stderr", data: detail }];
4819
5002
  }
4820
5003
  return [];
@@ -4839,6 +5022,7 @@ function parseCursorRunnerLine(line) {
4839
5022
  var init_cursor_events = __esm({
4840
5023
  "src/agents/cursor-events.ts"() {
4841
5024
  "use strict";
5025
+ init_error_detail();
4842
5026
  }
4843
5027
  });
4844
5028
 
@@ -5071,6 +5255,7 @@ var init_opencode = __esm({
5071
5255
  "src/agents/opencode.ts"() {
5072
5256
  "use strict";
5073
5257
  init_run();
5258
+ init_error_detail();
5074
5259
  init_turn_input();
5075
5260
  init_types();
5076
5261
  FALLBACK_OPENCODE_MODELS = [
@@ -5148,6 +5333,10 @@ var init_opencode = __esm({
5148
5333
  if (!trimmed) return null;
5149
5334
  try {
5150
5335
  const obj = JSON.parse(trimmed);
5336
+ if (obj.type === "error") {
5337
+ const detail = extractJsonErrorMessage(obj) || formatUnknownDetail(obj.error) || formatUnknownDetail(obj.message) || trimmed;
5338
+ return { type: "stderr", data: detail };
5339
+ }
5151
5340
  const sid = typeof obj.sessionID === "string" && obj.sessionID || typeof obj.sessionId === "string" && obj.sessionId || typeof obj.session_id === "string" && obj.session_id;
5152
5341
  if (sid) return { type: "session_id", data: sid };
5153
5342
  if (obj.type === "text") {
@@ -5171,12 +5360,6 @@ var init_opencode = __esm({
5171
5360
  content: part?.output ?? part?.content ?? obj.output ?? obj.content
5172
5361
  };
5173
5362
  }
5174
- if (obj.type === "error") {
5175
- return {
5176
- type: "stderr",
5177
- data: String(obj.error ?? trimmed)
5178
- };
5179
- }
5180
5363
  if (obj.type === "step_finish" || obj.type === "step-finish") {
5181
5364
  const part = obj.part;
5182
5365
  const usage = usageFromOpencode(
@@ -5184,8 +5367,11 @@ var init_opencode = __esm({
5184
5367
  );
5185
5368
  return usage ? { type: "usage", data: usage } : null;
5186
5369
  }
5187
- return { type: "stdout", data: trimmed };
5370
+ return null;
5188
5371
  } catch {
5372
+ if (/error|failed|unauthorized|quota|limit/i.test(trimmed)) {
5373
+ return { type: "stderr", data: trimmed };
5374
+ }
5189
5375
  return { type: "stdout", data: line };
5190
5376
  }
5191
5377
  },
@@ -6667,6 +6853,49 @@ function pipeLines(stream, onLine) {
6667
6853
  const rl = (0, import_node_readline2.createInterface)({ input: stream });
6668
6854
  rl.on("line", onLine);
6669
6855
  }
6856
+ function killScriptTree(child, ports = []) {
6857
+ const pid = child.pid;
6858
+ if (pid) {
6859
+ try {
6860
+ if (process.platform === "win32") {
6861
+ void (0, import_execa3.execa)("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
6862
+ } else {
6863
+ process.kill(-pid, "SIGTERM");
6864
+ setTimeout(() => {
6865
+ try {
6866
+ process.kill(-pid, "SIGKILL");
6867
+ } catch {
6868
+ }
6869
+ }, 2500).unref?.();
6870
+ }
6871
+ } catch {
6872
+ try {
6873
+ child.kill("SIGTERM");
6874
+ } catch {
6875
+ }
6876
+ }
6877
+ }
6878
+ for (const port of ports) {
6879
+ if (!Number.isFinite(port) || port <= 0) continue;
6880
+ if (process.platform === "win32") {
6881
+ void (0, import_execa3.execa)(
6882
+ "powershell",
6883
+ [
6884
+ "-NoProfile",
6885
+ "-Command",
6886
+ `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
6887
+ ],
6888
+ { reject: false }
6889
+ );
6890
+ } else {
6891
+ void (0, import_execa3.execa)(
6892
+ "zsh",
6893
+ ["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
6894
+ { reject: false }
6895
+ );
6896
+ }
6897
+ }
6898
+ }
6670
6899
  async function spawnWorkspaceScript(command, opts) {
6671
6900
  const loginEnv = await captureLoginEnv();
6672
6901
  const env = buildWorkspaceScriptEnv(
@@ -6683,18 +6912,17 @@ async function spawnWorkspaceScript(command, opts) {
6683
6912
  const child = (0, import_execa3.execa)(shell, ["-lc", command], {
6684
6913
  cwd: opts.worktreePath,
6685
6914
  reject: false,
6686
- env
6915
+ env,
6916
+ // Own process group so Stop can tear down the whole tree (not just the shell).
6917
+ // There is no settings.toml `stop=` / teardown hook for run scripts.
6918
+ ...process.platform === "win32" ? {} : { detached: true }
6687
6919
  });
6688
6920
  pipeLines(child.stdout, opts.onLine);
6689
6921
  pipeLines(child.stderr, opts.onLine);
6922
+ const ports = opts.ports ?? [];
6690
6923
  return {
6691
6924
  pid: child.pid,
6692
- kill: () => {
6693
- try {
6694
- child.kill("SIGTERM");
6695
- } catch {
6696
- }
6697
- },
6925
+ kill: () => killScriptTree(child, ports),
6698
6926
  done: child.then((r) => r.exitCode ?? null),
6699
6927
  child
6700
6928
  };
@@ -8860,6 +9088,7 @@ async function importConductorWorkspaceAsync(workspaceId) {
8860
9088
  // src/orchestrator/orchestrator.ts
8861
9089
  var import_node_events = require("events");
8862
9090
  var import_node_fs24 = require("fs");
9091
+ init_error_detail();
8863
9092
  init_agents();
8864
9093
  init_worktree();
8865
9094
 
@@ -9155,6 +9384,11 @@ var Orchestrator = class {
9155
9384
  * the killed turn's handle.done resolves.
9156
9385
  */
9157
9386
  stoppedTurns = /* @__PURE__ */ new Set();
9387
+ /**
9388
+ * Pause drainQueue after the in-flight turn unwinds (Stop with a preserved
9389
+ * queue). Cleared when the user sends or promotes a queued message again.
9390
+ */
9391
+ haltDrain = /* @__PURE__ */ new Set();
9158
9392
  /** WIP snapshot SHA at the start of the latest agent turn (per thread). */
9159
9393
  turnBaselines = /* @__PURE__ */ new Map();
9160
9394
  maxConcurrent;
@@ -9222,7 +9456,7 @@ var Orchestrator = class {
9222
9456
  } catch {
9223
9457
  }
9224
9458
  for (const thread of listThreads()) {
9225
- if (thread.queue.length > 0) {
9459
+ if (thread.queue.length > 0 && thread.status !== "stopped") {
9226
9460
  void this.drainQueue(thread.id);
9227
9461
  }
9228
9462
  }
@@ -9283,6 +9517,7 @@ var Orchestrator = class {
9283
9517
  return withThreadLock(thread.id, async () => {
9284
9518
  const current = this.requireThread(thread.id);
9285
9519
  const queue = [...current.queue, prompt];
9520
+ this.haltDrain.delete(thread.id);
9286
9521
  updateThread(thread.id, { queue, status: "queued" });
9287
9522
  this.emit({ type: "queue_changed", threadId: thread.id, queue });
9288
9523
  this.emit({ type: "status_changed", threadId: thread.id, status: "queued" });
@@ -9297,11 +9532,75 @@ var Orchestrator = class {
9297
9532
  }
9298
9533
  return results;
9299
9534
  }
9535
+ /** Edit the text of a not-yet-started queued message. */
9536
+ async editQueuedMessage(threadRef, index, text) {
9537
+ const thread = this.requireThread(threadRef);
9538
+ return withThreadLock(thread.id, async () => {
9539
+ const current = this.requireThread(thread.id);
9540
+ const trimmed = text.trim();
9541
+ if (!trimmed || index < 0 || index >= current.queue.length) {
9542
+ return current;
9543
+ }
9544
+ const queue = current.queue.map((p, i) => i === index ? trimmed : p);
9545
+ updateThread(thread.id, { queue });
9546
+ this.emit({ type: "queue_changed", threadId: thread.id, queue });
9547
+ return this.requireThread(thread.id);
9548
+ });
9549
+ }
9550
+ /** Remove a not-yet-started queued message. */
9551
+ async removeQueuedMessage(threadRef, index) {
9552
+ const thread = this.requireThread(threadRef);
9553
+ return withThreadLock(thread.id, async () => {
9554
+ const current = this.requireThread(thread.id);
9555
+ if (index < 0 || index >= current.queue.length) return current;
9556
+ const queue = current.queue.filter((_, i) => i !== index);
9557
+ const stillQueued = queue.length > 0;
9558
+ const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
9559
+ updateThread(thread.id, {
9560
+ queue,
9561
+ status: !stillQueued && !inFlight && current.status === "queued" ? "idle" : current.status
9562
+ });
9563
+ this.emit({ type: "queue_changed", threadId: thread.id, queue });
9564
+ const next = this.requireThread(thread.id);
9565
+ this.emit({ type: "status_changed", threadId: thread.id, status: next.status });
9566
+ return next;
9567
+ });
9568
+ }
9569
+ /**
9570
+ * Promote a queued message to run next, interrupting the in-flight turn (if any).
9571
+ * The current turn is stopped without clearing the rest of the queue — drainQueue
9572
+ * picks the promoted message up as soon as the interrupted turn unwinds.
9573
+ */
9574
+ async sendQueuedMessageNow(threadRef, index) {
9575
+ const thread = this.requireThread(threadRef);
9576
+ const promoted = await withThreadLock(thread.id, async () => {
9577
+ const current = this.requireThread(thread.id);
9578
+ if (index < 0 || index >= current.queue.length) return false;
9579
+ const item = current.queue[index];
9580
+ const rest = current.queue.filter((_, i) => i !== index);
9581
+ const queue = [item, ...rest];
9582
+ this.haltDrain.delete(thread.id);
9583
+ updateThread(thread.id, { queue });
9584
+ this.emit({ type: "queue_changed", threadId: thread.id, queue });
9585
+ return true;
9586
+ });
9587
+ if (!promoted) return this.requireThread(thread.id);
9588
+ const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
9589
+ if (inFlight) {
9590
+ this.stop(thread.id, { clearQueue: false, continueQueue: true });
9591
+ } else {
9592
+ void this.drainQueue(thread.id);
9593
+ }
9594
+ return this.requireThread(thread.id);
9595
+ }
9300
9596
  async drainQueue(threadId) {
9301
9597
  if (this.draining.has(threadId)) return;
9302
9598
  this.draining.add(threadId);
9303
9599
  try {
9304
9600
  while (true) {
9601
+ if (this.haltDrain.has(threadId)) {
9602
+ break;
9603
+ }
9305
9604
  const thread = readThread(threadId);
9306
9605
  if (!thread || thread.queue.length === 0) {
9307
9606
  if (thread && thread.status === "queued") {
@@ -9314,7 +9613,7 @@ var Orchestrator = class {
9314
9613
  await new Promise((r) => setTimeout(r, 250));
9315
9614
  continue;
9316
9615
  }
9317
- if (this.activeTurns.has(threadId)) {
9616
+ if (this.activeTurns.has(threadId) || this.startingTurns.has(threadId)) {
9318
9617
  await new Promise((r) => setTimeout(r, 100));
9319
9618
  continue;
9320
9619
  }
@@ -9449,7 +9748,7 @@ var Orchestrator = class {
9449
9748
  ...fresh.agent === "claude" && fresh.sessionId ? [] : [instructions, seed]
9450
9749
  ].filter(Boolean).join("\n\n---\n\n");
9451
9750
  try {
9452
- let lastStderr = "";
9751
+ const stderrTail = [];
9453
9752
  const handle = await spawnAgentTurn(
9454
9753
  fresh,
9455
9754
  { cachedPrefix, prompt: agentPrompt },
@@ -9458,8 +9757,8 @@ var Orchestrator = class {
9458
9757
  if (event.type === "session_id") {
9459
9758
  updateThread(threadId, { sessionId: event.data });
9460
9759
  }
9461
- if (event.type === "stderr" && typeof event.data === "string" && event.data.trim()) {
9462
- lastStderr = event.data.trim();
9760
+ if (event.type === "stderr" && typeof event.data === "string") {
9761
+ pushTurnStderr(stderrTail, event.data);
9463
9762
  }
9464
9763
  }
9465
9764
  );
@@ -9484,10 +9783,12 @@ var Orchestrator = class {
9484
9783
  if (result.sessionId) {
9485
9784
  updateThread(threadId, { sessionId: result.sessionId });
9486
9785
  }
9487
- if (result.assistantText.trim() || result.parts.length > 0) {
9786
+ const assistantText = result.assistantText.trim();
9787
+ const failureOnlyMessage = result.exitCode !== 0 && looksLikeAgentFailureMessage(assistantText) && !result.parts.some((p) => p.type === "tool" || p.type === "thinking");
9788
+ if (!failureOnlyMessage && (assistantText || result.parts.length > 0)) {
9488
9789
  appendMessage(threadId, {
9489
9790
  role: "agent",
9490
- text: result.assistantText.trim(),
9791
+ text: assistantText,
9491
9792
  parts: result.parts.length > 0 ? result.parts : void 0,
9492
9793
  durationMs: Math.max(0, Date.now() - turnStartedAt),
9493
9794
  usage: result.usage ?? void 0,
@@ -9506,7 +9807,9 @@ var Orchestrator = class {
9506
9807
  this.emit({ type: "status_changed", threadId, status: "stopped" });
9507
9808
  this.emit({ type: "turn_finished", threadId, exitCode: result.exitCode });
9508
9809
  } else {
9509
- const failDetail = lastStderr ? `exit ${result.exitCode}: ${lastStderr.slice(0, 500)}` : `exit ${result.exitCode}`;
9810
+ const lastStderr = summarizeTurnStderr(stderrTail);
9811
+ const detail = lastStderr || (result.exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
9812
+ const failDetail = formatTurnExitError(result.exitCode, detail);
9510
9813
  setStatus(
9511
9814
  threadId,
9512
9815
  result.exitCode === 0 ? "idle" : "error",
@@ -9542,20 +9845,30 @@ var Orchestrator = class {
9542
9845
  }
9543
9846
  /**
9544
9847
  * Stop an in-flight agent turn.
9545
- * Default `clearQueue: true` (force-stop): kills the turn AND empties queued prompts
9546
- * so drainQueue cannot continue / re-start work after an intentional stop. Desktop,
9547
- * CLI, MCP, and cloud-connect all share this default.
9848
+ *
9849
+ * - Default `clearQueue: true` (force-stop): empties queued prompts so nothing
9850
+ * resumes. Used by MCP force-stop, archive, and cloud-connect.
9851
+ * - Desktop Stop uses `{ clearQueue: false }` so follow-ups stay editable.
9852
+ * - `continueQueue: true` (Send now): keep the queue and let drainQueue resume
9853
+ * after the interrupted turn unwinds. Without it, drain pauses until send /
9854
+ * promote.
9548
9855
  */
9549
9856
  stop(threadRef, opts) {
9550
9857
  const clearQueue = opts?.clearQueue !== false;
9858
+ const continueQueue = opts?.continueQueue === true;
9551
9859
  const thread = this.requireThread(threadRef);
9552
9860
  const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
9553
9861
  if (inFlight) {
9554
9862
  this.stoppedTurns.add(thread.id);
9555
9863
  }
9556
9864
  if (clearQueue && thread.queue.length > 0) {
9865
+ this.haltDrain.delete(thread.id);
9557
9866
  updateThread(thread.id, { queue: [] });
9558
9867
  this.emit({ type: "queue_changed", threadId: thread.id, queue: [] });
9868
+ } else if (!clearQueue && !continueQueue) {
9869
+ this.haltDrain.add(thread.id);
9870
+ } else if (continueQueue) {
9871
+ this.haltDrain.delete(thread.id);
9559
9872
  }
9560
9873
  const handle = this.activeTurns.get(thread.id);
9561
9874
  if (handle) handle.kill();
@@ -10150,9 +10463,11 @@ function getOrchestrator() {
10150
10463
  }
10151
10464
  async function startOrchestration(opts) {
10152
10465
  const repoPath = opts.repoPath?.trim();
10466
+ const goal = opts.goal.trim();
10467
+ const orch = getOrchestrator();
10153
10468
  if (!repoPath || isGlobalRepoPath(repoPath)) {
10154
- return createGlobalChat({
10155
- sourceRef: opts.goal,
10469
+ const thread2 = createGlobalChat({
10470
+ sourceRef: goal,
10156
10471
  agent: opts.agent,
10157
10472
  autonomy: opts.autonomy,
10158
10473
  model: opts.model,
@@ -10160,9 +10475,13 @@ async function startOrchestration(opts) {
10160
10475
  planMode: opts.planMode,
10161
10476
  attachments: opts.attachments
10162
10477
  });
10478
+ if (goal) {
10479
+ return orch.send(thread2.id, goal);
10480
+ }
10481
+ return thread2;
10163
10482
  }
10164
10483
  const { titleFromPrompt: titleFromPrompt2 } = await Promise.resolve().then(() => (init_title(), title_exports));
10165
- const title = titleFromPrompt2(opts.goal) || "Orchestration";
10484
+ const title = titleFromPrompt2(goal) || "Orchestration";
10166
10485
  const createOpts = {
10167
10486
  agent: opts.agent,
10168
10487
  repoPath,
@@ -10189,10 +10508,14 @@ async function startOrchestration(opts) {
10189
10508
  });
10190
10509
  });
10191
10510
  const { updateThread: upd } = await Promise.resolve().then(() => (init_thread_store(), thread_store_exports));
10192
- return upd(thread.id, {
10511
+ const updated = upd(thread.id, {
10193
10512
  sourceType: "orchestration",
10194
- sourceRef: opts.goal
10513
+ sourceRef: goal
10195
10514
  });
10515
+ if (goal) {
10516
+ return orch.send(updated.id, goal);
10517
+ }
10518
+ return updated;
10196
10519
  }
10197
10520
 
10198
10521
  // src/index.ts
@@ -10288,11 +10611,15 @@ async function startMcpServer() {
10288
10611
  );
10289
10612
  server.tool(
10290
10613
  "present_artifact",
10291
- "Show an HTML, SVG, or markdown document in Sideboard\u2019s Claude-style side column (desktop). Use instead of claude.ai\u2019s artifact tool \u2014 pass the full document content. Prefer type=html for interactive pages.",
10614
+ "Show an HTML, SVG, markdown, or React document in Sideboard\u2019s Claude-style side column (desktop). Use instead of claude.ai\u2019s artifact tool \u2014 pass the full document content. Prefer type=html for interactive pages. For type=react, pass a single component module that `export default`s a component (JSX/TSX ok) \u2014 Sideboard bootstraps React/ReactDOM/Babel and renders it; only `react`/`react-dom` imports are available, no other npm packages.",
10292
10615
  {
10293
10616
  title: import_zod.z.string().describe("Short title shown in the artifact pane header"),
10294
- type: import_zod.z.enum(["html", "svg", "markdown"]).describe("Artifact kind \u2014 html opens an iframe preview"),
10295
- content: import_zod.z.string().describe("Full document body (complete HTML page, SVG markup, or markdown)"),
10617
+ type: import_zod.z.enum(["html", "svg", "markdown", "react"]).describe(
10618
+ "Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
10619
+ ),
10620
+ content: import_zod.z.string().describe(
10621
+ "Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
10622
+ ),
10296
10623
  artifact_id: import_zod.z.string().optional().describe("Stable id when updating the same artifact across turns")
10297
10624
  },
10298
10625
  async ({ title, type, content, artifact_id }) => {