agent-workbench 0.4.0 → 0.5.0

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/server.js CHANGED
@@ -170,6 +170,7 @@ function noticeText(part) {
170
170
  return part.detail.length > 0 ? `La CLI devolvi\xF3 un error: ${part.detail}` : "La CLI devolvi\xF3 un error";
171
171
  }
172
172
  }
173
+ var MAX_SUBAGENT_DESCRIPTION = 120;
173
174
  var EMPTY_CONTEXT_USAGE = {
174
175
  lastRequestTokens: 0,
175
176
  lastOutputTokens: 0,
@@ -679,6 +680,38 @@ function cleanDeviceLabel(label) {
679
680
  return label.replace(/\s+/g, " ").trim().slice(0, REMOTE_DEVICE_LABEL_MAX_CHARS);
680
681
  }
681
682
 
683
+ // packages/shared/src/settings.ts
684
+ var CONSOLE_SHELL_IDS = [
685
+ "auto",
686
+ "pwsh",
687
+ "powershell",
688
+ "cmd",
689
+ "git-bash",
690
+ "login",
691
+ "bash",
692
+ "sh",
693
+ "zsh"
694
+ ];
695
+ function asConsoleShellId(value) {
696
+ return asLiteral(value, CONSOLE_SHELL_IDS);
697
+ }
698
+ function asAgentIdList(value) {
699
+ if (!Array.isArray(value)) return null;
700
+ return [...new Set(value.filter((item) => typeof item === "string" && isAgentId(item)))];
701
+ }
702
+ function parseAppSettingsChange(record) {
703
+ const rawShell = record["consoleShell"];
704
+ const rawAgents = record["disabledAgents"];
705
+ const consoleShell = rawShell === void 0 ? void 0 : asConsoleShellId(rawShell);
706
+ const disabledAgents = rawAgents === void 0 ? void 0 : asAgentIdList(rawAgents);
707
+ if (consoleShell === null || disabledAgents === null) return null;
708
+ if (consoleShell === void 0 && disabledAgents === void 0) return null;
709
+ return {
710
+ ...consoleShell !== void 0 ? { consoleShell } : {},
711
+ ...disabledAgents !== void 0 ? { disabledAgents } : {}
712
+ };
713
+ }
714
+
682
715
  // packages/shared/src/protocol.ts
683
716
  var PROTOCOL_VERSION = 8;
684
717
  var WS_PATH = "/ws";
@@ -738,6 +771,8 @@ function parseClientMessage(raw) {
738
771
  if (files.length > 0) message.files = files;
739
772
  }
740
773
  if (record["send"] === false) message.send = false;
774
+ const requestId = asNonEmptyString(record["requestId"]);
775
+ if (requestId !== null) message.requestId = requestId;
741
776
  return message;
742
777
  }
743
778
  case "agent.mode": {
@@ -1067,6 +1102,10 @@ function parseClientMessage(raw) {
1067
1102
  const draft = parseComposerDraft(record["draft"]);
1068
1103
  return terminalId === null || draft === null ? null : { type: "composer.draft", terminalId, draft };
1069
1104
  }
1105
+ case "settings.update": {
1106
+ const change = parseAppSettingsChange(record);
1107
+ return change === null ? null : { type: "settings.update", ...change };
1108
+ }
1070
1109
  default:
1071
1110
  return null;
1072
1111
  }
@@ -5069,6 +5108,135 @@ function toTurnDuration(record) {
5069
5108
  return { eventId, durationMs };
5070
5109
  }
5071
5110
 
