@sideboard-ai/core 0.1.32 → 0.1.33

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.
@@ -27,6 +27,50 @@ function appDataDir() {
27
27
  return base;
28
28
  }
29
29
 
30
+ // src/agents/error-detail.ts
31
+ function formatUnknownDetail(err) {
32
+ if (err == null) return "";
33
+ if (typeof err === "string") return err.trim();
34
+ if (err instanceof Error) {
35
+ const base = err.message.trim() || err.name;
36
+ const code = "code" in err && typeof err.code === "string" ? err.code.trim() : "";
37
+ return code && !base.includes(code) ? `${base} (${code})` : base;
38
+ }
39
+ if (typeof err === "object") {
40
+ const o = err;
41
+ const nested = o.error != null && typeof o.error === "object" ? formatUnknownDetail(o.error) : "";
42
+ const message = typeof o.message === "string" ? o.message.trim() : typeof o.error === "string" ? o.error.trim() : typeof o.result === "string" ? o.result.trim() : nested;
43
+ const code = typeof o.code === "string" ? o.code.trim() : "";
44
+ if (message) return code && !message.includes(code) ? `${message} (${code})` : message;
45
+ try {
46
+ const json = JSON.stringify(err);
47
+ if (json && json !== "{}" && json !== "null") return json;
48
+ } catch {
49
+ }
50
+ }
51
+ const fallback = String(err);
52
+ return fallback === "[object Object]" ? "" : fallback;
53
+ }
54
+ function extractJsonErrorMessage(obj) {
55
+ const nested = obj.error != null && typeof obj.error === "object" ? obj.error : null;
56
+ const candidates = [
57
+ typeof obj.message === "string" ? obj.message : null,
58
+ typeof obj.error === "string" ? obj.error : null,
59
+ nested && typeof nested.message === "string" ? nested.message : null,
60
+ typeof obj.result === "string" ? obj.result : null,
61
+ typeof obj.detail === "string" ? obj.detail : null
62
+ ];
63
+ for (const c of candidates) {
64
+ const t = c?.trim();
65
+ if (t) return t;
66
+ }
67
+ if (Array.isArray(obj.errors)) {
68
+ const parts = obj.errors.map((e) => formatUnknownDetail(e)).map((s) => s.trim()).filter(Boolean);
69
+ if (parts.length) return parts.join("; ");
70
+ }
71
+ return null;
72
+ }
73
+
30
74
  // src/agents/cursor-events.ts
31
75
  function usageFromCursor(usage) {
32
76
  if (!usage) return null;
@@ -92,7 +136,12 @@ function cursorSdkMessageToEvents(msg) {
92
136
  if (usage) return [{ type: "usage", data: usage }];
93
137
  }
94
138
  if (msg.type === "status" && msg.status === "ERROR") {
95
- const detail = msg.message || msg.text || "Cursor run entered ERROR status";
139
+ const rawMessage = msg.message;
140
+ const detail = (typeof rawMessage === "string" ? rawMessage.trim() : "") || extractJsonErrorMessage(msg) || formatUnknownDetail(msg.error) || msg.text || "Cursor run entered ERROR status";
141
+ return [{ type: "stderr", data: detail }];
142
+ }
143
+ if (msg.type === "error") {
144
+ const detail = extractJsonErrorMessage(msg) || formatUnknownDetail(msg.error) || msg.text || "Cursor run error";
96
145
  return [{ type: "stderr", data: detail }];
97
146
  }
98
147
  return [];
@@ -171,12 +220,14 @@ async function main() {
171
220
  }
172
221
  const result = await run.wait();
173
222
  if (result.status === "error") {
223
+ const detail = formatUnknownDetail(result.error);
174
224
  emit({
175
225
  type: "stderr",
176
- data: `Cursor run failed (${result.id})${result.error ? `: ${String(result.error)}` : ""}`
226
+ data: detail ? `Cursor run failed (${result.id}): ${detail}` : `Cursor run failed (${result.id})`
177
227
  });
178
228
  return 2;
179
229
  }
230
+ if (result.status === "cancelled") return 0;
180
231
  return 0;
181
232
  } finally {
182
233
  await agent[Symbol.asyncDispose]();
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- cursorSdkMessageToEvents
4
- } from "../chunk-3DKGI32Q.js";
3
+ cursorSdkMessageToEvents,
4
+ formatUnknownDetail
5
+ } from "../chunk-BSZX63TV.js";
5
6
  import {
6
7
  appDataDir
7
8
  } from "../chunk-M37RITA6.js";
