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 +17 -16
- package/dist/server.js +251 -47
- package/dist/web/assets/de-D4f5KK7F.js +1 -0
- package/dist/web/assets/es-BPdfRlPL.js +1 -0
- package/dist/web/assets/fr-Cv2VZ3uf.js +1 -0
- package/dist/web/assets/{highlight-nRm-DNTr.js → highlight-DqX1MSlE.js} +1 -1
- package/dist/web/assets/index-B09L7LWU.css +32 -0
- package/dist/web/assets/index-CRZuxaEg.js +139 -0
- package/dist/web/assets/ja-X1nVuf-y.js +1 -0
- package/dist/web/assets/ko-c__ksx35.js +1 -0
- package/dist/web/assets/pt-BR-Bq9Rmy51.js +1 -0
- package/dist/web/assets/ru-4tlHIIyY.js +1 -0
- package/dist/web/assets/zh-CN-B_6-wR0g.js +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/de-CXvJ7uwt.js +0 -1
- package/dist/web/assets/es-BrWQ_4kO.js +0 -1
- package/dist/web/assets/fr-Cv_qM9E8.js +0 -1
- package/dist/web/assets/index-BFpUqUWT.js +0 -139
- package/dist/web/assets/index-BnrUWT0F.css +0 -32
- package/dist/web/assets/ja-h9u8sCMK.js +0 -1
- package/dist/web/assets/ko-m0EzMLN6.js +0 -1
- package/dist/web/assets/pt-BR-BAHKndFr.js +0 -1
- package/dist/web/assets/ru-D1S49VN_.js +0 -1
- package/dist/web/assets/zh-CN-CH6r3KGh.js +0 -1
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
|
|
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
|
-
| **
|
|
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
|
|
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
|
-
###
|
|
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
|
|
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
|
|
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
|
|
177
|
-
in Markdown; each project can be exported to Markdown. And with the
|
|
178
|
-
more than one CLI, the sidebar filter offers **In conversations**: it
|
|
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).
|
|
309
|
-
[
|
|
310
|
-
|
|
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
|
|
406
|
-
[their section](#
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
|
|
15743
|
-
|
|
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
|
-
|
|
15748
|
-
const
|
|
15749
|
-
if (
|
|
15750
|
-
|
|
15751
|
-
|
|
15752
|
-
|
|
15753
|
-
|
|
15754
|
-
|
|
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
|
|
15757
|
-
|
|
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
|
-
|
|
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
|
|
15763
|
-
const
|
|
15764
|
-
|
|
15765
|
-
|
|
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
|
-
|
|
15768
|
-
if (await isExecutable("/bin/sh")) return { file: "/bin/sh", args: [], name: "sh" };
|
|
15769
|
-
return null;
|
|
15867
|
+
return found;
|
|
15770
15868
|
}
|
|
15771
|
-
async function
|
|
15772
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
21532
|
+
appSettings,
|
|
21329
21533
|
registry,
|
|
21330
21534
|
index,
|
|
21331
21535
|
archived,
|