@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.
@@ -30,6 +30,120 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  mod
31
31
  ));
32
32
 
33
+ // src/agents/error-detail.ts
34
+ function formatUnknownDetail(err) {
35
+ if (err == null) return "";
36
+ if (typeof err === "string") return err.trim();
37
+ if (err instanceof Error) {
38
+ const base = err.message.trim() || err.name;
39
+ const code = "code" in err && typeof err.code === "string" ? err.code.trim() : "";
40
+ return code && !base.includes(code) ? `${base} (${code})` : base;
41
+ }
42
+ if (typeof err === "object") {
43
+ const o = err;
44
+ const nested = o.error != null && typeof o.error === "object" ? formatUnknownDetail(o.error) : "";
45
+ const message = typeof o.message === "string" ? o.message.trim() : typeof o.error === "string" ? o.error.trim() : typeof o.result === "string" ? o.result.trim() : nested;
46
+ const code = typeof o.code === "string" ? o.code.trim() : "";
47
+ if (message) return code && !message.includes(code) ? `${message} (${code})` : message;
48
+ try {
49
+ const json = JSON.stringify(err);
50
+ if (json && json !== "{}" && json !== "null") return json;
51
+ } catch {
52
+ }
53
+ }
54
+ const fallback = String(err);
55
+ return fallback === "[object Object]" ? "" : fallback;
56
+ }
57
+ function extractJsonErrorMessage(obj) {
58
+ const nested = obj.error != null && typeof obj.error === "object" ? obj.error : null;
59
+ const candidates = [
60
+ typeof obj.message === "string" ? obj.message : null,
61
+ typeof obj.error === "string" ? obj.error : null,
62
+ nested && typeof nested.message === "string" ? nested.message : null,
63
+ typeof obj.result === "string" ? obj.result : null,
64
+ typeof obj.detail === "string" ? obj.detail : null
65
+ ];
66
+ for (const c of candidates) {
67
+ const t = c?.trim();
68
+ if (t) return t;
69
+ }
70
+ if (Array.isArray(obj.errors)) {
71
+ const parts = obj.errors.map((e) => formatUnknownDetail(e)).map((s) => s.trim()).filter(Boolean);
72
+ if (parts.length) return parts.join("; ");
73
+ }
74
+ return null;
75
+ }
76
+ function pushTurnStderr(tail, line, maxLines = 12) {
77
+ const trimmed = line.trim();
78
+ if (!trimmed) return;
79
+ if (NODE_VERSION_FOOTER.test(trimmed)) return;
80
+ if (/^reconnecting\.\.\./i.test(trimmed)) return;
81
+ tail.push(trimmed);
82
+ while (tail.length > maxLines) tail.shift();
83
+ }
84
+ function summarizeTurnStderr(tail, maxChars = 500) {
85
+ if (tail.length === 0) return "";
86
+ const joined = tail.slice(-6).join("\n").trim();
87
+ if (joined.length <= maxChars) return joined;
88
+ return joined.slice(joined.length - maxChars);
89
+ }
90
+ function looksLikeAgentFailureMessage(text) {
91
+ const lower = text.trim().toLowerCase();
92
+ if (!lower) return false;
93
+ 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(
94
+ lower
95
+ ) || /\b429\b|too many requests|rate.?limit/.test(lower) || /prompt is too long|context.*(too long|exceed)|conversation too long/.test(lower);
96
+ }
97
+ function fallbackTurnFailDetail(assistantText) {
98
+ const t = assistantText.trim();
99
+ if (!t) return "";
100
+ if (looksLikeAgentFailureMessage(t)) return t;
101
+ if (t.length <= 400 && !/\n\n/.test(t)) return t;
102
+ return "";
103
+ }
104
+ function humanizeAgentFailDetail(detail) {
105
+ const raw = detail.trim();
106
+ if (!raw) return raw;
107
+ const lower = raw.toLowerCase();
108
+ if (/credit balance is too low|out of credits|insufficient.?quota|quota.?exceeded|billing/.test(lower)) {
109
+ return `${raw} \u2014 add credits or switch auth, then retry.`;
110
+ }
111
+ if (/hit your (session|weekly|opus) limit|usage limit|you've hit your/.test(lower)) {
112
+ return raw.includes("reset") ? raw : `${raw} \u2014 wait for the limit window to reset, then retry.`;
113
+ }
114
+ if (/\b429\b|rate.?limit|too many requests/.test(lower)) {
115
+ return `${raw} \u2014 wait a moment and retry.`;
116
+ }
117
+ 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(
118
+ lower
119
+ )) {
120
+ return `${raw} \u2014 check agent login / API key in Settings.`;
121
+ }
122
+ if (/model .{0,80}(not found|unavailable|unknown|invalid)/.test(lower)) {
123
+ return `${raw} \u2014 pick another model in the agent options.`;
124
+ }
125
+ if (/context.*(too long|exceed)|prompt is too long|conversation too long/.test(lower)) {
126
+ return `${raw} \u2014 start a new chat or compact context, then retry.`;
127
+ }
128
+ return raw;
129
+ }
130
+ function formatTurnExitError(exitCode, stderrSummary) {
131
+ const code = exitCode ?? 1;
132
+ const detail = humanizeAgentFailDetail(stderrSummary);
133
+ if (!detail) {
134
+ return `exit ${code}: agent exited without details (credits, auth, rate limits, or a CLI error)`;
135
+ }
136
+ if (looksLikeAgentFailureMessage(stderrSummary)) return detail;
137
+ return `exit ${code}: ${detail}`;
138
+ }
139
+ var NODE_VERSION_FOOTER;
140
+ var init_error_detail = __esm({
141
+ "src/agents/error-detail.ts"() {
142
+ "use strict";
143
+ NODE_VERSION_FOOTER = /^Node\.js v\d+/i;
144
+ }
145
+ });
146
+
33
147
  // src/hook/settings.ts
