agent-workbench 0.4.1 → 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/README.md CHANGED
@@ -33,14 +33,14 @@ the app touching it.
33
33
  | **History** | Your projects and past conversations in the sidebar, with a filter, and those of all four CLIs together under each project, each with its badge. Opening one resumes it with its CLI, in the same session. **Archive history…** hides a CLI's sessions from before today in one go, and each project can be archived whole with one button: nothing is deleted, and both can be undone. |
34
34
  | **Conversation** | The active session's messages, live, with tool calls collapsed and their results inside. A row at the bottom tells you when your message reached the CLI, whether the agent is working and, with Claude Code, which subagents are still at it after the agent finished its turn. Search, jump between matches and back to the end, copy any message or code block and, depending on the CLI, answer the agent's questions from the chat. |
35
35
  | **Continue with…** | With more than one CLI installed, a conversation can be continued with another CLI in the same folder. The new agent starts from a transcript of the last turns, not from the context the previous one had. |
36
- | **Search everything** | With more than one CLI and something saved in the local copy, the sidebar filter also searches the text of every saved conversation, not just their titles. |
36
+ | **Search everything** | With more than one CLI and something saved in the backup, the sidebar filter also searches the text of every saved conversation, not just their titles. |
37
37
  | **Context meter** | Tokens from the last request against the model's context window. Tokens, never money. |
38
38
  | **Changes** | Branch, ahead and behind against the upstream branch, worktrees, and the changed files with their diff. **Read-only.** |
39
39
  | **Files** | The tree of the tab's directory, with search by name and a preview with syntax highlighting. A button on each row puts its path in your message, without touching the clipboard. A context menu to copy paths, insert them as `@path` or open the file with the system's default app. |
40
40
  | **Plans** | The documents written by that conversation, rendered: the plans from plan mode, and also the `.md` files the agent created inside the project or in the session's temp folder. Only those named by the conversation you're viewing. |