5111
+ // packages/server/src/agents/claude-code/subagent-tracker.ts
5112
+ var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
5113
+ var MAX_TRACKED = 50;
5114
+ var NOTIFICATION = /<task-notification>([\s\S]*?)<\/task-notification>/g;
5115
+ var TOOL_USE_ID = /<tool-use-id>\s*([^<\s]+)\s*<\/tool-use-id>/;
5116
+ function labelOf(value) {
5117
+ if (typeof value !== "string") return "";
5118
+ return value.replace(/\s+/g, " ").trim().slice(0, MAX_SUBAGENT_DESCRIPTION);
5119
+ }
5120
+ function notifiedToolUseIds(text) {
5121
+ if (!text.includes("<task-notification>")) return [];
5122
+ const ids = [];
5123
+ for (const match of text.matchAll(NOTIFICATION)) {
5124
+ const id = TOOL_USE_ID.exec(match[1] ?? "")?.[1];
5125
+ if (id !== void 0) ids.push(id);
5126
+ }
5127
+ return ids;
5128
+ }
5129
+ function contentText(content) {
5130
+ if (typeof content === "string") return content;
5131
+ if (!Array.isArray(content)) return "";
5132
+ let text = "";
5133
+ for (const block of content) {
5134
+ const record = recordOf(block);
5135
+ if (record !== null && record["type"] === "text" && typeof record["text"] === "string") {
5136
+ text += record["text"];
5137
+ }
5138
+ }
5139
+ return text;
5140
+ }
5141
+ function launchedInBackground(toolUseResult) {
5142
+ const result = recordOf(toolUseResult);
5143
+ return result !== null && (result["status"] === "async_launched" || result["isAsync"] === true);
5144
+ }
5145
+ var SubagentTracker = class {
5146
+ /** Los que no terminaron, en orden de lanzamiento (`Map` conserva el de insercion). */
5147
+ pending = /* @__PURE__ */ new Map();
5148
+ /** Una linea del JSONL, ya parseada. Nunca lanza: el esquema cambia. */
5149
+ observe(record) {
5150
+ if (record["isSidechain"] === true) return;
5151
+ switch (record["type"]) {
5152
+ case "assistant":
5153
+ this.observeLaunches(record);
5154
+ return;
5155
+ case "user":
5156
+ this.observeResults(record);
5157
+ this.finish(notifiedToolUseIds(contentText(recordOf(record["message"])?.["content"])));
5158
+ return;
5159
+ case "queue-operation":
5160
+ if (record["operation"] === "enqueue" && typeof record["content"] === "string") {
5161
+ this.finish(notifiedToolUseIds(record["content"]));
5162
+ }
5163
+ return;
5164
+ case "attachment": {
5165
+ const attachment = recordOf(record["attachment"]);
5166
+ if (attachment?.["type"] === "queued_command" && typeof attachment["prompt"] === "string") {
5167
+ this.finish(notifiedToolUseIds(attachment["prompt"]));
5168
+ }
5169
+ return;
5170
+ }
5171
+ default:
5172
+ return;
5173
+ }
5174
+ }
5175
+ /**
5176
+ * Los que siguen trabajando y los lanzo el proceso de `launchedAt` o uno
5177
+ * posterior. Uno sin hora no se puede atribuir a ningun proceso y no cuenta.
5178
+ */
5179
+ running(launchedAt) {
5180
+ const result = [];
5181
+ for (const subagent of this.pending.values()) {
5182
+ if (subagent.startedAt > 0 && subagent.startedAt >= launchedAt) result.push({ ...subagent });
5183
+ }
5184
+ return result;
5185
+ }
5186
+ /** El archivo se reemplazo: lo anotado era de otra lectura. */
5187
+ reset() {
5188
+ this.pending.clear();
5189
+ }
5190
+ observeLaunches(record) {
5191
+ const content = recordOf(record["message"])?.["content"];
5192
+ if (!Array.isArray(content)) return;
5193
+ const startedAt = readTimestamp(record["timestamp"]);
5194
+ for (const block of content) {
5195
+ const call = recordOf(block);
5196
+ if (call === null || call["type"] !== "tool_use") continue;
5197
+ const toolUseId = call["id"];
5198
+ if (typeof toolUseId !== "string" || toolUseId.length === 0) continue;
5199
+ if (typeof call["name"] !== "string" || !SUBAGENT_TOOLS.has(call["name"])) continue;
5200
+ if (this.pending.has(toolUseId)) continue;
5201
+ const input = recordOf(call["input"]);
5202
+ this.pending.set(toolUseId, {
5203
+ toolUseId,
5204
+ description: labelOf(input?.["description"]),
5205
+ background: input?.["run_in_background"] === true,
5206
+ startedAt
5207
+ });
5208
+ if (this.pending.size > MAX_TRACKED) {
5209
+ const oldest = this.pending.keys().next();
5210
+ if (!oldest.done) this.pending.delete(oldest.value);
5211
+ }
5212
+ }
5213
+ }
5214
+ /**
5215
+ * El resultado de la llamada. En segundo plano dice que quedo andando, y
5216
+ * sigue; si no, termino —o no llego a lanzarse, que para el hilo es lo mismo—.
5217
+ */
5218
+ observeResults(record) {
5219
+ const content = recordOf(record["message"])?.["content"];
5220
+ if (!Array.isArray(content)) return;
5221
+ for (const block of content) {
5222
+ const result = recordOf(block);
5223
+ if (result === null || result["type"] !== "tool_result") continue;
5224
+ const toolUseId = result["tool_use_id"];
5225
+ if (typeof toolUseId !== "string") continue;
5226
+ const subagent = this.pending.get(toolUseId);
5227
+ if (subagent === void 0) continue;
5228
+ if (result["is_error"] !== true && launchedInBackground(record["toolUseResult"])) {
5229
+ subagent.background = true;
5230
+ } else {
5231
+ this.pending.delete(toolUseId);
5232
+ }
5233
+ }
5234
+ }
5235
+ finish(toolUseIds) {
5236
+ for (const toolUseId of toolUseIds) this.pending.delete(toolUseId);
5237
+ }
5238
+ };
5239
+
5072
5240
  // packages/server/src/agents/claude-code/conversation-follower.ts
5073
5241
  var MAX_ATTACHMENT_LINKS = 100;
5074
5242
  var MAX_PLAN_REFS = 50;