34
148
  function expandHome(path) {
35
149
  if (path.startsWith("~/") || path === "~") {
@@ -3343,7 +3457,7 @@ function parseBrightsyCliLine(line) {
3343
3457
  return { type: "thinking", data: obj.text };
3344
3458
  }
3345
3459
  if (obj.type === "error") {
3346
- const msg = String(obj.error ?? trimmed);
3460
+ const msg = extractJsonErrorMessage(obj) || formatUnknownDetail(obj.error) || formatUnknownDetail(obj.message) || trimmed;
3347
3461
  return [
3348
3462
  { type: "stderr", data: msg },
3349
3463
  { type: "stdout", data: `Error: ${msg}` }
@@ -3380,6 +3494,12 @@ function parseBrightsyCliLine(line) {
3380
3494
  if (trimmed.startsWith("{") && /"type"\s*:\s*"(tool_use|tool_result|tool|text|thinking|usage|done|error)"/.test(trimmed)) {
3381
3495
  return null;
3382
3496
  }
3497
+ if (/error|failed|unauthorized|quota|limit|not logged in/i.test(trimmed)) {
3498
+ return [
3499
+ { type: "stderr", data: trimmed },
3500
+ { type: "stdout", data: `Error: ${trimmed}` }
3501
+ ];
3502
+ }
3383
3503
  return { type: "stdout", data: line };
3384
3504
  }
3385
3505
  }
@@ -3534,6 +3654,7 @@ var init_brightsy = __esm({
3534
3654
  init_connected_teams();
3535
3655
  init_config();
3536
3656
  init_brightsy_targets();
3657
+ init_error_detail();
3537
3658
  init_turn_input();
3538
3659
  init_brightsy_targets();
3539
3660
  brightsyAdapter = {
@@ -3865,6 +3986,30 @@ function usageFromClaude(usage) {
3865
3986
  cacheWriteTokens: usage.cache_creation_input_tokens ? Number(usage.cache_creation_input_tokens) : void 0
3866
3987
  };
3867
3988
  }
3989
+ function claudeResultErrorDetail(obj) {
3990
+ const isError = Boolean(obj.is_error) || typeof obj.subtype === "string" && /^error/i.test(obj.subtype);
3991
+ const fromResult = typeof obj.result === "string" ? obj.result.trim() : "";
3992
+ if (fromResult && (isError || looksLikeAgentFailureMessage(fromResult))) {
3993
+ return fromResult;
3994
+ }
3995
+ if (!isError) return null;
3996
+ const errors = obj.errors;
3997
+ if (Array.isArray(errors)) {
3998
+ const parts = errors.map((e) => {
3999
+ if (typeof e === "string") return e.trim();
4000
+ if (e && typeof e === "object" && typeof e.message === "string") {
4001
+ return e.message.trim();
4002
+ }
4003
+ return "";
4004
+ }).filter(Boolean);
4005
+ if (parts.length) return parts.join("; ");
4006
+ }
4007
+ if (typeof obj.error === "string" && obj.error.trim()) return obj.error.trim();
4008
+ if (typeof obj.subtype === "string" && obj.subtype) {
4009
+ return obj.subtype.replace(/^error[_-]?/i, "").replace(/_/g, " ") || "Claude turn failed";
4010
+ }
4011
+ return "Claude turn failed";
4012
+ }
3868
4013
  function eventsFromContentBlocks(blocks) {
3869
4014
  if (!blocks?.length) return [];
3870
4015
  const out = [];
@@ -3934,6 +4079,7 @@ var init_claude = __esm({
3934
4079
  init_run();
3935
4080
  init_app_settings();
3936
4081
  init_claude_mcp();
4082
+ init_error_detail();
3937
4083
  init_injected_mcp();
3938
4084
  init_turn_input();
3939
4085
  init_types();
@@ -4128,8 +4274,13 @@ var init_claude = __esm({
4128
4274
  }
4129
4275
  if (obj.type === "result") {
4130
4276
  const events = [];
4131
- const text = obj.result;
4132
- if (typeof text === "string" && text) events.push({ type: "stdout", data: text });
4277
+ const errorDetail = claudeResultErrorDetail(obj);
4278
+ if (errorDetail) {
4279
+ events.push({ type: "stderr", data: errorDetail });
4280
+ } else {
4281
+ const text = obj.result;
4282
+ if (typeof text === "string" && text) events.push({ type: "stdout", data: text });
4283
+ }
4133
4284
  const usage = usageFromClaude(obj.usage);
4134
4285
  if (usage) events.push({ type: "usage", data: usage });
4135
4286
  if (events.length === 0) return null;
@@ -4237,6 +4388,7 @@ var init_codex = __esm({
4237
4388
  import_node_os7 = require("os");
4238
4389
  import_node_path11 = require("path");
4239
4390
  init_run();
4391
+ init_error_detail();
4240
4392
  init_turn_input();
4241
4393
  init_types();
4242
4394
  CODEX_PROMPT_ARG_MAX = 2e5;
@@ -4321,23 +4473,49 @@ var init_codex = __esm({
4321
4473
  if (!trimmed) return null;
4322
4474
  try {
4323
4475
  const obj = JSON.parse(trimmed);
4324
- 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;
4325
- if (sid) return { type: "session_id", data: sid };
4326
- if (obj.type === "turn.completed" || obj.type === "turn_completed") {
4327
- const usage = usageFromCodex(obj.usage);
4328
- return usage ? { type: "usage", data: usage } : null;
4476
+ const type = typeof obj.type === "string" ? obj.type : "";
4477
+ if (type === "turn.failed" || type === "turn_failed") {
4478
+ const detail = extractJsonErrorMessage(obj) || extractJsonErrorMessage(obj.error ?? {}) || "Codex turn failed";
4479
+ return { type: "stderr", data: detail };
4480
+ }
4481
+ if (type === "error") {
4482
+ const detail = extractJsonErrorMessage(obj) || trimmed;
4483
+ if (/^reconnecting\.\.\./i.test(detail)) return null;
4484
+ return { type: "stderr", data: detail };
4329
4485
  }
4330
4486
  if (typeof obj.item === "object" && obj.item !== null) {
4331
4487
  const item = obj.item;
4488
+ if (item.type === "error") {
4489
+ const detail = item.message?.trim() || extractJsonErrorMessage(obj) || "Codex item error";
4490
+ return { type: "stderr", data: detail };
4491
+ }
4332
4492
  if (item.type === "agent_message" && item.text) {
4333
4493
  return { type: "stdout", data: item.text };
4334
4494
  }
4495
+ if (item.status === "failed") {
4496
+ const detail = item.message?.trim() || extractJsonErrorMessage(item) || `Codex ${item.type ?? "item"} failed`;
4497
+ return { type: "stderr", data: detail };
4498
+ }
4499
+ }
4500
+ 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;
4501
+ if (sid && (type === "thread.started" || type === "session" || !type)) {
4502
+ return { type: "session_id", data: sid };
4503
+ }
4504
+ if (sid && type.endsWith(".started")) {
4505
+ return { type: "session_id", data: sid };
4506
+ }
4507
+ if (type === "turn.completed" || type === "turn_completed") {
4508
+ const usage = usageFromCodex(obj.usage);
4509
+ return usage ? { type: "usage", data: usage } : null;
4335
4510
  }
4336
- if (typeof obj.content === "string") {
4511
+ if (typeof obj.content === "string" && obj.content.trim()) {
4337
4512
  return { type: "stdout", data: obj.content };
4338
4513
  }
4339
- return { type: "stdout", data: trimmed };
4514
+ return null;
4340
4515
  } catch {
4516
+ if (/error|failed|unauthorized|quota|limit/i.test(trimmed)) {
4517
+ return { type: "stderr", data: trimmed };
4518
+ }
4341
4519
  return { type: "stdout", data: line };
4342
4520
  }
4343
4521
  },
@@ -4453,7 +4631,12 @@ function cursorSdkMessageToEvents(msg) {
4453
4631
  if (usage) return [{ type: "usage", data: usage }];
4454
4632
  }
4455
4633
  if (msg.type === "status" && msg.status === "ERROR") {
4456
- const detail = msg.message || msg.text || "Cursor run entered ERROR status";
4634
+ const rawMessage = msg.message;
4635
+ const detail = (typeof rawMessage === "string" ? rawMessage.trim() : "") || extractJsonErrorMessage(msg) || formatUnknownDetail(msg.error) || msg.text || "Cursor run entered ERROR status";
4636
+ return [{ type: "stderr", data: detail }];
4637
+ }
4638
+ if (msg.type === "error") {
4639
+ const detail = extractJsonErrorMessage(msg) || formatUnknownDetail(msg.error) || msg.text || "Cursor run error";
4457
4640
  return [{ type: "stderr", data: detail }];
4458
4641
  }
4459
4642
  return [];
@@ -4478,6 +4661,7 @@ function parseCursorRunnerLine(line) {
4478
4661
  var init_cursor_events = __esm({
4479
4662
  "src/agents/cursor-events.ts"() {
4480
4663
  "use strict";
4664
+ init_error_detail();
4481
4665
  }
4482
4666
  });
4483
4667
 
@@ -4710,6 +4894,7 @@ var init_opencode = __esm({
4710
4894
  "src/agents/opencode.ts"() {
4711
4895
  "use strict";
4712
4896
  init_run();
4897
+ init_error_detail();
4713
4898
  init_turn_input();
4714
4899
  init_types();
4715
4900
  FALLBACK_OPENCODE_MODELS = [
@@ -4787,6 +4972,10 @@ var init_opencode = __esm({
4787
4972
  if (!trimmed) return null;
4788
4973
  try {
4789
4974
  const obj = JSON.parse(trimmed);
4975
+ if (obj.type === "error") {
4976
+ const detail = extractJsonErrorMessage(obj) || formatUnknownDetail(obj.error) || formatUnknownDetail(obj.message) || trimmed;
4977
+ return { type: "stderr", data: detail };
4978
+ }
4790
4979
  const sid = typeof obj.sessionID === "string" && obj.sessionID || typeof obj.sessionId === "string" && obj.sessionId || typeof obj.session_id === "string" && obj.session_id;
4791
4980
  if (sid) return { type: "session_id", data: sid };
4792
4981
  if (obj.type === "text") {
@@ -4810,12 +4999,6 @@ var init_opencode = __esm({
4810
4999
  content: part?.output ?? part?.content ?? obj.output ?? obj.content
4811
5000
  };
4812
5001
  }
4813
- if (obj.type === "error") {
4814
- return {
4815
- type: "stderr",
4816
- data: String(obj.error ?? trimmed)
4817
- };
4818
- }
4819
5002
  if (obj.type === "step_finish" || obj.type === "step-finish") {
4820
5003
  const part = obj.part;
4821
5004
  const usage = usageFromOpencode(
@@ -4823,8 +5006,11 @@ var init_opencode = __esm({
4823
5006
  );
4824
5007
  return usage ? { type: "usage", data: usage } : null;
4825
5008
  }
4826
- return { type: "stdout", data: trimmed };
5009
+ return null;
4827
5010
  } catch {
5011
+ if (/error|failed|unauthorized|quota|limit/i.test(trimmed)) {
5012
+ return { type: "stderr", data: trimmed };
5013
+ }
4828
5014
  return { type: "stdout", data: line };
4829
5015
  }
4830
5016
  },
@@ -5267,6 +5453,7 @@ var import_node_path23 = require("path");
5267
5453
  // src/orchestrator/orchestrator.ts
5268
5454
  var import_node_events = require("events");
5269
5455
  var import_node_fs24 = require("fs");
5456
+ init_error_detail();
5270
5457
 
5271
5458
  // src/agents/spawn.ts
5272
5459
  var import_node_readline = require("readline");
@@ -5729,6 +5916,49 @@ function pipeLines(stream, onLine) {
5729
5916
  const rl = (0, import_node_readline2.createInterface)({ input: stream });
5730
5917
  rl.on("line", onLine);
5731
5918
  }
5919
+ function killScriptTree(child, ports = []) {
5920
+ const pid = child.pid;
5921
+ if (pid) {
5922
+ try {
5923
+ if (process.platform === "win32") {
5924
+ void (0, import_execa3.execa)("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
5925
+ } else {
5926
+ process.kill(-pid, "SIGTERM");
5927
+ setTimeout(() => {
5928
+ try {
5929
+ process.kill(-pid, "SIGKILL");
5930
+ } catch {
5931
+ }
5932
+ }, 2500).unref?.();
5933
+ }
5934
+ } catch {
5935
+ try {
5936
+ child.kill("SIGTERM");
5937
+ } catch {
5938
+ }
5939
+ }
5940
+ }
5941
+ for (const port of ports) {
5942
+ if (!Number.isFinite(port) || port <= 0) continue;
5943
+ if (process.platform === "win32") {
5944
+ void (0, import_execa3.execa)(
5945
+ "powershell",
5946
+ [
5947
+ "-NoProfile",
5948
+ "-Command",
5949
+ `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
5950
+ ],
5951
+ { reject: false }
5952
+ );
5953
+ } else {
5954
+ void (0, import_execa3.execa)(
5955
+ "zsh",
5956
+ ["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
5957
+ { reject: false }
5958
+ );
5959
+ }
5960
+ }
5961
+ }
5732
5962
  async function spawnWorkspaceScript(command, opts) {
5733
5963
  const loginEnv = await captureLoginEnv();
5734
5964
  const env = buildWorkspaceScriptEnv(
@@ -5745,18 +5975,17 @@ async function spawnWorkspaceScript(command, opts) {
5745
5975
  const child = (0, import_execa3.execa)(shell, ["-lc", command], {
5746
5976
  cwd: opts.worktreePath,
5747
5977
  reject: false,
5748
- env
5978
+ env,
5979
+ // Own process group so Stop can tear down the whole tree (not just the shell).
5980
+ // There is no settings.toml `stop=` / teardown hook for run scripts.
5981
+ ...process.platform === "win32" ? {} : { detached: true }
5749
5982
  });
5750
5983
  pipeLines(child.stdout, opts.onLine);
5751
5984
  pipeLines(child.stderr, opts.onLine);
5985
+ const ports = opts.ports ?? [];
5752
5986
  return {
5753
5987
  pid: child.pid,
5754
- kill: () => {
5755
- try {
5756
- child.kill("SIGTERM");
5757
- } catch {
5758
- }
5759
- },
5988
+ kill: () => killScriptTree(child, ports),
5760
5989
  done: child.then((r) => r.exitCode ?? null),
5761
5990
  child
5762
5991
  };
@@ -8282,6 +8511,11 @@ var Orchestrator = class {
8282
8511
  * the killed turn's handle.done resolves.
8283
8512
  */
8284
8513
  stoppedTurns = /* @__PURE__ */ new Set();
8514
+ /**
8515
+ * Pause drainQueue after the in-flight turn unwinds (Stop with a preserved
8516
+ * queue). Cleared when the user sends or promotes a queued message again.
8517
+ */
8518
+ haltDrain = /* @__PURE__ */ new Set();
8285
8519
  /** WIP snapshot SHA at the start of the latest agent turn (per thread). */
8286
8520
  turnBaselines = /* @__PURE__ */ new Map();
8287
8521
  maxConcurrent;
@@ -8349,7 +8583,7 @@ var Orchestrator = class {
8349
8583
  } catch {
8350
8584
  }
8351
8585
  for (const thread of listThreads()) {
8352
- if (thread.queue.length > 0) {
8586
+ if (thread.queue.length > 0 && thread.status !== "stopped") {
8353
8587
  void this.drainQueue(thread.id);
8354
8588
  }
8355
8589
  }
@@ -8410,6 +8644,7 @@ var Orchestrator = class {
8410
8644
  return withThreadLock(thread.id, async () => {
8411
8645
  const current = this.requireThread(thread.id);
8412
8646
  const queue = [...current.queue, prompt];
8647
+ this.haltDrain.delete(thread.id);
8413
8648
  updateThread(thread.id, { queue, status: "queued" });
8414
8649
  this.emit({ type: "queue_changed", threadId: thread.id, queue });
8415
8650
  this.emit({ type: "status_changed", threadId: thread.id, status: "queued" });
@@ -8424,11 +8659,75 @@ var Orchestrator = class {
8424
8659
  }
8425
8660
  return results;
8426
8661
  }
8662
+ /** Edit the text of a not-yet-started queued message. */
8663
+ async editQueuedMessage(threadRef, index, text) {
8664
+ const thread = this.requireThread(threadRef);
8665
+ return withThreadLock(thread.id, async () => {
8666
+ const current = this.requireThread(thread.id);
8667
+ const trimmed = text.trim();
8668
+ if (!trimmed || index < 0 || index >= current.queue.length) {
8669
+ return current;
8670
+ }
8671
+ const queue = current.queue.map((p, i) => i === index ? trimmed : p);
8672
+ updateThread(thread.id, { queue });
8673
+ this.emit({ type: "queue_changed", threadId: thread.id, queue });
8674
+ return this.requireThread(thread.id);
8675
+ });
8676
+ }
8677
+ /** Remove a not-yet-started queued message. */
8678
+ async removeQueuedMessage(threadRef, index) {
8679
+ const thread = this.requireThread(threadRef);
8680
+ return withThreadLock(thread.id, async () => {
8681
+ const current = this.requireThread(thread.id);
8682
+ if (index < 0 || index >= current.queue.length) return current;
8683
+ const queue = current.queue.filter((_, i) => i !== index);
8684
+ const stillQueued = queue.length > 0;
8685
+ const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
8686
+ updateThread(thread.id, {
8687
+ queue,
8688
+ status: !stillQueued && !inFlight && current.status === "queued" ? "idle" : current.status
8689
+ });
8690
+ this.emit({ type: "queue_changed", threadId: thread.id, queue });
8691
+ const next = this.requireThread(thread.id);
8692
+ this.emit({ type: "status_changed", threadId: thread.id, status: next.status });
8693
+ return next;
8694
+ });
8695
+ }
8696
+ /**
8697
+ * Promote a queued message to run next, interrupting the in-flight turn (if any).
8698
+ * The current turn is stopped without clearing the rest of the queue — drainQueue
8699
+ * picks the promoted message up as soon as the interrupted turn unwinds.
8700
+ */
8701
+ async sendQueuedMessageNow(threadRef, index) {
8702
+ const thread = this.requireThread(threadRef);
8703
+ const promoted = await withThreadLock(thread.id, async () => {
8704
+ const current = this.requireThread(thread.id);
8705
+ if (index < 0 || index >= current.queue.length) return false;
8706
+ const item = current.queue[index];
8707
+ const rest = current.queue.filter((_, i) => i !== index);
8708
+ const queue = [item, ...rest];
8709
+ this.haltDrain.delete(thread.id);
8710
+ updateThread(thread.id, { queue });
8711
+ this.emit({ type: "queue_changed", threadId: thread.id, queue });
8712
+ return true;
8713
+ });
8714
+ if (!promoted) return this.requireThread(thread.id);
8715
+ const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
8716
+ if (inFlight) {
8717
+ this.stop(thread.id, { clearQueue: false, continueQueue: true });
8718
+ } else {
8719
+ void this.drainQueue(thread.id);
8720
+ }
8721
+ return this.requireThread(thread.id);
8722
+ }
8427
8723
  async drainQueue(threadId) {
8428
8724
  if (this.draining.has(threadId)) return;
8429
8725
  this.draining.add(threadId);
8430
8726
  try {
8431
8727
  while (true) {
8728
+ if (this.haltDrain.has(threadId)) {
8729
+ break;
8730
+ }
8432
8731
  const thread = readThread(threadId);
8433
8732
  if (!thread || thread.queue.length === 0) {
8434
8733
  if (thread && thread.status === "queued") {
@@ -8441,7 +8740,7 @@ var Orchestrator = class {
8441
8740
  await new Promise((r) => setTimeout(r, 250));
8442
8741
  continue;
8443
8742
  }
8444
- if (this.activeTurns.has(threadId)) {
8743
+ if (this.activeTurns.has(threadId) || this.startingTurns.has(threadId)) {
8445
8744
  await new Promise((r) => setTimeout(r, 100));
8446
8745
  continue;
8447
8746
  }
@@ -8576,7 +8875,7 @@ var Orchestrator = class {
8576
8875
  ...fresh.agent === "claude" && fresh.sessionId ? [] : [instructions, seed]
8577
8876
  ].filter(Boolean).join("\n\n---\n\n");
8578
8877
  try {
8579
- let lastStderr = "";
8878
+ const stderrTail = [];
8580
8879
  const handle = await spawnAgentTurn(
8581
8880
  fresh,
8582
8881
  { cachedPrefix, prompt: agentPrompt },
@@ -8585,8 +8884,8 @@ var Orchestrator = class {
8585
8884
  if (event.type === "session_id") {
8586
8885
  updateThread(threadId, { sessionId: event.data });
8587
8886
  }
8588
- if (event.type === "stderr" && typeof event.data === "string" && event.data.trim()) {
8589
- lastStderr = event.data.trim();
8887
+ if (event.type === "stderr" && typeof event.data === "string") {
8888
+ pushTurnStderr(stderrTail, event.data);
8590
8889
  }
8591
8890
  }
8592
8891
  );
@@ -8611,10 +8910,12 @@ var Orchestrator = class {
8611
8910
  if (result.sessionId) {
8612
8911
  updateThread(threadId, { sessionId: result.sessionId });
8613
8912
  }
8614
- if (result.assistantText.trim() || result.parts.length > 0) {
8913
+ const assistantText = result.assistantText.trim();
8914
+ const failureOnlyMessage = result.exitCode !== 0 && looksLikeAgentFailureMessage(assistantText) && !result.parts.some((p) => p.type === "tool" || p.type === "thinking");
8915
+ if (!failureOnlyMessage && (assistantText || result.parts.length > 0)) {
8615
8916
  appendMessage(threadId, {
8616
8917
  role: "agent",
8617
- text: result.assistantText.trim(),
8918
+ text: assistantText,
8618
8919
  parts: result.parts.length > 0 ? result.parts : void 0,
8619
8920
  durationMs: Math.max(0, Date.now() - turnStartedAt),
8620
8921
  usage: result.usage ?? void 0,
@@ -8633,7 +8934,9 @@ var Orchestrator = class {
8633
8934
  this.emit({ type: "status_changed", threadId, status: "stopped" });
8634
8935
  this.emit({ type: "turn_finished", threadId, exitCode: result.exitCode });
8635
8936
  } else {
8636
- const failDetail = lastStderr ? `exit ${result.exitCode}: ${lastStderr.slice(0, 500)}` : `exit ${result.exitCode}`;
8937
+ const lastStderr = summarizeTurnStderr(stderrTail);
8938
+ const detail = lastStderr || (result.exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
8939
+ const failDetail = formatTurnExitError(result.exitCode, detail);
8637
8940
  setStatus(
8638
8941
  threadId,
8639
8942
  result.exitCode === 0 ? "idle" : "error",
@@ -8669,20 +8972,30 @@ var Orchestrator = class {
8669
8972
  }
8670
8973
  /**
8671
8974
  * Stop an in-flight agent turn.
8672
- * Default `clearQueue: true` (force-stop): kills the turn AND empties queued prompts
8673
- * so drainQueue cannot continue / re-start work after an intentional stop. Desktop,
8674
- * CLI, MCP, and cloud-connect all share this default.
8975
+ *
8976
+ * - Default `clearQueue: true` (force-stop): empties queued prompts so nothing
8977
+ * resumes. Used by MCP force-stop, archive, and cloud-connect.
8978
+ * - Desktop Stop uses `{ clearQueue: false }` so follow-ups stay editable.
8979
+ * - `continueQueue: true` (Send now): keep the queue and let drainQueue resume
8980
+ * after the interrupted turn unwinds. Without it, drain pauses until send /
8981
+ * promote.
8675
8982
  */
8676
8983
  stop(threadRef, opts) {
8677
8984
  const clearQueue = opts?.clearQueue !== false;
8985
+ const continueQueue = opts?.continueQueue === true;
8678
8986
  const thread = this.requireThread(threadRef);
8679
8987
  const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
8680
8988
  if (inFlight) {
8681
8989
  this.stoppedTurns.add(thread.id);
8682
8990
  }
8683
8991
  if (clearQueue && thread.queue.length > 0) {
8992
+ this.haltDrain.delete(thread.id);
8684
8993
  updateThread(thread.id, { queue: [] });
8685
8994
  this.emit({ type: "queue_changed", threadId: thread.id, queue: [] });
8995
+ } else if (!clearQueue && !continueQueue) {
8996
+ this.haltDrain.add(thread.id);
8997
+ } else if (continueQueue) {
8998
+ this.haltDrain.delete(thread.id);
8686
8999
  }
8687
9000
  const handle = this.activeTurns.get(thread.id);
8688
9001
  if (handle) handle.kill();
@@ -9483,11 +9796,15 @@ async function startMcpServer() {
9483
9796
  );
9484
9797
  server.tool(
9485
9798
  "present_artifact",
9486
- "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.",
9799
+ "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.",
9487
9800
  {
9488
9801
  title: import_zod.z.string().describe("Short title shown in the artifact pane header"),
9489
- type: import_zod.z.enum(["html", "svg", "markdown"]).describe("Artifact kind \u2014 html opens an iframe preview"),
9490
- content: import_zod.z.string().describe("Full document body (complete HTML page, SVG markup, or markdown)"),
9802
+ type: import_zod.z.enum(["html", "svg", "markdown", "react"]).describe(
9803
+ "Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
9804
+ ),
9805
+ content: import_zod.z.string().describe(
9806
+ "Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
9807
+ ),
9491
9808
  artifact_id: import_zod.z.string().optional().describe("Stable id when updating the same artifact across turns")
9492
9809
  },
9493
9810
  async ({ title, type, content, artifact_id }) => {
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startMcpServer
4
- } from "../chunk-OY47OEY2.js";
4
+ } from "../chunk-AL2GL5HJ.js";
5
5
  import "../chunk-PER4N6LS.js";
6
6
  import "../chunk-Y2EWQ4TL.js";
7
7
  import "../chunk-BMB7WCGF.js";
8
- import "../chunk-4YLTMPEO.js";
8
+ import "../chunk-MULWZLDI.js";
9
9
  import "../chunk-ILQK4P5R.js";
10
- import "../chunk-3DKGI32Q.js";
10
+ import "../chunk-BSZX63TV.js";
11
11
  import "../chunk-3WF3X46L.js";
12
12
  import "../chunk-UFWEANLU.js";
13
13
  import "../chunk-HYRHI3QU.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sideboard-ai/core",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Sideboard core — orchestration, agents, git worktrees, MCP server",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",