@sideboard-ai/core 0.1.24 → 0.1.31

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.
@@ -4177,6 +4177,35 @@ var init_claude = __esm({
4177
4177
  });
4178
4178
 
4179
4179
  // src/agents/codex.ts
4180
+ async function listCodexModels() {
4181
+ const now = Date.now();
4182
+ if (cachedCodexModels && now - cachedCodexModels.at < CODEX_MODEL_CACHE_MS) {
4183
+ return cachedCodexModels.models;
4184
+ }
4185
+ const which = await run("which", ["codex"], { reject: false });
4186
+ if (which.exitCode !== 0) return FALLBACK_CODEX_MODELS;
4187
+ const listed = await run("codex", ["debug", "models"], { reject: false });
4188
+ if (listed.exitCode !== 0 || !listed.stdout.trim()) {
4189
+ return cachedCodexModels?.models ?? FALLBACK_CODEX_MODELS;
4190
+ }
4191
+ try {
4192
+ const parsed = JSON.parse(listed.stdout);
4193
+ const rows = Array.isArray(parsed.models) ? parsed.models : [];
4194
+ const preferred = rows.filter((m) => (m.visibility ?? "list") === "list");
4195
+ const source = preferred.length > 0 ? preferred : rows;
4196
+ const models = source.map((m) => ({
4197
+ id: (m.slug || "").trim(),
4198
+ displayName: (m.display_name || m.slug || "").trim(),
4199
+ description: m.description,
4200
+ priority: typeof m.priority === "number" ? m.priority : 999
4201
+ })).filter((m) => m.id).sort((a, b) => a.priority - b.priority).map(({ id, displayName, description }) => ({ id, displayName, description }));
4202
+ if (models.length === 0) return FALLBACK_CODEX_MODELS;
4203
+ cachedCodexModels = { at: now, models };
4204
+ return models;
4205
+ } catch {
4206
+ return cachedCodexModels?.models ?? FALLBACK_CODEX_MODELS;
4207
+ }
4208
+ }
4180
4209
  function usageFromCodex(usage) {
4181
4210
  if (!usage) return null;
4182
4211
  const inputTokens = Number(usage.input_tokens ?? 0);
@@ -4200,7 +4229,7 @@ function codexConfigHasNetworkAccess() {
4200
4229
  }
4201
4230
  return false;
4202
4231
  }