41
41
  | **Shared memory** | What agents learn about a project, in `.agents/memory/`, read and written by all four CLIs ([below](#shared-memory)). |
42
- | **Local copy** | Optional. The history of all four CLIs in a folder of yours, in the app's own format, so you don't lose it if a CLI changes its format, deletes it, or you uninstall the CLI ([below](#local-copy-optional)). |
43
- | **Nine languages** | The interface in English, Español, 简体中文, 日本語, Português (Brasil), Русский, 한국어, Français and Deutsch. It follows the browser's language and can be changed from the header, without reloading. |
42
+ | **Backup** | Optional. The history of all four CLIs in a folder of yours, in the app's own format, so you don't lose it if a CLI changes its format, deletes it, or you uninstall the CLI ([below](#backup-optional)). |
43
+ | **Nine languages** | The interface in English, Español, 简体中文, 日本語, Português (Brasil), Русский, 한국어, Français and Deutsch. It follows the browser's language and can be changed in Settings (⚙ in the header), without reloading. |
44
44
  | **Theme** | Light, dark, or the system's. |
45
45
 
46
46
  <p>
@@ -159,13 +159,13 @@ prints nothing, so the CLI's own status line stays as it is. Once set, **every**
159
159
  instead of naming the script in quotes: the CLI runs it with `cmd /c`, and no
160
160
  quote reaches `node` intact.
161
161
 
162
- ### Local copy (optional)
162
+ ### Backup (optional)
163
163
 
164
164
  Each conversation's history belongs to its CLI, in its format, and a CLI can
165
- change it, prune it or cease to exist. The local copy keeps the same as that
165
+ change it, prune it or cease to exist. The backup keeps the same as that
166
166
  history —messages, tool inputs and results, images and each project's memory—
167
167
  in a folder of yours, in files you can read without the app. **It's off by
168
- default.** The local copy button, in the header of the projects sidebar, opens a
168
+ default.** The backup button, in the header of the projects sidebar, opens a
169
169
  dialog: first **Measure** tells you how much space it would take per CLI,
170
170
  without writing anything, and then **Turn on** enables it. Once on, it copies
171
171
  everything the sidebar lists that isn't archived, and copies each session that
@@ -173,10 +173,10 @@ changes again a minute after it goes quiet. Archived sessions aren't copied, and
173
173
  archiving doesn't delete what was already copied: the app never deletes anything
174
174
  from that folder.
175
175
 
176
- Whatever the CLI no longer has stays in the sidebar, marked as a copy, and opens
177
- in Markdown; each project can be exported to Markdown. And with the copy on and
178
- more than one CLI, the sidebar filter offers **In conversations**: it searches
179
- the text of everything copied, from every CLI.
176
+ Whatever the CLI no longer has stays in the sidebar, marked as a backup, and
177
+ opens in Markdown; each project can be exported to Markdown. And with the backup
178
+ on and more than one CLI, the sidebar filter offers **In conversations**: it
179
+ searches the text of everything copied, from every CLI.
180
180
 
181
181
  <img src="https://raw.githubusercontent.com/cvelasquez/agent-workbench/main/assets/captura-buscador.png" width="40%" alt="Search in conversations: a match in one session of each CLI, with its snippet">
182
182
 
@@ -305,9 +305,10 @@ waiting for you, with the screen off too; tapping a notification opens that
305
305
  tab. When the Wi‑Fi comes back, it reconnects on its own.
306
306
 
307
307
  It needs remote access on and the computer's SSH server running (steps 1 and 2
308
- above). It isn't on Google Play yet: to install it now, build it from
309
- [`android/`](android/) as [`CONTRIBUTING.md`](CONTRIBUTING.md#the-android-app)
310
- explains.
308
+ above). Install it from
309
+ [Google Play](https://play.google.com/store/apps/details?id=io.github.cvelasquez.agentworkbench),
310
+ or build it yourself from [`android/`](android/) as
311
+ [`CONTRIBUTING.md`](CONTRIBUTING.md#the-android-app) explains.
311
312
 
312
313
  **Pairing, once:**
313
314
 
@@ -402,8 +403,8 @@ you aren't logged in, you log in inside the CLI's terminal and the app doesn't
402
403
  even notice.
403
404
 
404
405
  **From each CLI it reads only this, and it writes nothing in the CLI's
405
- folder.** The local copy importers, when you run them, also read what
406
- [their section](#local-copy-optional) says.
406
+ folder.** The backup importers, when you run them, also read what
407
+ [their section](#backup-optional) says.
407
408
 
408
409
  | CLI | Reads | Never opens |
409
410
  |---|---|---|
@@ -442,7 +443,7 @@ and stops being valid when you close the app.
442
443
 
443
444
  - **In its own configuration directory:** the open tabs, the index cache, the
444
445
  notes, the archived sessions, the Antigravity status line script and, if you
445
- turn it on, the local copy (or in the folder you choose). With remote access
446
+ turn it on, the backup (or in the folder you choose). With remote access
446
447
  on, also the list of paired devices: a name, two dates and a hash of each
447
448
  one's credential — never the credential itself.
448
449
  - **In the temp folder:** the images you paste; each Antigravity tab's log,
package/dist/server.js CHANGED
@@ -680,6 +680,38 @@ function cleanDeviceLabel(label) {
680
680
  return label.replace(/\s+/g, " ").trim().slice(0, REMOTE_DEVICE_LABEL_MAX_CHARS);
681
681
  }
682
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
+
683
715
  // packages/shared/src/protocol.ts
684
716
  var PROTOCOL_VERSION = 8;
685
717
  var WS_PATH = "/ws";
@@ -1070,6 +1102,10 @@ function parseClientMessage(raw) {
1070
1102
  const draft = parseComposerDraft(record["draft"]);
1071
1103
  return terminalId === null || draft === null ? null : { type: "composer.draft", terminalId, draft };
1072
1104
  }
1105
+ case "settings.update": {
1106
+ const change = parseAppSettingsChange(record);
1107
+ return change === null ? null : { type: "settings.update", ...change };
1108
+ }
1073
1109
  default:
1074
1110
  return null;
1075
1111
  }
@@ -10114,28 +10150,48 @@ var AgentRegistry = class {
10114
10150
  agents = /* @__PURE__ */ new Map();
10115
10151
  /** Las suscripciones a cambios de configuracion que siguen abiertas, para soltarlas al apagar. */
10116
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;
10117
10160
  /** El orden del array es el orden de preferencia para `defaultAgent`. */
10118
- constructor(adapters) {
10161
+ constructor(adapters, disabled = []) {
10119
10162
  for (const adapter of adapters) {
10120
10163
  if (this.agents.has(adapter.id)) {
10121
10164
  throw new Error(`Adapter registered twice: ${adapter.id}`);
10122
10165
  }
10123
10166
  this.agents.set(adapter.id, { adapter, location: null });
10124
10167
  }
10168
+ this.disabled = new Set([...disabled].filter((id) => this.agents.has(id)));
10125
10169
  }
10126
10170
  /**
10127
10171
  * Localiza todos a la vez. Se llama una vez al arrancar.
10128
10172
  *
10129
10173
  * En paralelo porque cada uno puede correr `--version`, y eso son segundos
10130
- * que el arranque no tiene por que sumar.
10174
+ * que el arranque no tiene por que sumar. Una apagada no se busca.
10131
10175
  */
10132
10176
  async locateAll() {
10133
10177
  await Promise.all(
10134
10178
  [...this.agents.values()].map(async (entry) => {
10135
- entry.location = await entry.adapter.locate();
10179
+ entry.location = this.disabled.has(entry.adapter.id) ? null : await entry.adapter.locate();
10136
10180
  })
10137
10181
  );
10138
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
+ }
10139
10195
  /**
10140
10196
  * Lo que cada CLI encontrada prepara en las carpetas de la app
10141
10197
  * (`AgentAdapter.prepare`). Se llama una vez al arrancar, despues de
@@ -10287,13 +10343,13 @@ function resolveAgentForOpen(input) {
10287
10343
  }
10288
10344
  return fromTabs ?? input.defaultAgent;
10289
10345
  }
10290
- function createAgentRegistry() {
10346
+ function createAgentRegistry(disabled = []) {
10291
10347
  const registry = new AgentRegistry([
10292
10348
  createClaudeCodeAdapter(),
10293
10349
  createCodexAdapter(),
10294
10350
  createOpenCodeAdapter({ serveEnv: () => registry.composedEnvironment(process.env) }),
10295
10351
  createAntigravityAdapter()
10296
- ]);
10352
+ ], disabled);
10297
10353
  return registry;
10298
10354
  }
10299
10355
 
@@ -11205,6 +11261,7 @@ var OPENS_ON_HOST = /* @__PURE__ */ new Set([
11205
11261
  ]);
11206
11262
  function remoteRefusal(type, remoteClient) {
11207
11263
  if (!remoteClient) return null;
11264
+ if (type === "settings.update") return serverText("settingsHostOnly");
11208
11265
  if (HOST_ONLY.has(type)) return serverText("remoteHostOnly");
11209
11266
  if (OPENS_ON_HOST.has(type)) return serverText("remoteOpensOnHost");
11210
11267
  return null;
@@ -14463,7 +14520,10 @@ function vaultOnlySessions(natives, vault, listState, ready) {
14463
14520
  for (const copy of vault) {
14464
14521
  const pair = vaultPairKey(copy.agent, copy.sessionId);
14465
14522
  if (native.has(pair) || seen.has(pair)) continue;
14466
- 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
+ }
14467
14527
  seen.add(pair);
14468
14528
  const hasCwd = copy.cwd.length > 0;
14469
14529
  result.push({
@@ -14845,7 +14905,7 @@ var SessionIndex = class extends EventEmitter4 {
14845
14905
  }
14846
14906
  this.scanning = true;
14847
14907
  try {
14848
- const sources = this.agents.all().map(({ adapter }) => ({
14908
+ const sources = (this.agents.historySources?.() ?? this.agents.all()).map(({ adapter }) => ({
14849
14909
  agent: adapter.id,
14850
14910
  history: adapter.history
14851
14911
  }));
@@ -14865,6 +14925,9 @@ var SessionIndex = class extends EventEmitter4 {
14865
14925
  const outcome = outcomes[position] ?? { items: null, threw: true };
14866
14926
  states.set(source.agent, await nativeListState(source.history, outcome));
14867
14927
  }
14928
+ for (const { adapter } of this.agents.all()) {
14929
+ if (!states.has(adapter.id)) states.set(adapter.id, "off");
14930
+ }
14868
14931
  this.listState = states;
14869
14932
  if (lists.every((items) => items === null)) {
14870
14933
  this.sessions.clear();
@@ -15050,7 +15113,9 @@ function defaultAppSettings() {
15050
15113
  return {
15051
15114
  version: SETTINGS_VERSION,
15052
15115
  vault: { enabled: false, dir: null, toolResultMaxChars: VAULT_TOOL_RESULT_DEFAULT_CHARS },
15053
- remote: { enabled: false, port: REMOTE_DEFAULT_PORT }
15116
+ remote: { enabled: false, port: REMOTE_DEFAULT_PORT },
15117
+ console: { shell: "auto" },
15118
+ agents: { disabled: [] }
15054
15119
  };
15055
15120
  }
15056
15121
  function isAbsoluteDir(dir, platform) {
@@ -15066,6 +15131,12 @@ function parseAppSettings(value, platform, warn = () => void 0) {
15066
15131
  const record = asRecord(value);
15067
15132
  if (record === null || record["version"] !== SETTINGS_VERSION) return null;
15068
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;
15069
15140
  const remote = asRecord(record["remote"]);
15070
15141
  if (remote !== null) {
15071
15142
  settings2.remote.enabled = asBoolean(remote["enabled"]) ?? false;
@@ -15078,7 +15149,7 @@ function parseAppSettings(value, platform, warn = () => void 0) {
15078
15149
  if (typeof dir === "string" && isAbsoluteDir(dir, platform)) {
15079
15150
  settings2.vault.dir = dir;
15080
15151
  } else if (dir !== null && dir !== void 0) {
15081
- 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`);
15082
15153
  }
15083
15154
  settings2.vault.toolResultMaxChars = clampToolResultMaxChars(vault["toolResultMaxChars"]);
15084
15155
  return settings2;
@@ -15087,7 +15158,9 @@ function frozen(settings2) {
15087
15158
  return Object.freeze({
15088
15159
  ...settings2,
15089
15160
  vault: Object.freeze({ ...settings2.vault }),
15090
- 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]) })
15091
15164
  });
15092
15165
  }
15093
15166
  var SettingsStore = class {
@@ -15167,19 +15240,23 @@ var SettingsStore = class {
15167
15240
  const next = {
15168
15241
  version: SETTINGS_VERSION,
15169
15242
  vault: { ...this.current.vault },
15170
- remote: { ...this.current.remote }
15243
+ remote: { ...this.current.remote },
15244
+ console: { ...this.current.console },
15245
+ agents: { disabled: [...this.current.agents.disabled] }
15171
15246
  };
15172
15247
  const vault = patch.vault ?? {};
15173
15248
  if (vault.enabled !== void 0) next.vault.enabled = vault.enabled;
15174
15249
  if (vault.dir !== void 0) {
15175
15250
  if (vault.dir !== null && !isAbsoluteDir(vault.dir, this.platform)) {
15176
- 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)}`);
15177
15252
  }
15178
15253
  next.vault.dir = vault.dir;
15179
15254
  }
15180
15255
  if (vault.toolResultMaxChars !== void 0) {
15181
15256
  next.vault.toolResultMaxChars = clampToolResultMaxChars(vault.toolResultMaxChars);
15182
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)];
15183
15260
  const remote = patch.remote ?? {};
15184
15261
  if (remote.enabled !== void 0) next.remote.enabled = remote.enabled;
15185
15262
  if (remote.port !== void 0) {
@@ -15739,39 +15816,131 @@ async function isExecutable(candidate) {
15739
15816
  return false;
15740
15817
  }
15741
15818
  }
15742
- function windowsPowerShellPath() {
15743
- 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"];
15744
15827
  if (systemRoot === void 0 || systemRoot.length === 0) return null;
15745
- return path40.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
15828
+ return path40.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
15746
15829
  }
15747
- async function locateWindowsShell() {
15748
- const pwsh = await findInPath("pwsh");
15749
- if (pwsh !== null) return { file: pwsh, args: ["-NoLogo"], name: "PowerShell" };
15750
- const fromPath = await findInPath("powershell");
15751
- if (fromPath !== null) return { file: fromPath, args: ["-NoLogo"], name: "PowerShell" };
15752
- const systemCopy = windowsPowerShellPath();
15753
- if (systemCopy !== null && await isExecutable(systemCopy)) {
15754
- 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
+ }
15755
15840
  }
15756
- const comSpec = process.env["ComSpec"];
15757
- if (comSpec !== void 0 && comSpec.length > 0 && await isExecutable(comSpec)) {
15758
- 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"));
15759
15843
  }
15760
- return null;
15844
+ const systemRoot = (env["SystemRoot"] ?? env["windir"] ?? "").toLowerCase();
15845
+ return [...new Set(candidates)].filter((candidate) => systemRoot.length === 0 || !candidate.toLowerCase().startsWith(systemRoot));
15761
15846
  }
15762
- async function locatePosixShell() {
15763
- const preferred = process.env["SHELL"];
15764
- if (preferred !== void 0 && preferred.length > 0 && await isExecutable(preferred)) {
15765
- 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
+ }
15766
15866
  }
15767
- if (await isExecutable("/bin/bash")) return { file: "/bin/bash", args: [], name: "bash" };
15768
- if (await isExecutable("/bin/sh")) return { file: "/bin/sh", args: [], name: "sh" };
15769
- return null;
15867
+ return found;
15770
15868
  }
15771
- async function locateShell() {
15772
- return process.platform === "win32" ? locateWindowsShell() : locatePosixShell();
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;
15881
+ }
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;
15773
15892
  }
15774
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
+
15775
15944
  // packages/server/src/session-watcher.ts
15776
15945
  import { stat as stat21 } from "node:fs/promises";
15777
15946
  import chokidar3 from "chokidar";
@@ -15849,7 +16018,7 @@ function watchSessions(index, hub, agents, options = {}) {
15849
16018
  probe.unref();
15850
16019
  probes.add(probe);
15851
16020
  };
15852
- for (const { adapter } of agents.all()) {
16021
+ for (const { adapter } of agents.historySources?.() ?? agents.all()) {
15853
16022
  for (const root of adapter.history.roots()) {
15854
16023
  if (root.watch !== void 0) {
15855
16024
  subscribeRoot(adapter.id, root, root.watch);
@@ -15881,7 +16050,7 @@ function watchSessions(index, hub, agents, options = {}) {
15881
16050
 
15882
16051
  // packages/server/src/terminal-registry.ts
15883
16052
  import { randomUUID as randomUUID5 } from "node:crypto";
15884
- import { EventEmitter as EventEmitter5 } from "node:events";
16053
+ import { EventEmitter as EventEmitter6 } from "node:events";
15885
16054
  import { stat as stat22 } from "node:fs/promises";
15886
16055
 
15887
16056
  // packages/server/src/launch-hook-slot.ts
@@ -16314,7 +16483,7 @@ var MAX_TERMINALS = 24;
16314
16483
  var RESTART_EXIT_TIMEOUT_MS = 1e4;
16315
16484
  var DEFAULT_COLS = 80;
16316
16485
  var DEFAULT_ROWS = 24;
16317
- var TerminalRegistry = class extends EventEmitter5 {
16486
+ var TerminalRegistry = class extends EventEmitter6 {
16318
16487
  constructor(agents, shell, store, sessionAgentOf = () => null) {
16319
16488
  super();
16320
16489
  this.agents = agents;
@@ -16338,6 +16507,10 @@ var TerminalRegistry = class extends EventEmitter5 {
16338
16507
  unavailableTabs = [];
16339
16508
  /** Pestanas guardadas de una CLI que esta build no conoce. Idem, crudas. */
16340
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
+ }
16341
16514
  list() {
16342
16515
  return this.order.map((id) => this.terminals.get(id)?.descriptor).filter((descriptor) => descriptor !== void 0);
16343
16516
  }
@@ -16427,7 +16600,9 @@ var TerminalRegistry = class extends EventEmitter5 {
16427
16600
  cwd: options.cwd,
16428
16601
  // Provisional: `spawn` lo confirma con lo que diga el adaptador.
16429
16602
  sessionId: kind === "agent" ? options.resumeSessionId ?? randomUUID5() : "",
16430
- 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 ?? "" : ""),
16431
16606
  resumed,
16432
16607
  createdAt: Date.now(),
16433
16608
  // Lo pone `spawn`. Una entrada que no llega a tener proceso no queda en
@@ -19386,7 +19561,7 @@ function attachTerminalSocket(options) {
19386
19561
  httpServer,
19387
19562
  port,
19388
19563
  token,
19389
- shell,
19564
+ appSettings,
19390
19565
  registry,
19391
19566
  index,
19392
19567
  archived,
@@ -19597,6 +19772,8 @@ function attachTerminalSocket(options) {
19597
19772
  const stopRemoteStatus = remote.onChange(
19598
19773
  () => broadcastToHost({ type: "remote.status", status: remote.status() })
19599
19774
  );
19775
+ const onAppSettingsStatus = (settings2) => broadcast({ type: "settings.status", settings: settings2 });
19776
+ appSettings.on("status", onAppSettingsStatus);
19600
19777
  const stopRemoteRevoked = remote.onRevoked((deviceId) => {
19601
19778
  for (const [socket, id] of remoteSockets) {
19602
19779
  if (deviceId === null || id === deviceId) socket.close(REMOTE_REVOKED_CLOSE_CODE, "revoked");
@@ -19709,7 +19886,7 @@ function attachTerminalSocket(options) {
19709
19886
  defaultAgent: agents.defaultAgent(),
19710
19887
  platform: process.platform,
19711
19888
  defaultCwd,
19712
- shellName: shell?.name ?? null,
19889
+ shellName: appSettings.currentShell()?.name ?? null,
19713
19890
  remoteClient: deviceId !== null
19714
19891
  });
19715
19892
  send(socket, terminalListMessage());
@@ -19722,6 +19899,7 @@ function attachTerminalSocket(options) {
19722
19899
  send(socket, { type: "composer.drafts", drafts: draftEntriesOf(registry.list()) });
19723
19900
  send(socket, { type: "vault.status", status: vault.status() });
19724
19901
  if (deviceId === null) send(socket, { type: "remote.status", status: remote.status() });
19902
+ send(socket, { type: "settings.status", settings: appSettings.status() });
19725
19903
  socket.on("message", (raw) => {
19726
19904
  const text = Array.isArray(raw) ? Buffer.concat(raw).toString("utf8") : Buffer.from(raw).toString("utf8");
19727
19905
  const message = parseClientMessage(text);
@@ -20775,6 +20953,24 @@ function attachTerminalSocket(options) {
20775
20953
  case "search.cancel":
20776
20954
  globalSearch.cancel(message.searchId);
20777
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
+ }
20778
20974
  /*
20779
20975
  El acceso remoto (hito 37). Hasta aca solo llega una ventana del
20780
20976
  anfitrion: a un equipo remoto lo freno `remoteRefusal`. El estado
@@ -20875,6 +21071,7 @@ function attachTerminalSocket(options) {
20875
21071
  memory.off("status", onMemoryStatus);
20876
21072
  stopAgentChanges();
20877
21073
  stopVaultStatus();
21074
+ appSettings.off("status", onAppSettingsStatus);
20878
21075
  stopRemoteStatus();
20879
21076
  stopRemoteRevoked();
20880
21077
  for (const client of wss.clients) client.terminate();
@@ -21261,17 +21458,18 @@ ${line}`);
21261
21458
  async function main() {
21262
21459
  const token = createSessionToken();
21263
21460
  const defaultCwd = resolveDefaultCwd();
21264
- const agents = createAgentRegistry();
21265
- const [, shell] = await Promise.all([agents.locateAll(), locateShell()]);
21266
21461
  const movedFrom = await migrateLegacyConfigDir();
21267
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);
21268
21468
  await agents.prepareAll();
21269
21469
  ensurePtyHelperExecutable();
21270
21470
  const store = new WorkspaceStore();
21271
21471
  const archived = new ArchivedSessions();
21272
21472
  await archived.load();
21273
- const settings2 = new SettingsStore();
21274
- await settings2.load();
21275
21473
  const catalog = new VaultCatalog();
21276
21474
  await catalog.load(settings2.get().vault.dir ?? defaultVaultDir());
21277
21475
  const index = new SessionIndex(agents, archived, catalog);
@@ -21283,6 +21481,12 @@ async function main() {
21283
21481
  store,
21284
21482
  (sessionId) => index.agentOf(sessionId)
21285
21483
  );
21484
+ const appSettings = new AppSettingsService({
21485
+ settings: settings2,
21486
+ agents,
21487
+ shells,
21488
+ onShellChange: (next) => registry.setShell(next)
21489
+ });
21286
21490
  const notes = new NotesStore();
21287
21491
  await notes.load();
21288
21492
  const drafts = new DraftStore();
@@ -21325,7 +21529,7 @@ async function main() {
21325
21529
  httpServer,
21326
21530
  port,
21327
21531
  token,
21328
- shell,
21532
+ appSettings,
21329
21533
  registry,
21330
21534
  index,
21331
21535
  archived,