@@ -83,12 +84,14 @@ async function main() {
83
84
  }
84
85
  const result = await run.wait();
85
86
  if (result.status === "error") {
87
+ const detail = formatUnknownDetail(result.error);
86
88
  emit({
87
89
  type: "stderr",
88
- data: `Cursor run failed (${result.id})${result.error ? `: ${String(result.error)}` : ""}`
90
+ data: detail ? `Cursor run failed (${result.id}): ${detail}` : `Cursor run failed (${result.id})`
89
91
  });
90
92
  return 2;
91
93
  }
94
+ if (result.status === "cancelled") return 0;
92
95
  return 0;
93
96
  } finally {
94
97
  await agent[Symbol.asyncDispose]();
@@ -21,12 +21,12 @@ import {
21
21
  opencodeAdapter,
22
22
  permissionMode,
23
23
  resolveCursorModelId
24
- } from "./chunk-4YLTMPEO.js";
24
+ } from "./chunk-MULWZLDI.js";
25
25
  import "./chunk-ILQK4P5R.js";
26
26
  import {
27
27
  cursorSdkMessageToEvents,
28
28
  parseCursorRunnerLine
29
- } from "./chunk-3DKGI32Q.js";
29
+ } from "./chunk-BSZX63TV.js";
30
30
  import "./chunk-3WF3X46L.js";
31
31
  import "./chunk-M37RITA6.js";