@@ -5155,6 +5323,11 @@ var ConversationFollower = class {
5155
5323
  * ninguna—, y ahi vale con que se lanzo la pestana (`LAUNCH_PERMISSION_MODE`).
5156
5324
  */
5157
5325
  permissionMode = null;
5326
+ /**
5327
+ * Los subagentes que no terminaron (§4.15). Mira lineas que no son tarjetas
5328
+ * —el aviso de fin es una `queue-operation`—, asi que va antes de todo filtro.
5329
+ */
5330
+ subagents = new SubagentTracker();
5158
5331
  /** Los topes de cada parte. Los de transporte salvo que se lea para la copia propia. */
5159
5332
  limits;
5160
5333
  /** La pestana cuyos documentos se reconocen. Ver el constructor. */
@@ -5207,6 +5380,10 @@ var ConversationFollower = class {
5207
5380
  getUsage() {
5208
5381
  return this.usage;
5209
5382
  }
5383
+ /** Los subagentes del proceso de `launchedAt` que siguen trabajando (§4.15). */
5384
+ runningSubagents(launchedAt) {
5385
+ return this.subagents.running(launchedAt);
5386
+ }
5210
5387
  /** Ultimos `limit` eventos, que es lo que se quiere ver al abrir el panel. */
5211
5388
  getTail(limit) {
5212
5389
  return this.jsonl.getTail(limit);
@@ -5252,6 +5429,7 @@ var ConversationFollower = class {
5252
5429
  this.permissionMode = null;
5253
5430
  this.attachmentRoots.clear();
5254
5431
  this.planFiles = [];
5432
+ this.subagents.reset();
5255
5433
  }
5256
5434
  /** true si la linea es la invocacion de `/model` que la CLI guarda. */
5257
5435
  mentionsModelCommand(record) {
@@ -5268,6 +5446,7 @@ var ConversationFollower = class {
5268
5446
  * los que ya guardo, para pegarle a uno anterior la duracion o una imagen.
5269
5447
  */
5270
5448
  consume(record, lineNumber, events) {
5449
+ this.subagents.observe(record);
5271
5450
  if (record["type"] === "cost-state") {
5272
5451
  this.sessionVariants.observeCostState(record, Date.now());
5273
5452
  this.recomputeModel();
@@ -5647,6 +5826,10 @@ var ClaudeCodeSessionFollower = class {
5647
5826
  hasOpenToolCall() {
5648
5827
  return false;
5649
5828
  }
5829
+ /** Los subagentes del proceso de `launchedAt` que siguen trabajando (§4.15). */
5830
+ runningSubagents(launchedAt) {
5831
+ return this.inner.runningSubagents(launchedAt);
5832
+ }
5650
5833
  };
5651
5834
 
5652
5835
  // packages/server/src/agents/claude-code/session-scan.ts
@@ -9967,28 +10150,48 @@ var AgentRegistry = class {
9967
10150
  agents = /* @__PURE__ */ new Map();
9968
10151
  /** Las suscripciones a cambios de configuracion que siguen abiertas, para soltarlas al apagar. */
9969
10152
  changeSubscriptions = /* @__PURE__ */ new Set();
10153
+ /**
10154
+ * Las que el usuario apago en Ajustes (Hito 41, §6.32), tal como estaban al
10155
+ * arrancar. Una apagada es, para todo el resto, una CLI que no esta: no se
10156
+ * busca —no se corre su `--version`—, no se lanza, y su historial no se lee
10157
+ * (`historySources`). Cambiarlo vale al reiniciar.
10158
+ */
10159
+ disabled;
9970
10160
  /** El orden del array es el orden de preferencia para `defaultAgent`. */
9971
- constructor(adapters) {
10161
+ constructor(adapters, disabled = []) {
9972
10162
  for (const adapter of adapters) {
9973
10163
  if (this.agents.has(adapter.id)) {
9974
10164
  throw new Error(`Adapter registered twice: ${adapter.id}`);
9975
10165
  }
9976
10166
  this.agents.set(adapter.id, { adapter, location: null });
9977
10167
  }
10168
+ this.disabled = new Set([...disabled].filter((id) => this.agents.has(id)));
9978
10169
  }
9979
10170
  /**
9980
10171
  * Localiza todos a la vez. Se llama una vez al arrancar.
9981
10172
  *
9982
10173
  * En paralelo porque cada uno puede correr `--version`, y eso son segundos
9983
- * que el arranque no tiene por que sumar.
10174
+ * que el arranque no tiene por que sumar. Una apagada no se busca.
9984
10175
  */
9985
10176
  async locateAll() {
9986
10177
  await Promise.all(
9987
10178
  [...this.agents.values()].map(async (entry) => {
9988
- entry.location = await entry.adapter.locate();
10179
+ entry.location = this.disabled.has(entry.adapter.id) ? null : await entry.adapter.locate();
9989
10180
  })
9990
10181
  );
9991
10182
  }
10183
+ /** Las que este arranque dejo apagadas. */
10184
+ disabledIds() {
10185
+ return [...this.agents.keys()].filter((id) => this.disabled.has(id));
10186
+ }
10187
+ /**
10188
+ * Las CLIs cuyo historial se lee: todas menos las apagadas. Instalada o no da
10189
+ * igual —el historial de una ausente se lee igual, apagado en la barra—; una
10190
+ * apagada es lo que el usuario pidio no ver.
10191
+ */
10192
+ historySources() {
10193
+ return this.all().filter((entry) => !this.disabled.has(entry.adapter.id));
10194
+ }
9992
10195
  /**
9993
10196
  * Lo que cada CLI encontrada prepara en las carpetas de la app
9994
10197
  * (`AgentAdapter.prepare`). Se llama una vez al arrancar, despues de
@@ -10140,13 +10343,13 @@ function resolveAgentForOpen(input) {
10140
10343
  }
10141
10344
  return fromTabs ?? input.defaultAgent;
10142
10345
  }
10143
- function createAgentRegistry() {
10346
+ function createAgentRegistry(disabled = []) {
10144
10347
  const registry = new AgentRegistry([
10145
10348
  createClaudeCodeAdapter(),
10146
10349
  createCodexAdapter(),
10147
10350
  createOpenCodeAdapter({ serveEnv: () => registry.composedEnvironment(process.env) }),
10148
10351
  createAntigravityAdapter()
10149
- ]);
10352
+ ], disabled);
10150
10353
  return registry;
10151
10354
  }
10152
10355
 
@@ -10168,6 +10371,9 @@ var INITIAL_EVENT_LIMIT = 300;
10168
10371
  var MAX_PAGE_LIMIT = 500;
10169
10372
  var PENDING_QUESTION_LOOKBACK = 50;
10170
10373
  var POLL_DEBOUNCE_MS = 150;
10374
+ function sameSubagents(a, b) {
10375
+ return a.length === b.length && a.every((item, index) => item.toolUseId === b[index]?.toolUseId && item.background === b[index]?.background);
10376
+ }
10171
10377
  var ConversationHub = class extends EventEmitter {
10172
10378
  constructor(registry, agents) {
10173
10379
  super();
@@ -10175,7 +10381,10 @@ var ConversationHub = class extends EventEmitter {
10175
10381
  this.agents = agents;
10176
10382
  registry.on("session", (terminalId) => void this.rebind(terminalId));
10177
10383
  registry.on("changed", () => {
10178
- for (const [terminalId, entry] of this.entries) this.refreshOpenToolCall(terminalId, entry);
10384
+ for (const [terminalId, entry] of this.entries) {
10385
+ this.refreshOpenToolCall(terminalId, entry);
10386
+ this.refreshSubagents(terminalId, entry);
10387
+ }
10179
10388
  });
10180
10389
  }
10181
10390
  registry;
@@ -10199,6 +10408,7 @@ var ConversationHub = class extends EventEmitter {
10199
10408
  usage: { ...EMPTY_CONTEXT_USAGE },
10200
10409
  waitingFor: null,
10201
10410
  openToolCall: false,
10411
+ subagents: [],
10202
10412
  plans: []
10203
10413
  };
10204
10414
  }
@@ -10235,6 +10445,7 @@ var ConversationHub = class extends EventEmitter {
10235
10445
  permissionMode: entry.observedMode ?? entry.assumedMode,
10236
10446
  waitingFor: entry.waitingFor,
10237
10447
  openToolCall: entry.openToolCall,
10448
+ subagents: entry.subagents,
10238
10449
  plans: plans === null ? [] : await plans.describe(
10239
10450
  { cwd: entry.cwd, sessionId: entry.sessionId },
10240
10451
  entry.follower.getPlanFiles()
@@ -10284,6 +10495,7 @@ var ConversationHub = class extends EventEmitter {
10284
10495
  waitingFor: null,
10285
10496
  statusActivity: null,
10286
10497
  openToolCall: false,
10498
+ subagents: [],
10287
10499
  primed: false,
10288
10500
  stopWatchingStatus: null,
10289
10501
  followTicker: null
@@ -10326,6 +10538,23 @@ var ConversationHub = class extends EventEmitter {
10326
10538
  entry.openToolCall = open8;
10327
10539
  this.emit("toolCall", terminalId, open8);
10328
10540
  }
10541
+ /**
10542
+ * Recalcula los subagentes que siguen trabajando y avisa si cambiaron
10543
+ * (§4.15). Solo los del proceso vivo: sin pty no queda ninguno, y los de un
10544
+ * proceso anterior murieron con el sin escribir el aviso de fin. Una CLI cuyo
10545
+ * seguidor no los declara no tiene nunca.
10546
+ */
10547
+ refreshSubagents(terminalId, entry) {
10548
+ const launchedAt = this.registry.launchedAtOf(terminalId);
10549
+ const next = launchedAt === null || entry.follower.runningSubagents === void 0 ? [] : [...entry.follower.runningSubagents(launchedAt)];
10550
+ if (sameSubagents(next, entry.subagents)) return;
10551
+ entry.subagents = next;
10552
+ this.emit("subagents", terminalId, next);
10553
+ }
10554
+ /** Los subagentes que siguen trabajando, lo ultimo avisado. [] si nadie sigue la pestana. */
10555
+ getSubagents(terminalId) {
10556
+ return this.entries.get(terminalId)?.subagents ?? [];
10557
+ }
10329
10558
  /**
10330
10559
  * La pestana cambio de sesion: se suelta el seguidor viejo y se sigue la
10331
10560
  * nueva, sin perder a quien estaba mirando.
@@ -10506,6 +10735,7 @@ var ConversationHub = class extends EventEmitter {
10506
10735
  permissionMode: this.resetMode(entry),
10507
10736
  waitingFor: entry.waitingFor,
10508
10737
  openToolCall: entry.openToolCall,
10738
+ subagents: entry.subagents,
10509
10739
  // Este snapshot es sincronico y describir un plan pide el disco. Quien
10510
10740
  // necesita los planes usa `subscribe`, que si puede esperar.
10511
10741
  plans: []
@@ -10662,6 +10892,7 @@ var ConversationHub = class extends EventEmitter {
10662
10892
  return;
10663
10893
  }
10664
10894
  this.refreshOpenToolCall(terminalId, entry);
10895
+ this.refreshSubagents(terminalId, entry);
10665
10896
  if (options.silent) return;
10666
10897
  const observed = entry.follower.getPermissionMode();
10667
10898
  if (observed !== null) {
@@ -11030,6 +11261,7 @@ var OPENS_ON_HOST = /* @__PURE__ */ new Set([
11030
11261
  ]);
11031
11262
  function remoteRefusal(type, remoteClient) {
11032
11263
  if (!remoteClient) return null;
11264
+ if (type === "settings.update") return serverText("settingsHostOnly");
11033
11265
  if (HOST_ONLY.has(type)) return serverText("remoteHostOnly");
11034
11266
  if (OPENS_ON_HOST.has(type)) return serverText("remoteOpensOnHost");
11035
11267
  return null;
@@ -14288,7 +14520,10 @@ function vaultOnlySessions(natives, vault, listState, ready) {
14288
14520
  for (const copy of vault) {
14289
14521
  const pair = vaultPairKey(copy.agent, copy.sessionId);
14290
14522
  if (native.has(pair) || seen.has(pair)) continue;
14291
- if (!isImportedAgentId(copy.agent) && (listState.get(copy.agent) ?? "missing") === "unreadable") continue;
14523
+ if (!isImportedAgentId(copy.agent)) {
14524
+ const state = listState.get(copy.agent) ?? "missing";
14525
+ if (state === "unreadable" || state === "off") continue;
14526
+ }
14292
14527
  seen.add(pair);
14293
14528
  const hasCwd = copy.cwd.length > 0;
14294
14529
  result.push({
@@ -14670,7 +14905,7 @@ var SessionIndex = class extends EventEmitter4 {
14670
14905
  }
14671
14906
  this.scanning = true;
14672
14907
  try {
14673
- const sources = this.agents.all().map(({ adapter }) => ({
14908
+ const sources = (this.agents.historySources?.() ?? this.agents.all()).map(({ adapter }) => ({
14674
14909
  agent: adapter.id,
14675
14910
  history: adapter.history
14676
14911
  }));
@@ -14690,6 +14925,9 @@ var SessionIndex = class extends EventEmitter4 {
14690
14925
  const outcome = outcomes[position] ?? { items: null, threw: true };
14691
14926
  states.set(source.agent, await nativeListState(source.history, outcome));
14692
14927
  }
14928
+ for (const { adapter } of this.agents.all()) {
14929
+ if (!states.has(adapter.id)) states.set(adapter.id, "off");
14930
+ }
14693
14931
  this.listState = states;
14694
14932
  if (lists.every((items) => items === null)) {
14695
14933
  this.sessions.clear();
@@ -14875,7 +15113,9 @@ function defaultAppSettings() {
14875
15113
  return {
14876
15114
  version: SETTINGS_VERSION,
14877
15115
  vault: { enabled: false, dir: null, toolResultMaxChars: VAULT_TOOL_RESULT_DEFAULT_CHARS },
14878
- remote: { enabled: false, port: REMOTE_DEFAULT_PORT }
15116
+ remote: { enabled: false, port: REMOTE_DEFAULT_PORT },
15117
+ console: { shell: "auto" },
15118
+ agents: { disabled: [] }
14879
15119
  };
14880
15120
  }
14881
15121
  function isAbsoluteDir(dir, platform) {
@@ -14891,6 +15131,12 @@ function parseAppSettings(value, platform, warn = () => void 0) {
14891
15131
  const record = asRecord(value);
14892
15132
  if (record === null || record["version"] !== SETTINGS_VERSION) return null;
14893
15133
  const settings2 = defaultAppSettings();
15134
+ const consoleRecord = asRecord(record["console"]);
15135
+ const shell = consoleRecord === null ? null : asLiteral(consoleRecord["shell"], CONSOLE_SHELL_IDS);
15136
+ if (shell !== null) settings2.console.shell = shell;
15137
+ const agentsRecord = asRecord(record["agents"]);
15138
+ const disabled = agentsRecord === null ? null : asAgentIdList(agentsRecord["disabled"]);
15139
+ if (disabled !== null) settings2.agents.disabled = disabled;
14894
15140
  const remote = asRecord(record["remote"]);
14895
15141
  if (remote !== null) {
14896
15142
  settings2.remote.enabled = asBoolean(remote["enabled"]) ?? false;
@@ -14903,7 +15149,7 @@ function parseAppSettings(value, platform, warn = () => void 0) {
14903
15149
  if (typeof dir === "string" && isAbsoluteDir(dir, platform)) {
14904
15150
  settings2.vault.dir = dir;
14905
15151
  } else if (dir !== null && dir !== void 0) {
14906
- warn(`[settings] the local copy folder ${JSON.stringify(dir)} isn't an absolute path: using the default one`);
15152
+ warn(`[settings] the backup folder ${JSON.stringify(dir)} isn't an absolute path: using the default one`);
14907
15153
  }
14908
15154
  settings2.vault.toolResultMaxChars = clampToolResultMaxChars(vault["toolResultMaxChars"]);
14909
15155
  return settings2;
@@ -14912,7 +15158,9 @@ function frozen(settings2) {
14912
15158
  return Object.freeze({
14913
15159
  ...settings2,
14914
15160
  vault: Object.freeze({ ...settings2.vault }),
14915
- remote: Object.freeze({ ...settings2.remote })
15161
+ remote: Object.freeze({ ...settings2.remote }),
15162
+ console: Object.freeze({ ...settings2.console }),
15163
+ agents: Object.freeze({ disabled: Object.freeze([...settings2.agents.disabled]) })
14916
15164
  });
14917
15165
  }
14918
15166
  var SettingsStore = class {
@@ -14992,19 +15240,23 @@ var SettingsStore = class {
14992
15240
  const next = {
14993
15241
  version: SETTINGS_VERSION,
14994
15242
  vault: { ...this.current.vault },
14995
- remote: { ...this.current.remote }
15243
+ remote: { ...this.current.remote },
15244
+ console: { ...this.current.console },
15245
+ agents: { disabled: [...this.current.agents.disabled] }
14996
15246
  };
14997
15247
  const vault = patch.vault ?? {};
14998
15248
  if (vault.enabled !== void 0) next.vault.enabled = vault.enabled;
14999
15249
  if (vault.dir !== void 0) {
15000
15250
  if (vault.dir !== null && !isAbsoluteDir(vault.dir, this.platform)) {
15001
- throw new Error(`The local copy folder has to be an absolute path: ${JSON.stringify(vault.dir)}`);
15251
+ throw new Error(`The backup folder has to be an absolute path: ${JSON.stringify(vault.dir)}`);
15002
15252
  }
15003
15253
  next.vault.dir = vault.dir;
15004
15254
  }
15005
15255
  if (vault.toolResultMaxChars !== void 0) {
15006
15256
  next.vault.toolResultMaxChars = clampToolResultMaxChars(vault.toolResultMaxChars);
15007
15257
  }
15258
+ if (patch.console?.shell !== void 0) next.console.shell = patch.console.shell;
15259
+ if (patch.agents?.disabled !== void 0) next.agents.disabled = [...new Set(patch.agents.disabled)];
15008
15260
  const remote = patch.remote ?? {};
15009
15261
  if (remote.enabled !== void 0) next.remote.enabled = remote.enabled;
15010
15262
  if (remote.port !== void 0) {
@@ -15564,39 +15816,131 @@ async function isExecutable(candidate) {
15564
15816
  return false;
15565
15817
  }
15566
15818
  }
15567
- function windowsPowerShellPath() {
15568
- const systemRoot = process.env["SystemRoot"] ?? process.env["windir"];
15819
+ var SYSTEM_PROBE = {
15820
+ platform: process.platform,
15821
+ env: process.env,
15822
+ findInPath,
15823
+ isExecutable
15824
+ };
15825
+ function windowsPowerShellPath(env) {
15826
+ const systemRoot = env["SystemRoot"] ?? env["windir"];
15569
15827
  if (systemRoot === void 0 || systemRoot.length === 0) return null;
15570
- return path40.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
15828
+ return path40.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
15571
15829
  }
15572
- async function locateWindowsShell() {
15573
- const pwsh = await findInPath("pwsh");
15574
- if (pwsh !== null) return { file: pwsh, args: ["-NoLogo"], name: "PowerShell" };
15575
- const fromPath = await findInPath("powershell");
15576
- if (fromPath !== null) return { file: fromPath, args: ["-NoLogo"], name: "PowerShell" };
15577
- const systemCopy = windowsPowerShellPath();
15578
- if (systemCopy !== null && await isExecutable(systemCopy)) {
15579
- return { file: systemCopy, args: ["-NoLogo"], name: "PowerShell" };
15830
+ function gitBashCandidates(gitPath, env) {
15831
+ const candidates = [];
15832
+ if (gitPath !== null) {
15833
+ let dir = path40.win32.dirname(gitPath);
15834
+ for (let level = 0; level < 3; level++) {
15835
+ candidates.push(path40.win32.join(dir, "bin", "bash.exe"));
15836
+ const parent = path40.win32.dirname(dir);
15837
+ if (parent === dir) break;
15838
+ dir = parent;
15839
+ }
15580
15840
  }
15581
- const comSpec = process.env["ComSpec"];
15582
- if (comSpec !== void 0 && comSpec.length > 0 && await isExecutable(comSpec)) {
15583
- return { file: comSpec, args: [], name: "cmd" };
15841
+ for (const base of [env["ProgramFiles"], env["LOCALAPPDATA"] === void 0 ? void 0 : path40.win32.join(env["LOCALAPPDATA"], "Programs")]) {
15842
+ if (base !== void 0 && base.length > 0) candidates.push(path40.win32.join(base, "Git", "bin", "bash.exe"));
15584
15843
  }
15585
- return null;
15844
+ const systemRoot = (env["SystemRoot"] ?? env["windir"] ?? "").toLowerCase();
15845
+ return [...new Set(candidates)].filter((candidate) => systemRoot.length === 0 || !candidate.toLowerCase().startsWith(systemRoot));
15586
15846
  }
15587
- async function locatePosixShell() {
15588
- const preferred = process.env["SHELL"];
15589
- if (preferred !== void 0 && preferred.length > 0 && await isExecutable(preferred)) {
15590
- return { file: preferred, args: [], name: path40.basename(preferred) };
15847
+ async function locateWindowsShells(probe) {
15848
+ const found = [];
15849
+ const pwsh = await probe.findInPath("pwsh");
15850
+ if (pwsh !== null) found.push({ id: "pwsh", label: "PowerShell 7", location: { file: pwsh, args: ["-NoLogo"], name: "PowerShell" } });
15851
+ const fromPath = await probe.findInPath("powershell");
15852
+ const systemCopy = windowsPowerShellPath(probe.env);
15853
+ const windowsPowerShell = fromPath ?? (systemCopy !== null && await probe.isExecutable(systemCopy) ? systemCopy : null);
15854
+ if (windowsPowerShell !== null) {
15855
+ found.push({ id: "powershell", label: "Windows PowerShell", location: { file: windowsPowerShell, args: ["-NoLogo"], name: "PowerShell" } });
15856
+ }
15857
+ const comSpec = probe.env["ComSpec"];
15858
+ if (comSpec !== void 0 && comSpec.length > 0 && await probe.isExecutable(comSpec)) {
15859
+ found.push({ id: "cmd", label: "cmd", location: { file: comSpec, args: [], name: "cmd" } });
15860
+ }
15861
+ for (const candidate of gitBashCandidates(await probe.findInPath("git"), probe.env)) {
15862
+ if (await probe.isExecutable(candidate)) {
15863
+ found.push({ id: "git-bash", label: "Git Bash", location: { file: candidate, args: ["--login", "-i"], name: "Git Bash" } });
15864
+ break;
15865
+ }
15591
15866
  }
15592
- if (await isExecutable("/bin/bash")) return { file: "/bin/bash", args: [], name: "bash" };
15593
- if (await isExecutable("/bin/sh")) return { file: "/bin/sh", args: [], name: "sh" };
15594
- return null;
15867
+ return found;
15868
+ }
15869
+ async function locatePosixShells(probe) {
15870
+ const found = [];
15871
+ const preferred = probe.env["SHELL"];
15872
+ if (preferred !== void 0 && preferred.length > 0 && await probe.isExecutable(preferred)) {
15873
+ const name = path40.posix.basename(preferred);
15874
+ found.push({ id: "login", label: name, location: { file: preferred, args: [], name } });
15875
+ }
15876
+ for (const [id, file] of [["bash", "/bin/bash"], ["sh", "/bin/sh"], ["zsh", "/bin/zsh"]]) {
15877
+ if (found.some((shell) => shell.location.file === file)) continue;
15878
+ if (await probe.isExecutable(file)) found.push({ id, label: id, location: { file, args: [], name: id } });
15879
+ }
15880
+ return found;
15595
15881
  }
15596
- async function locateShell() {
15597
- return process.platform === "win32" ? locateWindowsShell() : locatePosixShell();
15882
+ async function locateShells(probe = SYSTEM_PROBE) {
15883
+ return probe.platform === "win32" ? locateWindowsShells(probe) : locatePosixShells(probe);
15884
+ }
15885
+ var NEVER_AUTO = /* @__PURE__ */ new Set(["git-bash", "zsh"]);
15886
+ function pickShell(candidates, choice) {
15887
+ if (choice !== "auto") {
15888
+ const chosen = candidates.find((candidate) => candidate.id === choice);
15889
+ if (chosen !== void 0) return chosen.location;
15890
+ }
15891
+ return candidates.find((candidate) => !NEVER_AUTO.has(candidate.id))?.location ?? null;
15598
15892
  }
15599
15893
 
15894
+ // packages/server/src/app-settings-service.ts
15895
+ import { EventEmitter as EventEmitter5 } from "node:events";
15896
+ var AppSettingsChangeError = class extends Error {
15897
+ constructor(reason2) {
15898
+ super(reason2);
15899
+ this.reason = reason2;
15900
+ }
15901
+ reason;
15902
+ };
15903
+ var AppSettingsService = class extends EventEmitter5 {
15904
+ constructor(options) {
15905
+ super();
15906
+ this.options = options;
15907
+ this.disabledAtStart = options.agents.disabledIds();
15908
+ }
15909
+ options;
15910
+ disabledAtStart;
15911
+ /** La consola que abre una nueva con lo que dice `settings.json`. */
15912
+ currentShell() {
15913
+ return pickShell(this.options.shells, this.options.settings.get().console.shell);
15914
+ }
15915
+ status() {
15916
+ const settings2 = this.options.settings.get();
15917
+ return {
15918
+ consoleShell: settings2.console.shell,
15919
+ consoleShells: this.options.shells.map(({ id, label }) => ({ id, label })),
15920
+ shellName: this.currentShell()?.name ?? null,
15921
+ disabledAgents: [...settings2.agents.disabled],
15922
+ disabledAtStart: [...this.disabledAtStart]
15923
+ };
15924
+ }
15925
+ /** Guarda el cambio, avisa al registro si cambio la consola, y emite `status`. */
15926
+ async update(change) {
15927
+ if (change.consoleShell !== void 0 && !this.offered(change.consoleShell)) {
15928
+ throw new AppSettingsChangeError("shell-not-installed");
15929
+ }
15930
+ const before = this.currentShell();
15931
+ await this.options.settings.update({
15932
+ ...change.consoleShell !== void 0 ? { console: { shell: change.consoleShell } } : {},
15933
+ ...change.disabledAgents !== void 0 ? { agents: { disabled: change.disabledAgents } } : {}
15934
+ });
15935
+ const after = this.currentShell();
15936
+ if (after !== before) this.options.onShellChange(after);
15937
+ this.emit("status", this.status());
15938
+ }
15939
+ offered(choice) {
15940
+ return choice === "auto" || this.options.shells.some((shell) => shell.id === choice);
15941
+ }
15942
+ };
15943
+
15600
15944
  // packages/server/src/session-watcher.ts
15601
15945
  import { stat as stat21 } from "node:fs/promises";
15602
15946
  import chokidar3 from "chokidar";
@@ -15674,7 +16018,7 @@ function watchSessions(index, hub, agents, options = {}) {
15674
16018
  probe.unref();
15675
16019
  probes.add(probe);
15676
16020
  };
15677
- for (const { adapter } of agents.all()) {
16021
+ for (const { adapter } of agents.historySources?.() ?? agents.all()) {
15678
16022
  for (const root of adapter.history.roots()) {
15679
16023
  if (root.watch !== void 0) {
15680
16024
  subscribeRoot(adapter.id, root, root.watch);
@@ -15706,7 +16050,7 @@ function watchSessions(index, hub, agents, options = {}) {
15706
16050
 
15707
16051
  // packages/server/src/terminal-registry.ts
15708
16052
  import { randomUUID as randomUUID5 } from "node:crypto";
15709
- import { EventEmitter as EventEmitter5 } from "node:events";
16053
+ import { EventEmitter as EventEmitter6 } from "node:events";
15710
16054
  import { stat as stat22 } from "node:fs/promises";
15711
16055
 
15712
16056
  // packages/server/src/launch-hook-slot.ts
@@ -16139,7 +16483,7 @@ var MAX_TERMINALS = 24;
16139
16483
  var RESTART_EXIT_TIMEOUT_MS = 1e4;
16140
16484
  var DEFAULT_COLS = 80;
16141
16485
  var DEFAULT_ROWS = 24;
16142
- var TerminalRegistry = class extends EventEmitter5 {
16486
+ var TerminalRegistry = class extends EventEmitter6 {
16143
16487
  constructor(agents, shell, store, sessionAgentOf = () => null) {
16144
16488
  super();
16145
16489
  this.agents = agents;
@@ -16163,6 +16507,10 @@ var TerminalRegistry = class extends EventEmitter5 {
16163
16507
  unavailableTabs = [];
16164
16508
  /** Pestanas guardadas de una CLI que esta build no conoce. Idem, crudas. */
16165
16509
  foreignTabs = [];
16510
+ /** Otra consola para las que se abran desde ahora (Ajustes, Hito 41). Las abiertas no cambian. */
16511
+ setShell(shell) {
16512
+ this.shell = shell;
16513
+ }
16166
16514
  list() {
16167
16515
  return this.order.map((id) => this.terminals.get(id)?.descriptor).filter((descriptor) => descriptor !== void 0);
16168
16516
  }
@@ -16252,7 +16600,9 @@ var TerminalRegistry = class extends EventEmitter5 {
16252
16600
  cwd: options.cwd,
16253
16601
  // Provisional: `spawn` lo confirma con lo que diga el adaptador.
16254
16602
  sessionId: kind === "agent" ? options.resumeSessionId ?? randomUUID5() : "",
16255
- label: options.label ?? "",
16603
+ // Una consola lleva el nombre de la suya: desde el Hito 41 pueden convivir
16604
+ // PowerShell y Git Bash, y el nombre de la consola nueva ya no es el de todas.
16605
+ label: options.label ?? (kind === "shell" ? this.shell?.name ?? "" : ""),
16256
16606
  resumed,
16257
16607
  createdAt: Date.now(),
16258
16608
  // Lo pone `spawn`. Una entrada que no llega a tener proceso no queda en
@@ -19211,7 +19561,7 @@ function attachTerminalSocket(options) {
19211
19561
  httpServer,
19212
19562
  port,
19213
19563
  token,
19214
- shell,
19564
+ appSettings,
19215
19565
  registry,
19216
19566
  index,
19217
19567
  archived,
@@ -19331,6 +19681,7 @@ function attachTerminalSocket(options) {
19331
19681
  const onConversationMode = (terminalId, mode) => broadcast({ type: "conversation.mode", terminalId, mode });
19332
19682
  const onConversationWaiting = (terminalId, waitingFor) => broadcast({ type: "conversation.waiting", terminalId, waitingFor });
19333
19683
  const onConversationToolCall = (terminalId, open8) => broadcast({ type: "conversation.toolCall", terminalId, open: open8 });
19684
+ const onConversationSubagents = (terminalId, subagents) => broadcast({ type: "conversation.subagents", terminalId, subagents });
19334
19685
  const onConversationPlans = (terminalId, plans) => broadcast({ type: "conversation.plans", terminalId, plans });
19335
19686
  const onConversationTurns = (terminalId, turns) => broadcast({ type: "conversation.turns", terminalId, turns });
19336
19687
  const onConversationParts = (terminalId, updates) => broadcast({ type: "conversation.parts", terminalId, updates });
@@ -19394,7 +19745,8 @@ function attachTerminalSocket(options) {
19394
19745
  waitingFor: snapshot.waitingFor,
19395
19746
  // El de ahora y no el del snapshot: leer la configuracion es asincrono,
19396
19747
  // y un aviso que salio mientras tanto no puede quedar pisado por uno viejo.
19397
- openToolCall: conversations.isToolCallOpen(terminalId)
19748
+ openToolCall: conversations.isToolCallOpen(terminalId),
19749
+ subagents: conversations.getSubagents(terminalId)
19398
19750
  });
19399
19751
  });
19400
19752
  };
@@ -19409,6 +19761,7 @@ function attachTerminalSocket(options) {
19409
19761
  conversations.on("mode", onConversationMode);
19410
19762
  conversations.on("waiting", onConversationWaiting);
19411
19763
  conversations.on("toolCall", onConversationToolCall);
19764
+ conversations.on("subagents", onConversationSubagents);
19412
19765
  conversations.on("turns", onConversationTurns);
19413
19766
  conversations.on("plans", onConversationPlans);
19414
19767
  conversations.on("parts", onConversationParts);
@@ -19419,6 +19772,8 @@ function attachTerminalSocket(options) {
19419
19772
  const stopRemoteStatus = remote.onChange(
19420
19773
  () => broadcastToHost({ type: "remote.status", status: remote.status() })
19421
19774
  );
19775
+ const onAppSettingsStatus = (settings2) => broadcast({ type: "settings.status", settings: settings2 });
19776
+ appSettings.on("status", onAppSettingsStatus);
19422
19777
  const stopRemoteRevoked = remote.onRevoked((deviceId) => {
19423
19778
  for (const [socket, id] of remoteSockets) {
19424
19779
  if (deviceId === null || id === deviceId) socket.close(REMOTE_REVOKED_CLOSE_CODE, "revoked");
@@ -19531,7 +19886,7 @@ function attachTerminalSocket(options) {
19531
19886
  defaultAgent: agents.defaultAgent(),
19532
19887
  platform: process.platform,
19533
19888
  defaultCwd,
19534
- shellName: shell?.name ?? null,
19889
+ shellName: appSettings.currentShell()?.name ?? null,
19535
19890
  remoteClient: deviceId !== null
19536
19891
  });
19537
19892
  send(socket, terminalListMessage());
@@ -19544,6 +19899,7 @@ function attachTerminalSocket(options) {
19544
19899
  send(socket, { type: "composer.drafts", drafts: draftEntriesOf(registry.list()) });
19545
19900
  send(socket, { type: "vault.status", status: vault.status() });
19546
19901
  if (deviceId === null) send(socket, { type: "remote.status", status: remote.status() });
19902
+ send(socket, { type: "settings.status", settings: appSettings.status() });
19547
19903
  socket.on("message", (raw) => {
19548
19904
  const text = Array.isArray(raw) ? Buffer.concat(raw).toString("utf8") : Buffer.from(raw).toString("utf8");
19549
19905
  const message = parseClientMessage(text);
@@ -19578,6 +19934,12 @@ function attachTerminalSocket(options) {
19578
19934
  */
19579
19935
  case "agent.submit": {
19580
19936
  const { terminalId, text: text2, images } = message;
19937
+ let settled = false;
19938
+ const settle = (delivered) => {
19939
+ if (settled || message.requestId === void 0) return;
19940
+ settled = true;
19941
+ send(socket, { type: "agent.submitted", terminalId, requestId: message.requestId, delivered });
19942
+ };
19581
19943
  const submittedOwner = draftOwnerOf(registry.get(terminalId));
19582
19944
  if (submittedOwner !== null) drafts.delete(submittedOwner);
19583
19945
  const files = message.files ?? [];
@@ -19587,6 +19949,7 @@ function attachTerminalSocket(options) {
19587
19949
  "submit-failed",
19588
19950
  serverText("filesPerMessage", { max: MAX_FILES_PER_SUBMIT })
19589
19951
  );
19952
+ settle(false);
19590
19953
  break;
19591
19954
  }
19592
19955
  if (images.length > MAX_IMAGES_PER_SUBMIT) {
@@ -19595,9 +19958,11 @@ function attachTerminalSocket(options) {
19595
19958
  "submit-failed",
19596
19959
  serverText("imagesPerMessage", { max: MAX_IMAGES_PER_SUBMIT })
19597
19960
  );
19961
+ settle(false);
19598
19962
  break;
19599
19963
  }
19600
19964
  if (images.length > 0 && imageStyleFor(socket, terminalId, serverText("imagesUnsupported")) === null) {
19965
+ settle(false);
19601
19966
  break;
19602
19967
  }
19603
19968
  const submitting = adapterOf(terminalId);
@@ -19618,6 +19983,7 @@ function attachTerminalSocket(options) {
19618
19983
  });
19619
19984
  if (refusal2 !== null) {
19620
19985
  sendError(socket, "submit-failed", refusal2);
19986
+ settle(false);
19621
19987
  break;
19622
19988
  }
19623
19989
  }
@@ -19685,6 +20051,7 @@ function attachTerminalSocket(options) {
19685
20051
  approvalGuard,
19686
20052
  { startAfterMs }
19687
20053
  );
20054
+ settle(outcome === "written");
19688
20055
  if (outcome === "interrupted") interrupted();
19689
20056
  if (outcome === "blocked") {
19690
20057
  const waitingNow = blocksWhileWaiting && registry.activityOf(terminalId) === "waiting";
@@ -19696,7 +20063,7 @@ function attachTerminalSocket(options) {
19696
20063
  }
19697
20064
  },
19698
20065
  { onDropped: interrupted }
19699
- );
20066
+ ).then(() => settle(false));
19700
20067
  break;
19701
20068
  }
19702
20069
  /*
@@ -20014,7 +20381,8 @@ function attachTerminalSocket(options) {
20014
20381
  permissionMode: snapshot.permissionMode,
20015
20382
  defaults: await defaultsFor(message.terminalId),
20016
20383
  waitingFor: snapshot.waitingFor,
20017
- openToolCall: conversations.isToolCallOpen(message.terminalId)
20384
+ openToolCall: conversations.isToolCallOpen(message.terminalId),
20385
+ subagents: conversations.getSubagents(message.terminalId)
20018
20386
  });
20019
20387
  if (snapshot.plans.length > 0) {
20020
20388
  send(socket, {
@@ -20585,6 +20953,24 @@ function attachTerminalSocket(options) {
20585
20953
  case "search.cancel":
20586
20954
  globalSearch.cancel(message.searchId);
20587
20955
  break;
20956
+ /*
20957
+ Ajustes (Hito 41, §6.32). Una consola que ya no esta, o un disco que no
20958
+ deja escribir, se dicen; lo demas lo valido el parser. A una ventana
20959
+ remota no llega: lo frena `remoteRefusal`.
20960
+ */
20961
+ case "settings.update": {
20962
+ const { type: _type, ...change } = message;
20963
+ void appSettings.update(change).catch((error) => {
20964
+ if (error instanceof AppSettingsChangeError) {
20965
+ sendError(socket, "settings-failed", serverText("settingsShellMissing"));
20966
+ return;
20967
+ }
20968
+ const detail = error instanceof Error ? error.message : String(error);
20969
+ console.warn("[settings] a change failed:", detail);
20970
+ sendError(socket, "settings-failed", serverText("settingsFailed"), detail);
20971
+ });
20972
+ break;
20973
+ }
20588
20974
  /*
20589
20975
  El acceso remoto (hito 37). Hasta aca solo llega una ventana del
20590
20976
  anfitrion: a un equipo remoto lo freno `remoteRefusal`. El estado
@@ -20680,10 +21066,12 @@ function attachTerminalSocket(options) {
20680
21066
  conversations.off("mode", onConversationMode);
20681
21067
  conversations.off("waiting", onConversationWaiting);
20682
21068
  conversations.off("toolCall", onConversationToolCall);
21069
+ conversations.off("subagents", onConversationSubagents);
20683
21070
  repos.off("status", onGitStatus);
20684
21071
  memory.off("status", onMemoryStatus);
20685
21072
  stopAgentChanges();
20686
21073
  stopVaultStatus();
21074
+ appSettings.off("status", onAppSettingsStatus);
20687
21075
  stopRemoteStatus();
20688
21076
  stopRemoteRevoked();
20689
21077
  for (const client of wss.clients) client.terminate();
@@ -21070,17 +21458,18 @@ ${line}`);
21070
21458
  async function main() {
21071
21459
  const token = createSessionToken();
21072
21460
  const defaultCwd = resolveDefaultCwd();
21073
- const agents = createAgentRegistry();
21074
- const [, shell] = await Promise.all([agents.locateAll(), locateShell()]);
21075
21461
  const movedFrom = await migrateLegacyConfigDir();
21076
21462
  if (movedFrom !== null) console.log(`Configuration moved from ${movedFrom} to ${appConfigDir()}`);
21463
+ const settings2 = new SettingsStore();
21464
+ await settings2.load();
21465
+ const agents = createAgentRegistry(settings2.get().agents.disabled);
21466
+ const [, shells] = await Promise.all([agents.locateAll(), locateShells()]);
21467
+ const shell = pickShell(shells, settings2.get().console.shell);
21077
21468
  await agents.prepareAll();
21078
21469
  ensurePtyHelperExecutable();
21079
21470
  const store = new WorkspaceStore();
21080
21471
  const archived = new ArchivedSessions();
21081
21472
  await archived.load();
21082
- const settings2 = new SettingsStore();
21083
- await settings2.load();
21084
21473
  const catalog = new VaultCatalog();
21085
21474
  await catalog.load(settings2.get().vault.dir ?? defaultVaultDir());
21086
21475
  const index = new SessionIndex(agents, archived, catalog);
@@ -21092,6 +21481,12 @@ async function main() {
21092
21481
  store,
21093
21482
  (sessionId) => index.agentOf(sessionId)
21094
21483
  );
21484
+ const appSettings = new AppSettingsService({
21485
+ settings: settings2,
21486
+ agents,
21487
+ shells,
21488
+ onShellChange: (next) => registry.setShell(next)
21489
+ });
21095
21490
  const notes = new NotesStore();
21096
21491
  await notes.load();
21097
21492
  const drafts = new DraftStore();
@@ -21134,7 +21529,7 @@ async function main() {
21134
21529
  httpServer,
21135
21530
  port,
21136
21531
  token,
21137
- shell,
21532
+ appSettings,
21138
21533
  registry,
21139
21534
  index,
21140
21535
  archived,