4203
- var import_node_fs11, import_node_os7, import_node_path11, CODEX_PROMPT_ARG_MAX, codexAdapter;
4232
+ var import_node_fs11, import_node_os7, import_node_path11, CODEX_PROMPT_ARG_MAX, FALLBACK_CODEX_MODELS, cachedCodexModels, CODEX_MODEL_CACHE_MS, codexAdapter;
4204
4233
  var init_codex = __esm({
4205
4234
  "src/agents/codex.ts"() {
4206
4235
  "use strict";
@@ -4211,6 +4240,15 @@ var init_codex = __esm({
4211
4240
  init_turn_input();
4212
4241
  init_types();
4213
4242
  CODEX_PROMPT_ARG_MAX = 2e5;
4243
+ FALLBACK_CODEX_MODELS = [
4244
+ { id: "gpt-5.6-sol", displayName: "GPT-5.6 Sol" },
4245
+ { id: "gpt-5.6-terra", displayName: "GPT-5.6 Terra" },
4246
+ { id: "gpt-5.6-luna", displayName: "GPT-5.6 Luna" },
4247
+ { id: "gpt-5.5", displayName: "GPT-5.5" },
4248
+ { id: "gpt-5.2", displayName: "GPT-5.2" }
4249
+ ];
4250
+ cachedCodexModels = null;
4251
+ CODEX_MODEL_CACHE_MS = 5 * 60 * 1e3;
4214
4252
  codexAdapter = {
4215
4253
  kind: "codex",
4216
4254
  async detect() {
@@ -4256,6 +4294,7 @@ var init_codex = __esm({
4256
4294
  );
4257
4295
  }
4258
4296
  const mode = permissionMode(thread);
4297
+ const model = thread.model?.trim();
4259
4298
  const args = [
4260
4299
  "exec",
4261
4300
  ...sessionId ? ["resume", sessionId] : [],
@@ -4266,7 +4305,8 @@ var init_codex = __esm({
4266
4305
  "--sandbox",
4267
4306
  mode.codexSandbox,
4268
4307
  "--ask-for-approval",
4269
- "never"
4308
+ "never",
4309
+ ...model ? ["--model", model] : []
4270
4310
  ];
4271
4311
  return {
4272
4312
  file: "codex",
@@ -4447,6 +4487,35 @@ function resolveCursorApiKey() {
4447
4487
  if (fromEnv) return fromEnv;
4448
4488
  return (loadAppSettings().environment.CURSOR_API_KEY || "").trim();
4449
4489
  }
4490
+ function isCursorAutoModel(model) {
4491
+ const id = (model ?? "").trim().toLowerCase();
4492
+ return !id || id === "default" || id === "auto";
4493
+ }
4494
+ function resolveCursorModelId(model) {
4495
+ if (isCursorAutoModel(model)) return "default";
4496
+ return model.trim();
4497
+ }
4498
+ async function listCursorModels() {
4499
+ const now = Date.now();
4500
+ if (cachedModels && now - cachedModels.at < MODEL_CACHE_MS) {
4501
+ return cachedModels.models;
4502
+ }
4503
+ const apiKey = resolveCursorApiKey();
4504
+ if (!apiKey) return FALLBACK_CURSOR_MODELS;
4505
+ try {
4506
+ const listed = await import_sdk.Cursor.models.list({ apiKey });
4507
+ const models = listed.map((m) => ({
4508
+ id: m.id,
4509
+ displayName: m.displayName || m.id,
4510
+ description: m.description
4511
+ })).filter((m) => Boolean(m.id));
4512
+ if (models.length === 0) return FALLBACK_CURSOR_MODELS;
4513
+ cachedModels = { at: now, models };
4514
+ return models;
4515
+ } catch {
4516
+ return cachedModels?.models ?? FALLBACK_CURSOR_MODELS;
4517
+ }
4518
+ }
4450
4519
  function entryDir() {
4451
4520
  const cjsDir = typeof __dirname !== "undefined" ? __dirname : "";
4452
4521
  if (cjsDir) return cjsDir;
@@ -4478,7 +4547,7 @@ function cursorRunnerPath() {
4478
4547
  }
4479
4548
  return candidates[0];
4480
4549
  }
4481
- var import_node_fs12, import_node_module2, import_node_path12, import_node_url2, import_sdk, import_meta2, cursorAdapter;
4550
+ var import_node_fs12, import_node_module2, import_node_path12, import_node_url2, import_sdk, import_meta2, FALLBACK_CURSOR_MODELS, cachedModels, MODEL_CACHE_MS, cursorAdapter;
4482
4551
  var init_cursor = __esm({
4483
4552
  "src/agents/cursor.ts"() {
4484
4553
  "use strict";
@@ -4493,6 +4562,13 @@ var init_cursor = __esm({
4493
4562
  init_turn_input();
4494
4563
  init_cursor_events();
4495
4564
  import_meta2 = {};
4565
+ FALLBACK_CURSOR_MODELS = [
4566
+ { id: "default", displayName: "Auto" },
4567
+ { id: "composer-2.5", displayName: "Composer 2.5" },
4568
+ { id: "composer-2", displayName: "Composer 2" }
4569
+ ];
4570
+ cachedModels = null;
4571
+ MODEL_CACHE_MS = 5 * 60 * 1e3;
4496
4572
  cursorAdapter = {
4497
4573
  kind: "cursor",
4498
4574
  async detect() {
@@ -4543,12 +4619,16 @@ var init_cursor = __esm({
4543
4619
  };
4544
4620
  const runner = cursorRunnerPath();
4545
4621
  const isTs = runner.endsWith(".ts");
4622
+ const whichNode = await run("which", ["node"], { reject: false });
4623
+ const nodeBin = whichNode.exitCode === 0 && whichNode.stdout.trim() ? whichNode.stdout.trim() : null;
4624
+ const file = nodeBin || process.execPath;
4546
4625
  return {
4547
- file: process.execPath,
4626
+ file,
4548
4627
  args: isTs ? ["--import", "tsx", runner] : [runner],
4549
4628
  cwd: thread.worktreePath,
4550
4629
  stdin: JSON.stringify(req),
4551
4630
  env: {
4631
+ ...nodeBin ? {} : { ELECTRON_RUN_AS_NODE: "1" },
4552
4632
  ...apiKey ? { CURSOR_API_KEY: apiKey } : {}
4553
4633
  }
4554
4634
  };
@@ -4573,6 +4653,46 @@ var init_cursor = __esm({
4573
4653
  });
4574
4654
 
4575
4655
  // src/agents/opencode.ts
4656
+ function displayNameFromOpencodeId(id) {
4657
+ const slash = id.indexOf("/");
4658
+ if (slash <= 0) return id;
4659
+ const provider = id.slice(0, slash);
4660
+ const name = id.slice(slash + 1);
4661
+ return `${provider} \xB7 ${name}`;
4662
+ }
4663
+ function sortOpencodeModelIds(ids) {
4664
+ return [...ids].sort((a, b) => {
4665
+ const aLatest = /latest|~/.test(a) ? 0 : 1;
4666
+ const bLatest = /latest|~/.test(b) ? 0 : 1;
4667
+ if (aLatest !== bLatest) return aLatest - bLatest;
4668
+ const aOc = a.startsWith("opencode/") ? 0 : 1;
4669
+ const bOc = b.startsWith("opencode/") ? 0 : 1;
4670
+ if (aOc !== bOc) return aOc - bOc;
4671
+ return a.localeCompare(b);
4672
+ });
4673
+ }
4674
+ async function listOpencodeModels() {
4675
+ const now = Date.now();
4676
+ if (cachedOpencodeModels && now - cachedOpencodeModels.at < OPENCODE_MODEL_CACHE_MS) {
4677
+ return cachedOpencodeModels.models;
4678
+ }
4679
+ const which = await run("which", ["opencode"], { reject: false });
4680
+ if (which.exitCode !== 0) return FALLBACK_OPENCODE_MODELS;
4681
+ const listed = await run("opencode", ["models"], { reject: false });
4682
+ if (listed.exitCode !== 0 || !listed.stdout.trim()) {
4683
+ return cachedOpencodeModels?.models ?? FALLBACK_OPENCODE_MODELS;
4684
+ }
4685
+ const ids = listed.stdout.split("\n").map((l) => l.trim()).filter((l) => /^[\w.~@+-]+\/[\w.~@+/-]+$/.test(l));
4686
+ const unique = [...new Set(ids)];
4687
+ if (unique.length === 0) return FALLBACK_OPENCODE_MODELS;
4688
+ const OPENCODE_PICKER_LIMIT = 60;
4689
+ const models = sortOpencodeModelIds(unique).slice(0, OPENCODE_PICKER_LIMIT).map((id) => ({
4690
+ id,
4691
+ displayName: displayNameFromOpencodeId(id)
4692
+ }));
4693
+ cachedOpencodeModels = { at: now, models };
4694
+ return models;
4695
+ }
4576
4696
  function usageFromOpencode(tokens) {
4577
4697
  if (!tokens) return null;
4578
4698
  const inputTokens = Number(tokens.input ?? 0);
@@ -4585,13 +4705,26 @@ function usageFromOpencode(tokens) {
4585
4705
  cacheWriteTokens: tokens.cache?.write ? Number(tokens.cache.write) : void 0
4586
4706
  };
4587
4707
  }
4588
- var opencodeAdapter;
4708
+ var FALLBACK_OPENCODE_MODELS, cachedOpencodeModels, OPENCODE_MODEL_CACHE_MS, opencodeAdapter;
4589
4709
  var init_opencode = __esm({
4590
4710
  "src/agents/opencode.ts"() {
4591
4711
  "use strict";
4592
4712
  init_run();
4593
4713
  init_turn_input();
4594
4714
  init_types();
4715
+ FALLBACK_OPENCODE_MODELS = [
4716
+ { id: "opencode/big-pickle", displayName: "opencode \xB7 big-pickle" },
4717
+ {
4718
+ id: "openrouter/~anthropic/claude-sonnet-latest",
4719
+ displayName: "openrouter \xB7 claude-sonnet-latest"
4720
+ },
4721
+ {
4722
+ id: "openrouter/~openai/gpt-latest",
4723
+ displayName: "openrouter \xB7 gpt-latest"
4724
+ }
4725
+ ];
4726
+ cachedOpencodeModels = null;
4727
+ OPENCODE_MODEL_CACHE_MS = 5 * 60 * 1e3;
4595
4728
  opencodeAdapter = {
4596
4729
  kind: "opencode",
4597
4730
  async detect() {
@@ -4623,6 +4756,7 @@ var init_opencode = __esm({
4623
4756
  const prompt = flattenTurnInput(input);
4624
4757
  const sessionId = await this.resolveSessionId(thread.worktreePath, thread.sessionId);
4625
4758
  const mode = permissionMode(thread);
4759
+ const model = thread.model?.trim();
4626
4760
  const args = [
4627
4761
  "run",
4628
4762
  "--dir",
@@ -4633,6 +4767,9 @@ var init_opencode = __esm({
4633
4767
  if (sessionId) {
4634
4768
  args.push("--session", sessionId);
4635
4769
  }
4770
+ if (model) {
4771
+ args.push("--model", model);
4772
+ }
4636
4773
  return {
4637
4774
  file: "opencode",
4638
4775
  args,
@@ -4760,6 +4897,183 @@ var init_opencode = __esm({
4760
4897
  }
4761
4898
  });
4762
4899
 
4900
+ // src/agents/install.ts
4901
+ function getAgentSetupInfo(agent) {
4902
+ return SETUP[agent];
4903
+ }
4904
+ function listAgentSetupInfo() {
4905
+ return Object.values(SETUP);
4906
+ }
4907
+ async function openInSystemTerminal(command) {
4908
+ ensureAgentPath();
4909
+ const trimmed = command.trim();
4910
+ if (!trimmed) throw new Error("Command is empty");
4911
+ if (process.platform === "darwin") {
4912
+ const escaped = trimmed.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
4913
+ const script = `tell application "Terminal" to do script "${escaped}"`;
4914
+ const result = await run("osascript", ["-e", script], { reject: false });
4915
+ if (result.exitCode !== 0) {
4916
+ throw new Error(result.stderr.trim() || result.stdout.trim() || "Failed to open Terminal");
4917
+ }
4918
+ await run("osascript", ["-e", 'tell application "Terminal" to activate'], {
4919
+ reject: false
4920
+ });
4921
+ return;
4922
+ }
4923
+ if (process.platform === "win32") {
4924
+ const result = await run(
4925
+ "cmd.exe",
4926
+ ["/c", "start", "cmd.exe", "/k", trimmed],
4927
+ { reject: false }
4928
+ );
4929
+ if (result.exitCode !== 0) {
4930
+ throw new Error(result.stderr.trim() || result.stdout.trim() || "Failed to open cmd");
4931
+ }
4932
+ return;
4933
+ }
4934
+ const candidates = [
4935
+ { file: "gnome-terminal", args: ["--", "bash", "-lc", `${trimmed}; exec bash`] },
4936
+ { file: "x-terminal-emulator", args: ["-e", "bash", "-lc", `${trimmed}; exec bash`] },
4937
+ { file: "konsole", args: ["-e", "bash", "-lc", `${trimmed}; exec bash`] },
4938
+ { file: "xterm", args: ["-e", "bash", "-lc", `${trimmed}; exec bash`] }
4939
+ ];
4940
+ for (const c of candidates) {
4941
+ const which = await run("which", [c.file], { reject: false });
4942
+ if (which.exitCode !== 0 || !which.stdout.trim()) continue;
4943
+ const result = await run(c.file, c.args, { reject: false });
4944
+ if (result.exitCode === 0) return;
4945
+ }
4946
+ throw new Error(
4947
+ `No terminal found to run: ${trimmed}. Install gnome-terminal (or similar), or run the command manually.`
4948
+ );
4949
+ }
4950
+ async function installAgent(agent) {
4951
+ const info = getAgentSetupInfo(agent);
4952
+ if (info.kind === "bundled-sdk" || info.kind === "api-key") {
4953
+ return {
4954
+ ok: true,
4955
+ message: info.summary
4956
+ };
4957
+ }
4958
+ if (!info.installCommand) {
4959
+ return { ok: false, message: `No install command for ${agent}` };
4960
+ }
4961
+ if (/curl\s| \|\s*bash/.test(info.installCommand) || !info.npmPackage) {
4962
+ await openInSystemTerminal(info.installCommand);
4963
+ return {
4964
+ ok: true,
4965
+ openedTerminal: true,
4966
+ command: info.installCommand,
4967
+ message: `Opened Terminal to run: ${info.installCommand}`
4968
+ };
4969
+ }
4970
+ ensureAgentPath();
4971
+ const result = await run("npm", ["install", "-g", info.npmPackage], { reject: false });
4972
+ const ok = result.exitCode === 0;
4973
+ if (!ok) {
4974
+ try {
4975
+ await openInSystemTerminal(info.installCommand);
4976
+ return {
4977
+ ok: false,
4978
+ openedTerminal: true,
4979
+ command: info.installCommand,
4980
+ exitCode: result.exitCode,
4981
+ stdout: result.stdout,
4982
+ stderr: result.stderr,
4983
+ message: `npm install failed (exit ${result.exitCode}). Opened Terminal with: ${info.installCommand}`
4984
+ };
4985
+ } catch {
4986
+ return {
4987
+ ok: false,
4988
+ command: info.installCommand,
4989
+ exitCode: result.exitCode,
4990
+ stdout: result.stdout,
4991
+ stderr: result.stderr,
4992
+ message: result.stderr.trim() || result.stdout.trim() || `npm install failed (exit ${result.exitCode})`
4993
+ };
4994
+ }
4995
+ }
4996
+ return {
4997
+ ok: true,
4998
+ command: info.installCommand,
4999
+ exitCode: 0,
5000
+ stdout: result.stdout,
5001
+ stderr: result.stderr,
5002
+ message: `Installed ${info.npmPackage}`
5003
+ };
5004
+ }
5005
+ async function loginAgent(agent) {
5006
+ const info = getAgentSetupInfo(agent);
5007
+ if (!info.loginCommand) {
5008
+ return {
5009
+ ok: true,
5010
+ message: info.kind === "bundled-sdk" || info.kind === "api-key" ? info.summary : `No login command for ${agent}`
5011
+ };
5012
+ }
5013
+ await openInSystemTerminal(info.loginCommand);
5014
+ return {
5015
+ ok: true,
5016
+ openedTerminal: true,
5017
+ command: info.loginCommand,
5018
+ message: `Opened Terminal to run: ${info.loginCommand}`
5019
+ };
5020
+ }
5021
+ var SETUP;
5022
+ var init_install = __esm({
5023
+ "src/agents/install.ts"() {
5024
+ "use strict";
5025
+ init_run();
5026
+ init_path();
5027
+ SETUP = {
5028
+ claude: {
5029
+ agent: "claude",
5030
+ kind: "cli",
5031
+ summary: "Install the Claude Code CLI, then complete login in a terminal.",
5032
+ docsUrl: "https://code.claude.com/docs/en/install",
5033
+ installCommand: "npm install -g @anthropic-ai/claude-code",
5034
+ loginCommand: "claude auth login",
5035
+ npmPackage: "@anthropic-ai/claude-code"
5036
+ },
5037
+ codex: {
5038
+ agent: "codex",
5039
+ kind: "cli",
5040
+ summary: "Install the Codex CLI, then run login in a terminal.",
5041
+ docsUrl: "https://github.com/openai/codex",
5042
+ installCommand: "npm install -g @openai/codex",
5043
+ loginCommand: "codex login",
5044
+ npmPackage: "@openai/codex"
5045
+ },
5046
+ opencode: {
5047
+ agent: "opencode",
5048
+ kind: "cli",
5049
+ summary: "Install OpenCode (curl installer or npm), then authenticate providers.",
5050
+ docsUrl: "https://opencode.ai/docs",
5051
+ // Prefer the official installer; npm package name is opencode-ai.
5052
+ installCommand: "curl -fsSL https://opencode.ai/install | bash",
5053
+ loginCommand: "opencode auth login",
5054
+ npmPackage: "opencode-ai@latest"
5055
+ },
5056
+ cursor: {
5057
+ agent: "cursor",
5058
+ kind: "bundled-sdk",
5059
+ summary: "No CLI install \u2014 Sideboard ships the Cursor SDK. Add a CURSOR_API_KEY from the Cursor dashboard.",
5060
+ docsUrl: "https://cursor.com/dashboard/integrations",
5061
+ installCommand: null,
5062
+ loginCommand: null
5063
+ },
5064
+ brightsy: {
5065
+ agent: "brightsy",
5066
+ kind: "cli",
5067
+ summary: "Install the Brightsy CLI, then run `brightsy login`.",
5068
+ docsUrl: "https://www.npmjs.com/package/@brightsy/cli",
5069
+ installCommand: "npm install -g @brightsy/cli",
5070
+ loginCommand: "brightsy login",
5071
+ npmPackage: "@brightsy/cli"
5072
+ }
5073
+ };
5074
+ }
5075
+ });
5076
+
4763
5077
  // src/agents/index.ts
4764
5078
  var agents_exports = {};
4765
5079
  __export(agents_exports, {
@@ -4774,10 +5088,20 @@ __export(agents_exports, {
4774
5088
  encodeBrightsyTarget: () => encodeBrightsyTarget,
4775
5089
  ensureAgentPath: () => ensureAgentPath,
4776
5090
  getAdapter: () => getAdapter,
5091
+ getAgentSetupInfo: () => getAgentSetupInfo,
5092
+ installAgent: () => installAgent,
5093
+ isCursorAutoModel: () => isCursorAutoModel,
5094
+ listAgentSetupInfo: () => listAgentSetupInfo,
4777
5095
  listBrightsyChatTargets: () => listBrightsyChatTargets,
5096
+ listCodexModels: () => listCodexModels,
5097
+ listCursorModels: () => listCursorModels,
5098
+ listOpencodeModels: () => listOpencodeModels,
5099
+ loginAgent: () => loginAgent,
5100
+ openInSystemTerminal: () => openInSystemTerminal,
4778
5101
  opencodeAdapter: () => opencodeAdapter,
4779
5102
  parseCursorRunnerLine: () => parseCursorRunnerLine,
4780
- permissionMode: () => permissionMode
5103
+ permissionMode: () => permissionMode,
5104
+ resolveCursorModelId: () => resolveCursorModelId
4781
5105
  });
4782
5106
  function getAdapter(kind) {
4783
5107
  return adapters[kind];
@@ -4801,8 +5125,10 @@ var init_agents = __esm({
4801
5125
  init_codex();
4802
5126
  init_cursor();
4803
5127
  init_cursor_events();
5128
+ init_cursor();
4804
5129
  init_opencode();
4805
5130
  init_path();
5131
+ init_install();
4806
5132
  adapters = {
4807
5133
  claude: claudeAdapter,
4808
5134
  codex: codexAdapter,
@@ -4936,11 +5262,11 @@ var init_workspaces = __esm({
4936
5262
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
4937
5263
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
4938
5264
  var import_zod = require("zod");
4939
- var import_node_path22 = require("path");
5265
+ var import_node_path23 = require("path");
4940
5266
 
4941
5267
  // src/orchestrator/orchestrator.ts
4942
5268
  var import_node_events = require("events");
4943
- var import_node_fs23 = require("fs");
5269
+ var import_node_fs24 = require("fs");
4944
5270
 
4945
5271
  // src/agents/spawn.ts
4946
5272
  var import_node_readline = require("readline");
@@ -6310,10 +6636,10 @@ function createChatTab(input) {
6310
6636
  userSetTitle: true,
6311
6637
  ...binding,
6312
6638
  agent: input.agent ?? from.agent,
6313
- model: input.agent && input.agent !== from.agent ? null : from.model,
6639
+ model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
6314
6640
  fast: from.fast,
6315
6641
  planMode: from.planMode,
6316
- autonomy: from.autonomy,
6642
+ autonomy: input.autonomy ?? from.autonomy,
6317
6643
  attachments: input.attachments ?? [],
6318
6644
  status: "idle"
6319
6645
  });
@@ -6350,12 +6676,16 @@ async function forkThreadWorktree(input, onSetupLine) {
6350
6676
  repoPath: from.repoPath,
6351
6677
  agent: input.agent ?? from.agent,
6352
6678
  autonomy: from.autonomy,
6679
+ model: from.model,
6680
+ fast: from.fast,
6681
+ planMode: from.planMode,
6353
6682
  title: input.title?.trim() || void 0,
6354
- parentThreadId: from.id
6683
+ parentThreadId: from.id,
6684
+ attachments: [attachment]
6355
6685
  },
6356
6686
  onSetupLine
6357
6687
  );
6358
- return updateThread(thread.id, { attachments: [attachment] });
6688
+ return thread;
6359
6689
  }
6360
6690
 
6361
6691
  // src/threads/adopt.ts
@@ -7513,6 +7843,9 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
7513
7843
  parts.push("");
7514
7844
  parts.push(`## Attachment: ${att.name}`);
7515
7845
  parts.push(`Kind: ${att.kind}`);
7846
+ if (att.path) {
7847
+ parts.push(`Path in worktree: \`${att.path}\``);
7848
+ }
7516
7849
  parts.push("");
7517
7850
  parts.push(att.content);
7518
7851
  }
@@ -7555,9 +7888,198 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
7555
7888
  };
7556
7889
  }
7557
7890
 
7558
- // src/agents/instructions.ts
7891
+ // src/composer/stage-files.ts
7559
7892
  var import_node_fs22 = require("fs");
7560
7893
  var import_node_path21 = require("path");
7894
+ var import_node_crypto3 = require("crypto");
7895
+ var IMAGE_EXTENSIONS2 = /* @__PURE__ */ new Set([
7896
+ "png",
7897
+ "jpg",
7898
+ "jpeg",
7899
+ "gif",
7900
+ "webp",
7901
+ "svg",
7902
+ "bmp",
7903
+ "ico"
7904
+ ]);
7905
+ var IMAGE_MIME_BY_EXT = {
7906
+ png: "image/png",
7907
+ jpg: "image/jpeg",
7908
+ jpeg: "image/jpeg",
7909
+ gif: "image/gif",
7910
+ webp: "image/webp",
7911
+ svg: "image/svg+xml",
7912
+ bmp: "image/bmp",
7913
+ ico: "image/x-icon"
7914
+ };
7915
+ var ATTACHMENTS_DIR = ".sideboard/attachments";
7916
+ var ATTACHMENTS_GITIGNORE = `# Sideboard review / composer attachments (local only)
7917
+ *
7918
+ !.gitignore
7919
+ `;
7920
+ var MAX_INLINE_BYTES = 4e5;
7921
+ var MAX_PREVIEW_BYTES = 5e6;
7922
+ function fileExtension(filePath) {
7923
+ const base = (0, import_node_path21.basename)(filePath).toLowerCase();
7924
+ return base.includes(".") ? base.split(".").pop() || "" : "";
7925
+ }
7926
+ function isImageFilePath(filePath) {
7927
+ return IMAGE_EXTENSIONS2.has(fileExtension(filePath));
7928
+ }
7929
+ function imageMimeType(filePath) {
7930
+ return IMAGE_MIME_BY_EXT[fileExtension(filePath)] || "image/png";
7931
+ }
7932
+ function ensureAttachmentsDir(worktreePath) {
7933
+ const dir = (0, import_node_path21.join)(worktreePath, ATTACHMENTS_DIR);
7934
+ (0, import_node_fs22.mkdirSync)(dir, { recursive: true });
7935
+ const gi = (0, import_node_path21.join)(dir, ".gitignore");
7936
+ if (!(0, import_node_fs22.existsSync)(gi)) {
7937
+ (0, import_node_fs22.writeFileSync)(gi, ATTACHMENTS_GITIGNORE, "utf8");
7938
+ }
7939
+ return dir;
7940
+ }
7941
+ function uniqueAttachmentName(dir, originalName) {
7942
+ const safe = originalName.replace(/[/\\]/g, "_") || "file";
7943
+ if (!(0, import_node_fs22.existsSync)((0, import_node_path21.join)(dir, safe))) return safe;
7944
+ const ext = (0, import_node_path21.extname)(safe);
7945
+ const stem = ext ? safe.slice(0, -ext.length) : safe;
7946
+ for (let i = 1; i < 1e4; i++) {
7947
+ const candidate = `${stem}-${i}${ext}`;
7948
+ if (!(0, import_node_fs22.existsSync)((0, import_node_path21.join)(dir, candidate))) return candidate;
7949
+ }
7950
+ return `${stem}-${(0, import_node_crypto3.randomUUID)()}${ext}`;
7951
+ }
7952
+ function previewDataUrlFromBuf(filePath, buf) {
7953
+ if (!isImageFilePath(filePath)) return void 0;
7954
+ if (buf.length > MAX_PREVIEW_BYTES) return void 0;
7955
+ return `data:${imageMimeType(filePath)};base64,${buf.toString("base64")}`;
7956
+ }
7957
+ function attachmentFromBuffer(name, buf, opts) {
7958
+ const previewDataUrl = previewDataUrlFromBuf(name, buf);
7959
+ if (isImageFilePath(name)) {
7960
+ const pathHint = opts.path ? `\`${opts.path}\`` : opts.sourceLabel || name;
7961
+ return {
7962
+ id: (0, import_node_crypto3.randomUUID)(),
7963
+ name,
7964
+ kind: "file",
7965
+ path: opts.path,
7966
+ previewDataUrl,
7967
+ content: [
7968
+ `Image attached: ${pathHint}`,
7969
+ opts.path ? `Use the Read tool on \`${opts.path}\` to view this image.` : "The image is shown in the composer; copy it into the worktree if you need to inspect pixels."
7970
+ ].join("\n")
7971
+ };
7972
+ }
7973
+ if (buf.length > MAX_INLINE_BYTES) {
7974
+ return {
7975
+ id: (0, import_node_crypto3.randomUUID)(),
7976
+ name,
7977
+ kind: "file",
7978
+ path: opts.path,
7979
+ content: opts.path ? `(file too large to attach inline: \`${opts.path}\`, ${buf.length} bytes \u2014 use the Read tool)` : `(file too large to attach inline: ${opts.sourceLabel || name}, ${buf.length} bytes)`
7980
+ };
7981
+ }
7982
+ if (buf.includes(0)) {
7983
+ return {
7984
+ id: (0, import_node_crypto3.randomUUID)(),
7985
+ name,
7986
+ kind: "file",
7987
+ path: opts.path,
7988
+ content: opts.path ? `(binary file at \`${opts.path}\` \u2014 use tools to inspect)` : `(binary file attached by path only: ${opts.sourceLabel || name})`
7989
+ };
7990
+ }
7991
+ return {
7992
+ id: (0, import_node_crypto3.randomUUID)(),
7993
+ name,
7994
+ kind: "file",
7995
+ path: opts.path,
7996
+ content: buf.toString("utf8")
7997
+ };
7998
+ }
7999
+ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
8000
+ if (absolutePaths.length === 0) return [];
8001
+ const dir = ensureAttachmentsDir(worktreePath);
8002
+ const out = [];
8003
+ for (const abs of absolutePaths) {
8004
+ const originalName = (0, import_node_path21.basename)(abs);
8005
+ try {
8006
+ const st = (0, import_node_fs22.statSync)(abs);
8007
+ if (!st.isFile()) continue;
8008
+ const name = uniqueAttachmentName(dir, originalName);
8009
+ const destAbs = (0, import_node_path21.join)(dir, name);
8010
+ (0, import_node_fs22.copyFileSync)(abs, destAbs);
8011
+ const rel = `${ATTACHMENTS_DIR}/${name}`;
8012
+ const buf = (0, import_node_fs22.readFileSync)(destAbs);
8013
+ out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
8014
+ } catch (err) {
8015
+ out.push({
8016
+ id: (0, import_node_crypto3.randomUUID)(),
8017
+ name: originalName,
8018
+ kind: "file",
8019
+ content: `(could not attach ${abs}: ${err instanceof Error ? err.message : String(err)})`
8020
+ });
8021
+ }
8022
+ }
8023
+ return out;
8024
+ }
8025
+ function stageBuffersAsAttachments(worktreePath, buffers) {
8026
+ if (buffers.length === 0) return [];
8027
+ const dir = ensureAttachmentsDir(worktreePath);
8028
+ const out = [];
8029
+ for (const item of buffers) {
8030
+ const originalName = (item.name || "file").replace(/[/\\]/g, "_") || "file";
8031
+ try {
8032
+ const buf = Buffer.from(item.dataBase64, "base64");
8033
+ const name = uniqueAttachmentName(dir, originalName);
8034
+ const destAbs = (0, import_node_path21.join)(dir, name);
8035
+ (0, import_node_fs22.writeFileSync)(destAbs, buf);
8036
+ const rel = `${ATTACHMENTS_DIR}/${name}`;
8037
+ out.push(attachmentFromBuffer(name, buf, { path: rel }));
8038
+ } catch (err) {
8039
+ out.push({
8040
+ id: (0, import_node_crypto3.randomUUID)(),
8041
+ name: originalName,
8042
+ kind: "file",
8043
+ content: `(could not attach ${originalName}: ${err instanceof Error ? err.message : String(err)})`
8044
+ });
8045
+ }
8046
+ }
8047
+ return out;
8048
+ }
8049
+ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
8050
+ const out = [];
8051
+ for (const rel of relativePaths) {
8052
+ if (!rel || rel.includes("..") || rel.startsWith("/")) {
8053
+ out.push({
8054
+ id: (0, import_node_crypto3.randomUUID)(),
8055
+ name: (0, import_node_path21.basename)(rel) || "file",
8056
+ kind: "file",
8057
+ content: `(invalid path: ${rel})`
8058
+ });
8059
+ continue;
8060
+ }
8061
+ const name = (0, import_node_path21.basename)(rel);
8062
+ try {
8063
+ const abs = (0, import_node_path21.join)(worktreePath, rel);
8064
+ const st = (0, import_node_fs22.statSync)(abs);
8065
+ if (!st.isFile()) continue;
8066
+ const buf = (0, import_node_fs22.readFileSync)(abs);
8067
+ out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
8068
+ } catch (err) {
8069
+ out.push({
8070
+ id: (0, import_node_crypto3.randomUUID)(),
8071
+ name,
8072
+ kind: "file",
8073
+ content: `(could not read ${rel}: ${err instanceof Error ? err.message : String(err)})`
8074
+ });
8075
+ }
8076
+ }
8077
+ return out;
8078
+ }
8079
+
8080
+ // src/agents/instructions.ts
8081
+ var import_node_fs23 = require("fs");
8082
+ var import_node_path22 = require("path");
7561
8083
  init_worktree_labels();
7562
8084
  function normPath2(p) {
7563
8085
  return p.replace(/\/+$/, "");
@@ -7671,11 +8193,11 @@ function loadAgentInstructions(worktreePath, agent) {
7671
8193
  const out = [];
7672
8194
  for (const rel of candidates) {
7673
8195
  if (seen.has(rel)) continue;
7674
- const abs = (0, import_node_path21.join)(worktreePath, rel);
7675
- if (!(0, import_node_fs22.existsSync)(abs)) continue;
8196
+ const abs = (0, import_node_path22.join)(worktreePath, rel);
8197
+ if (!(0, import_node_fs23.existsSync)(abs)) continue;
7676
8198
  try {
7677
- if (!(0, import_node_fs22.statSync)(abs).isFile()) continue;
7678
- let content = (0, import_node_fs22.readFileSync)(abs, "utf8");
8199
+ if (!(0, import_node_fs23.statSync)(abs).isFile()) continue;
8200
+ let content = (0, import_node_fs23.readFileSync)(abs, "utf8");
7679
8201
  if (!content.trim()) continue;
7680
8202
  if (content.length > MAX_CHARS_PER_FILE) {
7681
8203
  content = `${content.slice(0, MAX_CHARS_PER_FILE)}
@@ -7801,7 +8323,7 @@ var Orchestrator = class {
7801
8323
  }
7802
8324
  continue;
7803
8325
  }
7804
- if (!(0, import_node_fs23.existsSync)(thread.worktreePath)) {
8326
+ if (!(0, import_node_fs24.existsSync)(thread.worktreePath)) {
7805
8327
  setStatus(thread.id, "broken", "Worktree missing on disk");
7806
8328
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
7807
8329
  continue;
@@ -8054,6 +8576,7 @@ var Orchestrator = class {
8054
8576
  ...fresh.agent === "claude" && fresh.sessionId ? [] : [instructions, seed]
8055
8577
  ].filter(Boolean).join("\n\n---\n\n");
8056
8578
  try {
8579
+ let lastStderr = "";
8057
8580
  const handle = await spawnAgentTurn(
8058
8581
  fresh,
8059
8582
  { cachedPrefix, prompt: agentPrompt },
@@ -8062,6 +8585,9 @@ var Orchestrator = class {
8062
8585
  if (event.type === "session_id") {
8063
8586
  updateThread(threadId, { sessionId: event.data });
8064
8587
  }
8588
+ if (event.type === "stderr" && typeof event.data === "string" && event.data.trim()) {
8589
+ lastStderr = event.data.trim();
8590
+ }
8065
8591
  }
8066
8592
  );
8067
8593
  this.activeTurns.set(threadId, handle);
@@ -8107,7 +8633,12 @@ var Orchestrator = class {
8107
8633
  this.emit({ type: "status_changed", threadId, status: "stopped" });
8108
8634
  this.emit({ type: "turn_finished", threadId, exitCode: result.exitCode });
8109
8635
  } else {
8110
- setStatus(threadId, result.exitCode === 0 ? "idle" : "error", result.exitCode === 0 ? null : `exit ${result.exitCode}`);
8636
+ const failDetail = lastStderr ? `exit ${result.exitCode}: ${lastStderr.slice(0, 500)}` : `exit ${result.exitCode}`;
8637
+ setStatus(
8638
+ threadId,
8639
+ result.exitCode === 0 ? "idle" : "error",
8640
+ result.exitCode === 0 ? null : failDetail
8641
+ );
8111
8642
  this.emit({
8112
8643
  type: "status_changed",
8113
8644
  threadId,
@@ -8581,9 +9112,16 @@ var Orchestrator = class {
8581
9112
  if (patch.planMode !== void 0) next.planMode = patch.planMode;
8582
9113
  if (patch.model !== void 0) next.model = patch.model;
8583
9114
  if (patch.agent !== void 0) {
8584
- next.agent = patch.agent;
8585
- if (patch.agent !== "claude" && patch.model === void 0) next.model = null;
8586
- next.sessionId = null;
9115
+ if (patch.agent !== thread.agent) {
9116
+ if (thread.messages.length > 0) {
9117
+ throw new Error(
9118
+ `Cannot switch agent provider mid-chat (${thread.agent} \u2192 ${patch.agent}). Start a new chat tab instead.`
9119
+ );
9120
+ }
9121
+ next.agent = patch.agent;
9122
+ if (patch.agent !== "claude" && patch.model === void 0) next.model = null;
9123
+ next.sessionId = null;
9124
+ }
8587
9125
  }
8588
9126
  return updateThread(thread.id, next);
8589
9127
  }
@@ -8613,6 +9151,23 @@ var Orchestrator = class {
8613
9151
  setAttachments(threadRef, attachments) {
8614
9152
  return updateThread(this.requireThread(threadRef).id, { attachments });
8615
9153
  }
9154
+ /**
9155
+ * Stage OS / worktree files into composer attachments (copies external files
9156
+ * into `.sideboard/attachments/` so agents can Read images and binaries).
9157
+ */
9158
+ attachComposerFiles(threadRef, opts) {
9159
+ const thread = this.requireThread(threadRef);
9160
+ const fromAbs = stageAbsolutePathsAsAttachments(
9161
+ thread.worktreePath,
9162
+ opts.absolutePaths ?? []
9163
+ );
9164
+ const fromRel = attachmentsFromWorktreePaths(
9165
+ thread.worktreePath,
9166
+ opts.relativePaths ?? []
9167
+ );
9168
+ const fromBuf = stageBuffersAsAttachments(thread.worktreePath, opts.buffers ?? []);
9169
+ return [...fromAbs, ...fromRel, ...fromBuf];
9170
+ }
8616
9171
  listWorktreeChats(threadRef) {
8617
9172
  const thread = this.requireThread(threadRef);
8618
9173
  return threadsSharingWorktree(thread.worktreePath);
@@ -8672,7 +9227,7 @@ var Orchestrator = class {
8672
9227
  updateThread(thread.id, { worktreePath: globalAgentCwd2() });
8673
9228
  return setStatus(thread.id, "idle");
8674
9229
  }
8675
- if (!(0, import_node_fs23.existsSync)(thread.worktreePath)) {
9230
+ if (!(0, import_node_fs24.existsSync)(thread.worktreePath)) {
8676
9231
  const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
8677
9232
  const { execa: execa7 } = await import("execa");
8678
9233
  const slug = thread.worktreePath.split("/").pop();
@@ -8890,7 +9445,7 @@ async function startMcpServer() {
8890
9445
  async () => {
8891
9446
  const threads = orch.getThreads(true);
8892
9447
  const lines = threads.map((t) => {
8893
- const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path22.basename)(t.repoPath) || t.repoPath;
9448
+ const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path23.basename)(t.repoPath) || t.repoPath;
8894
9449
  return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}`;
8895
9450
  });
8896
9451
  return {