32
32
  import {
@@ -25,7 +25,14 @@ import {
25
25
  allAdapters,
26
26
  getAdapter,
27
27
  parseBrightsyCliLine
28
- } from "./chunk-4YLTMPEO.js";
28
+ } from "./chunk-MULWZLDI.js";
29
+ import {
30
+ fallbackTurnFailDetail,
31
+ formatTurnExitError,
32
+ looksLikeAgentFailureMessage,
33
+ pushTurnStderr,
34
+ summarizeTurnStderr
35
+ } from "./chunk-BSZX63TV.js";
29
36
  import {
30
37
  childEnvWithAppSettings,
31
38
  getLinearApiKey,
@@ -846,6 +853,49 @@ function pipeLines(stream, onLine) {
846
853
  const rl = createInterface2({ input: stream });
847
854
  rl.on("line", onLine);
848
855
  }
856
+ function killScriptTree(child, ports = []) {
857
+ const pid = child.pid;
858
+ if (pid) {
859
+ try {
860
+ if (process.platform === "win32") {
861
+ void execa2("taskkill", ["/pid", String(pid), "/T", "/F"], { reject: false });
862
+ } else {
863
+ process.kill(-pid, "SIGTERM");
864
+ setTimeout(() => {
865
+ try {
866
+ process.kill(-pid, "SIGKILL");
867
+ } catch {
868
+ }
869
+ }, 2500).unref?.();
870
+ }
871
+ } catch {
872
+ try {
873
+ child.kill("SIGTERM");
874
+ } catch {
875
+ }
876
+ }
877
+ }
878
+ for (const port of ports) {
879
+ if (!Number.isFinite(port) || port <= 0) continue;
880
+ if (process.platform === "win32") {
881
+ void execa2(
882
+ "powershell",
883
+ [
884
+ "-NoProfile",
885
+ "-Command",
886
+ `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
887
+ ],
888
+ { reject: false }
889
+ );
890
+ } else {
891
+ void execa2(
892
+ "zsh",
893
+ ["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
894
+ { reject: false }
895
+ );
896
+ }
897
+ }
898
+ }
849
899
  async function spawnWorkspaceScript(command, opts) {
850
900
  const loginEnv = await captureLoginEnv();
851
901
  const env = buildWorkspaceScriptEnv(
@@ -862,18 +912,17 @@ async function spawnWorkspaceScript(command, opts) {
862
912
  const child = execa2(shell, ["-lc", command], {
863
913
  cwd: opts.worktreePath,
864
914
  reject: false,
865
- env
915
+ env,
916
+ // Own process group so Stop can tear down the whole tree (not just the shell).
917
+ // There is no settings.toml `stop=` / teardown hook for run scripts.
918
+ ...process.platform === "win32" ? {} : { detached: true }
866
919
  });
867
920
  pipeLines(child.stdout, opts.onLine);
868
921
  pipeLines(child.stderr, opts.onLine);
922
+ const ports = opts.ports ?? [];
869
923
  return {
870
924
  pid: child.pid,
871
- kill: () => {
872
- try {
873
- child.kill("SIGTERM");
874
- } catch {
875
- }
876
- },
925
+ kill: () => killScriptTree(child, ports),
877
926
  done: child.then((r) => r.exitCode ?? null),
878
927
  child
879
928
  };
@@ -2557,7 +2606,7 @@ async function createThread(input, onSetupLine) {
2557
2606
  return readThread(thread.id) ?? thread;
2558
2607
  }
2559
2608
  async function listLinearIssues(agent, repoPath) {
2560
- const { getAdapter: getAdapter2 } = await import("./agents-HWIPJLC3.js");
2609
+ const { getAdapter: getAdapter2 } = await import("./agents-2OX5XXYP.js");
2561
2610
  await requireAgent(agent, { requireLinear: true });
2562
2611
  const adapter = getAdapter2(agent);
2563
2612
  if (!adapter.listLinearIssues) {
@@ -3252,6 +3301,11 @@ var Orchestrator = class {
3252
3301
  * the killed turn's handle.done resolves.
3253
3302
  */
3254
3303
  stoppedTurns = /* @__PURE__ */ new Set();
3304
+ /**
3305
+ * Pause drainQueue after the in-flight turn unwinds (Stop with a preserved
3306
+ * queue). Cleared when the user sends or promotes a queued message again.
3307
+ */
3308
+ haltDrain = /* @__PURE__ */ new Set();
3255
3309
  /** WIP snapshot SHA at the start of the latest agent turn (per thread). */
3256
3310
  turnBaselines = /* @__PURE__ */ new Map();
3257
3311
  maxConcurrent;
@@ -3319,7 +3373,7 @@ var Orchestrator = class {
3319
3373
  } catch {
3320
3374
  }
3321
3375
  for (const thread of listThreads()) {
3322
- if (thread.queue.length > 0) {
3376
+ if (thread.queue.length > 0 && thread.status !== "stopped") {
3323
3377
  void this.drainQueue(thread.id);
3324
3378
  }
3325
3379
  }
@@ -3380,6 +3434,7 @@ var Orchestrator = class {
3380
3434
  return withThreadLock(thread.id, async () => {
3381
3435
  const current = this.requireThread(thread.id);
3382
3436
  const queue = [...current.queue, prompt];
3437
+ this.haltDrain.delete(thread.id);
3383
3438
  updateThread(thread.id, { queue, status: "queued" });
3384
3439
  this.emit({ type: "queue_changed", threadId: thread.id, queue });
3385
3440
  this.emit({ type: "status_changed", threadId: thread.id, status: "queued" });
@@ -3394,11 +3449,75 @@ var Orchestrator = class {
3394
3449
  }
3395
3450
  return results;
3396
3451
  }
3452
+ /** Edit the text of a not-yet-started queued message. */
3453
+ async editQueuedMessage(threadRef, index, text) {
3454
+ const thread = this.requireThread(threadRef);
3455
+ return withThreadLock(thread.id, async () => {
3456
+ const current = this.requireThread(thread.id);
3457
+ const trimmed = text.trim();
3458
+ if (!trimmed || index < 0 || index >= current.queue.length) {
3459
+ return current;
3460
+ }
3461
+ const queue = current.queue.map((p, i) => i === index ? trimmed : p);
3462
+ updateThread(thread.id, { queue });
3463
+ this.emit({ type: "queue_changed", threadId: thread.id, queue });
3464
+ return this.requireThread(thread.id);
3465
+ });
3466
+ }
3467
+ /** Remove a not-yet-started queued message. */
3468
+ async removeQueuedMessage(threadRef, index) {
3469
+ const thread = this.requireThread(threadRef);
3470
+ return withThreadLock(thread.id, async () => {
3471
+ const current = this.requireThread(thread.id);
3472
+ if (index < 0 || index >= current.queue.length) return current;
3473
+ const queue = current.queue.filter((_, i) => i !== index);
3474
+ const stillQueued = queue.length > 0;
3475
+ const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
3476
+ updateThread(thread.id, {
3477
+ queue,
3478
+ status: !stillQueued && !inFlight && current.status === "queued" ? "idle" : current.status
3479
+ });
3480
+ this.emit({ type: "queue_changed", threadId: thread.id, queue });
3481
+ const next = this.requireThread(thread.id);
3482
+ this.emit({ type: "status_changed", threadId: thread.id, status: next.status });
3483
+ return next;
3484
+ });
3485
+ }
3486
+ /**
3487
+ * Promote a queued message to run next, interrupting the in-flight turn (if any).
3488
+ * The current turn is stopped without clearing the rest of the queue — drainQueue
3489
+ * picks the promoted message up as soon as the interrupted turn unwinds.
3490
+ */
3491
+ async sendQueuedMessageNow(threadRef, index) {
3492
+ const thread = this.requireThread(threadRef);
3493
+ const promoted = await withThreadLock(thread.id, async () => {
3494
+ const current = this.requireThread(thread.id);
3495
+ if (index < 0 || index >= current.queue.length) return false;
3496
+ const item = current.queue[index];
3497
+ const rest = current.queue.filter((_, i) => i !== index);
3498
+ const queue = [item, ...rest];
3499
+ this.haltDrain.delete(thread.id);
3500
+ updateThread(thread.id, { queue });
3501
+ this.emit({ type: "queue_changed", threadId: thread.id, queue });
3502
+ return true;
3503
+ });
3504
+ if (!promoted) return this.requireThread(thread.id);
3505
+ const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
3506
+ if (inFlight) {
3507
+ this.stop(thread.id, { clearQueue: false, continueQueue: true });
3508
+ } else {
3509
+ void this.drainQueue(thread.id);
3510
+ }
3511
+ return this.requireThread(thread.id);
3512
+ }
3397
3513
  async drainQueue(threadId) {
3398
3514
  if (this.draining.has(threadId)) return;
3399
3515
  this.draining.add(threadId);
3400
3516
  try {
3401
3517
  while (true) {
3518
+ if (this.haltDrain.has(threadId)) {
3519
+ break;
3520
+ }
3402
3521
  const thread = readThread(threadId);
3403
3522
  if (!thread || thread.queue.length === 0) {
3404
3523
  if (thread && thread.status === "queued") {
@@ -3411,7 +3530,7 @@ var Orchestrator = class {
3411
3530
  await new Promise((r) => setTimeout(r, 250));
3412
3531
  continue;
3413
3532
  }
3414
- if (this.activeTurns.has(threadId)) {
3533
+ if (this.activeTurns.has(threadId) || this.startingTurns.has(threadId)) {
3415
3534
  await new Promise((r) => setTimeout(r, 100));
3416
3535
  continue;
3417
3536
  }
@@ -3546,7 +3665,7 @@ var Orchestrator = class {
3546
3665
  ...fresh.agent === "claude" && fresh.sessionId ? [] : [instructions, seed]
3547
3666
  ].filter(Boolean).join("\n\n---\n\n");
3548
3667
  try {
3549
- let lastStderr = "";
3668
+ const stderrTail = [];
3550
3669
  const handle = await spawnAgentTurn(
3551
3670
  fresh,
3552
3671
  { cachedPrefix, prompt: agentPrompt },
@@ -3555,8 +3674,8 @@ var Orchestrator = class {
3555
3674
  if (event.type === "session_id") {
3556
3675
  updateThread(threadId, { sessionId: event.data });
3557
3676
  }
3558
- if (event.type === "stderr" && typeof event.data === "string" && event.data.trim()) {
3559
- lastStderr = event.data.trim();
3677
+ if (event.type === "stderr" && typeof event.data === "string") {
3678
+ pushTurnStderr(stderrTail, event.data);
3560
3679
  }
3561
3680
  }
3562
3681
  );
@@ -3581,10 +3700,12 @@ var Orchestrator = class {
3581
3700
  if (result.sessionId) {
3582
3701
  updateThread(threadId, { sessionId: result.sessionId });
3583
3702
  }
3584
- if (result.assistantText.trim() || result.parts.length > 0) {
3703
+ const assistantText = result.assistantText.trim();
3704
+ const failureOnlyMessage = result.exitCode !== 0 && looksLikeAgentFailureMessage(assistantText) && !result.parts.some((p) => p.type === "tool" || p.type === "thinking");
3705
+ if (!failureOnlyMessage && (assistantText || result.parts.length > 0)) {
3585
3706
  appendMessage(threadId, {
3586
3707
  role: "agent",
3587
- text: result.assistantText.trim(),
3708
+ text: assistantText,
3588
3709
  parts: result.parts.length > 0 ? result.parts : void 0,
3589
3710
  durationMs: Math.max(0, Date.now() - turnStartedAt),
3590
3711
  usage: result.usage ?? void 0,
@@ -3603,7 +3724,9 @@ var Orchestrator = class {
3603
3724
  this.emit({ type: "status_changed", threadId, status: "stopped" });
3604
3725
  this.emit({ type: "turn_finished", threadId, exitCode: result.exitCode });
3605
3726
  } else {
3606
- const failDetail = lastStderr ? `exit ${result.exitCode}: ${lastStderr.slice(0, 500)}` : `exit ${result.exitCode}`;
3727
+ const lastStderr = summarizeTurnStderr(stderrTail);
3728
+ const detail = lastStderr || (result.exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
3729
+ const failDetail = formatTurnExitError(result.exitCode, detail);
3607
3730
  setStatus(
3608
3731
  threadId,
3609
3732
  result.exitCode === 0 ? "idle" : "error",
@@ -3639,20 +3762,30 @@ var Orchestrator = class {
3639
3762
  }
3640
3763
  /**
3641
3764
  * Stop an in-flight agent turn.
3642
- * Default `clearQueue: true` (force-stop): kills the turn AND empties queued prompts
3643
- * so drainQueue cannot continue / re-start work after an intentional stop. Desktop,
3644
- * CLI, MCP, and cloud-connect all share this default.
3765
+ *
3766
+ * - Default `clearQueue: true` (force-stop): empties queued prompts so nothing
3767
+ * resumes. Used by MCP force-stop, archive, and cloud-connect.
3768
+ * - Desktop Stop uses `{ clearQueue: false }` so follow-ups stay editable.
3769
+ * - `continueQueue: true` (Send now): keep the queue and let drainQueue resume
3770
+ * after the interrupted turn unwinds. Without it, drain pauses until send /
3771
+ * promote.
3645
3772
  */
3646
3773
  stop(threadRef, opts) {
3647
3774
  const clearQueue = opts?.clearQueue !== false;
3775
+ const continueQueue = opts?.continueQueue === true;
3648
3776
  const thread = this.requireThread(threadRef);
3649
3777
  const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
3650
3778
  if (inFlight) {
3651
3779
  this.stoppedTurns.add(thread.id);
3652
3780
  }
3653
3781
  if (clearQueue && thread.queue.length > 0) {
3782
+ this.haltDrain.delete(thread.id);
3654
3783
  updateThread(thread.id, { queue: [] });
3655
3784
  this.emit({ type: "queue_changed", threadId: thread.id, queue: [] });
3785
+ } else if (!clearQueue && !continueQueue) {
3786
+ this.haltDrain.add(thread.id);
3787
+ } else if (continueQueue) {
3788
+ this.haltDrain.delete(thread.id);
3656
3789
  }
3657
3790
  const handle = this.activeTurns.get(thread.id);
3658
3791
  if (handle) handle.kill();
@@ -4247,9 +4380,11 @@ function getOrchestrator() {
4247
4380
  }
4248
4381
  async function startOrchestration(opts) {
4249
4382
  const repoPath = opts.repoPath?.trim();
4383
+ const goal = opts.goal.trim();
4384
+ const orch = getOrchestrator();
4250
4385
  if (!repoPath || isGlobalRepoPath(repoPath)) {
4251
- return createGlobalChat({
4252
- sourceRef: opts.goal,
4386
+ const thread2 = createGlobalChat({
4387
+ sourceRef: goal,
4253
4388
  agent: opts.agent,
4254
4389
  autonomy: opts.autonomy,
4255
4390
  model: opts.model,
@@ -4257,9 +4392,13 @@ async function startOrchestration(opts) {
4257
4392
  planMode: opts.planMode,
4258
4393
  attachments: opts.attachments
4259
4394
  });
4395
+ if (goal) {
4396
+ return orch.send(thread2.id, goal);
4397
+ }
4398
+ return thread2;
4260
4399
  }
4261
4400
  const { titleFromPrompt } = await import("./title-4A2ATYNY.js");
4262
- const title = titleFromPrompt(opts.goal) || "Orchestration";
4401
+ const title = titleFromPrompt(goal) || "Orchestration";
4263
4402
  const createOpts = {
4264
4403
  agent: opts.agent,
4265
4404
  repoPath,
@@ -4286,10 +4425,14 @@ async function startOrchestration(opts) {
4286
4425
  });
4287
4426
  });
4288
4427
  const { updateThread: upd } = await import("./thread-store-UNPZNIFW.js");
4289
- return upd(thread.id, {
4428
+ const updated = upd(thread.id, {
4290
4429
  sourceType: "orchestration",
4291
- sourceRef: opts.goal
4430
+ sourceRef: goal
4292
4431
  });
4432
+ if (goal) {
4433
+ return orch.send(updated.id, goal);
4434
+ }
4435
+ return updated;
4293
4436
  }
4294
4437
 
4295
4438
  // src/mcp/server.ts
@@ -4379,11 +4522,15 @@ async function startMcpServer() {
4379
4522
  );
4380
4523
  server.tool(
4381
4524
  "present_artifact",
4382
- "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.",
4525
+ "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.",
4383
4526
  {
4384
4527
  title: z.string().describe("Short title shown in the artifact pane header"),
4385
- type: z.enum(["html", "svg", "markdown"]).describe("Artifact kind \u2014 html opens an iframe preview"),
4386
- content: z.string().describe("Full document body (complete HTML page, SVG markup, or markdown)"),
4528
+ type: z.enum(["html", "svg", "markdown", "react"]).describe(
4529
+ "Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
4530
+ ),
4531
+ content: z.string().describe(
4532
+ "Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
4533
+ ),
4387
4534
  artifact_id: z.string().optional().describe("Stable id when updating the same artifact across turns")
4388
4535
  },
4389
4536
  async ({ title, type, content, artifact_id }) => {