@robota-sdk/agent-command 3.0.0-beta.82 → 3.0.0-beta.83
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/CHANGELOG.md +1044 -0
- package/dist/node/index.cjs +76 -70
- package/dist/node/index.d.cts +100 -8
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +100 -8
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +76 -70
- package/dist/node/index.js.map +1 -1
- package/package.json +14 -9
- package/src/command-module-utils.test.ts +37 -0
- package/src/command-module-utils.ts +2 -0
- package/src/default/__tests__/default-command-modules.test.ts +50 -11
- package/src/default/__tests__/model-exposure.test.ts +16 -3
- package/src/default/__tests__/org-policy-forwarding.test.ts +10 -2
- package/src/default/default-command-modules.ts +7 -8
- package/src/devices/__tests__/devices-command-module.test.ts +99 -12
- package/src/devices/devices-command-module.ts +108 -24
- package/src/devices/devices-command-port.ts +62 -1
- package/src/devices/index.ts +3 -0
- package/src/doctor/doctor-node-deps.ts +6 -7
- package/src/editor/editor-command-module.ts +5 -0
- package/src/events/__tests__/events-command.test.ts +73 -0
- package/src/events/events-command-module.ts +49 -0
- package/src/events/events-command.ts +69 -0
- package/src/events/index.ts +2 -0
- package/src/git/__tests__/git-command-module.test.ts +10 -2
- package/src/handoff/__tests__/handoff-command-module.test.ts +2 -1
- package/src/handoff/handoff-command-module.ts +1 -1
- package/src/handoff/handoff-command.ts +2 -2
- package/src/help/__tests__/help-command-module.test.ts +3 -0
- package/src/help/__tests__/help-command.test.ts +3 -0
- package/src/index.ts +8 -0
- package/src/keybindings/keybindings-command-module.ts +17 -3
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +41 -0
- package/src/mcp-activation/mcp-activation-command.ts +8 -3
- package/src/peers/__tests__/peers-command-module.test.ts +24 -0
- package/src/peers/__tests__/peers-command.test.ts +78 -0
- package/src/peers/peers-command-module.ts +1 -1
- package/src/peers/peers-command.ts +33 -4
- package/src/rewind/__tests__/rewind-command-module.test.ts +31 -1
- package/src/rewind/rewind-command.ts +23 -2
- package/src/schedule/__tests__/schedule-redos.test.ts +33 -0
- package/src/schedule/loop-command.ts +3 -1
- package/src/shell/shell-command-module.ts +5 -0
- package/src/terminal-client/__tests__/terminal-client-commands.test.ts +216 -0
- package/src/terminal-client/index.ts +5 -0
- package/src/terminal-client/terminal-client-commands.ts +109 -0
- package/src/theme/__tests__/theme-command.test.ts +1 -1
- package/src/theme/theme-command-module.ts +12 -2
- package/src/user-local/__tests__/user-local-command.test.ts +2 -2
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3189 — the commands a terminal attached to a workspace daemon runs in its own process.
|
|
3
|
+
*
|
|
4
|
+
* The attached terminal routes by this set, not by the daemon's catalog, so the set must name exactly
|
|
5
|
+
* the commands the default assembly marks `runner: 'client'` — a command in one list and not the
|
|
6
|
+
* other would either run on the daemon or never run at all. And each client command must answer as
|
|
7
|
+
* the in-process command does, because it is the same command running somewhere else.
|
|
8
|
+
*/
|
|
9
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { afterAll, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
|
|
15
|
+
import { createDefaultCommandModules } from '../../default/index.js';
|
|
16
|
+
import { createEditorCommandModule } from '../../editor/index.js';
|
|
17
|
+
import { createKeybindingsCommandModule } from '../../keybindings/index.js';
|
|
18
|
+
import { createShellCommandModule } from '../../shell/index.js';
|
|
19
|
+
import { createThemeCommandModule } from '../../theme/index.js';
|
|
20
|
+
import { createTerminalClientCommands } from '../terminal-client-commands.js';
|
|
21
|
+
|
|
22
|
+
import type { ITerminalClientCommand } from '../terminal-client-commands.js';
|
|
23
|
+
import type { IProviderDefinition } from '@robota-sdk/agent-core';
|
|
24
|
+
import type {
|
|
25
|
+
ICommandHostContext,
|
|
26
|
+
ICommandHostTerminalHandoff,
|
|
27
|
+
ICommandHostWorkspace,
|
|
28
|
+
ICommandModule,
|
|
29
|
+
IProviderCommandSettingsAdapter,
|
|
30
|
+
TProviderSettingsDocument,
|
|
31
|
+
} from '@robota-sdk/agent-framework';
|
|
32
|
+
import type {
|
|
33
|
+
IThemeCatalogueEntry,
|
|
34
|
+
IThemeCataloguePort,
|
|
35
|
+
} from '@robota-sdk/agent-interface-command';
|
|
36
|
+
|
|
37
|
+
/** User-local storage lives in a private per-run directory, never a fixed name under /tmp. */
|
|
38
|
+
const USER_LOCAL_STORAGE_ROOT = mkdtempSync(join(tmpdir(), 'robota-test-'));
|
|
39
|
+
afterAll(() => rmSync(USER_LOCAL_STORAGE_ROOT, { recursive: true, force: true }));
|
|
40
|
+
|
|
41
|
+
const providerDefinitions: readonly IProviderDefinition[] = [];
|
|
42
|
+
const providerSettingsAdapter: IProviderCommandSettingsAdapter = {
|
|
43
|
+
readMergedSettings: () => ({}) as TProviderSettingsDocument,
|
|
44
|
+
readTargetSettings: () => ({}) as TProviderSettingsDocument,
|
|
45
|
+
writeTargetSettings: () => undefined,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const THEMES: IThemeCatalogueEntry[] = [
|
|
49
|
+
{ id: 'dark', name: 'Dark', appearance: 'dark', source: 'built-in' },
|
|
50
|
+
{ id: 'light', name: 'Light', appearance: 'light', source: 'built-in' },
|
|
51
|
+
];
|
|
52
|
+
const themeCatalogue: IThemeCataloguePort = {
|
|
53
|
+
listThemes: () => THEMES,
|
|
54
|
+
getTheme: (id) => THEMES.find((theme) => theme.id === id),
|
|
55
|
+
getAppearance: () => ({
|
|
56
|
+
settings: { theme: 'dark', syntaxHighlighting: true, reducedMotion: false },
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function allPortsGiven(): readonly ITerminalClientCommand[] {
|
|
61
|
+
return createTerminalClientCommands({
|
|
62
|
+
keybindingsFile: { ensureFile: async () => '/tmp/keybindings.json' },
|
|
63
|
+
themeCatalogue,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function find(commands: readonly ITerminalClientCommand[], name: string): ITerminalClientCommand {
|
|
68
|
+
const command = commands.find((candidate) => candidate.name === name);
|
|
69
|
+
if (!command) throw new Error(`no terminal client command "${name}"`);
|
|
70
|
+
return command;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A terminal the command cannot take over: a host with no interactive TTY. */
|
|
74
|
+
function hostWithoutTerminal(): ICommandHostTerminalHandoff & ICommandHostWorkspace {
|
|
75
|
+
return {
|
|
76
|
+
canHandoffTerminal: () => false,
|
|
77
|
+
runWithTerminal: vi.fn(async () => {
|
|
78
|
+
throw new Error('a refused handoff must not be attempted');
|
|
79
|
+
}),
|
|
80
|
+
getCwd: () => '/tmp',
|
|
81
|
+
getCommandInvocationSource: () => 'user',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** What the in-process command answers for the same host and arguments. */
|
|
86
|
+
async function inProcess(module: ICommandModule, host: unknown, args: string) {
|
|
87
|
+
const command = module.systemCommands?.[0];
|
|
88
|
+
if (!command) throw new Error(`module ${module.name} has no command`);
|
|
89
|
+
return command.execute(host as ICommandHostContext, args);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe('createTerminalClientCommands (#3189)', () => {
|
|
93
|
+
it('names exactly the commands the default assembly marks as client-run', () => {
|
|
94
|
+
const { modules } = createDefaultCommandModules({
|
|
95
|
+
cwd: '/tmp',
|
|
96
|
+
userLocalStorageRoot: USER_LOCAL_STORAGE_ROOT,
|
|
97
|
+
providerDefinitions,
|
|
98
|
+
providerSettingsAdapter,
|
|
99
|
+
});
|
|
100
|
+
const clientRun = modules
|
|
101
|
+
.flatMap((module) => module.systemCommands ?? [])
|
|
102
|
+
.filter((command) => command.runner === 'client')
|
|
103
|
+
.map((command) => command.name)
|
|
104
|
+
.sort();
|
|
105
|
+
|
|
106
|
+
expect(clientRun.length).toBeGreaterThan(0);
|
|
107
|
+
expect(
|
|
108
|
+
allPortsGiven()
|
|
109
|
+
.map((command) => command.name)
|
|
110
|
+
.sort(),
|
|
111
|
+
).toEqual(clientRun);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('leaves out a client command whose module the selection disables, as the default assembly does', () => {
|
|
115
|
+
const selection = { disabledCommandModules: ['agent-command-shell'] };
|
|
116
|
+
const { modules } = createDefaultCommandModules({
|
|
117
|
+
cwd: '/tmp',
|
|
118
|
+
userLocalStorageRoot: USER_LOCAL_STORAGE_ROOT,
|
|
119
|
+
providerDefinitions,
|
|
120
|
+
providerSettingsAdapter,
|
|
121
|
+
...selection,
|
|
122
|
+
});
|
|
123
|
+
const clientRun = modules
|
|
124
|
+
.flatMap((module) => module.systemCommands ?? [])
|
|
125
|
+
.filter((command) => command.runner === 'client')
|
|
126
|
+
.map((command) => command.name)
|
|
127
|
+
.sort();
|
|
128
|
+
|
|
129
|
+
const names = createTerminalClientCommands({ themeCatalogue, ...selection })
|
|
130
|
+
.map((command) => command.name)
|
|
131
|
+
.sort();
|
|
132
|
+
|
|
133
|
+
expect(names).not.toContain('shell');
|
|
134
|
+
expect(names).toEqual(clientRun);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('keeps only the enabled client modules, and a disabled name wins over an enabled one', () => {
|
|
138
|
+
const names = createTerminalClientCommands({
|
|
139
|
+
enabledCommandModules: ['agent-command-theme', 'agent-command-editor', 'agent-command-help'],
|
|
140
|
+
disabledCommandModules: ['agent-command-editor'],
|
|
141
|
+
}).map((command) => command.name);
|
|
142
|
+
|
|
143
|
+
expect(names).toEqual(['theme']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('with no selection, offers all four client commands', () => {
|
|
147
|
+
expect(
|
|
148
|
+
createTerminalClientCommands({})
|
|
149
|
+
.map((command) => command.name)
|
|
150
|
+
.sort(),
|
|
151
|
+
).toEqual(['editor', 'keybindings', 'shell', 'theme']);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('/shell refuses a host that cannot hand off the terminal exactly as the in-process command does', async () => {
|
|
155
|
+
const host = hostWithoutTerminal();
|
|
156
|
+
|
|
157
|
+
const client = await find(allPortsGiven(), 'shell').execute(host, 'ls');
|
|
158
|
+
|
|
159
|
+
expect(client).toEqual(await inProcess(createShellCommandModule(), host, 'ls'));
|
|
160
|
+
expect(client.success).toBe(false);
|
|
161
|
+
expect(host.runWithTerminal).not.toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('/editor and /keybindings refuse a host without a terminal as their in-process commands do', async () => {
|
|
165
|
+
const keybindingsFile = { ensureFile: vi.fn(async () => '/tmp/keybindings.json') };
|
|
166
|
+
const commands = createTerminalClientCommands({ keybindingsFile, themeCatalogue });
|
|
167
|
+
|
|
168
|
+
const editorHost = hostWithoutTerminal();
|
|
169
|
+
const editor = await find(commands, 'editor').execute(editorHost, 'draft');
|
|
170
|
+
expect(editor).toEqual(await inProcess(createEditorCommandModule(), editorHost, 'draft'));
|
|
171
|
+
expect(editor.success).toBe(false);
|
|
172
|
+
|
|
173
|
+
const keybindingsHost = hostWithoutTerminal();
|
|
174
|
+
const keybindings = await find(commands, 'keybindings').execute(keybindingsHost, '');
|
|
175
|
+
expect(keybindings).toEqual(
|
|
176
|
+
await inProcess(createKeybindingsCommandModule(keybindingsFile), keybindingsHost, ''),
|
|
177
|
+
);
|
|
178
|
+
expect(keybindings.success).toBe(false);
|
|
179
|
+
expect(keybindingsFile.ensureFile).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('/theme with no arguments asks the terminal to open its theme picker', async () => {
|
|
183
|
+
const result = await find(allPortsGiven(), 'theme').execute(hostWithoutTerminal(), '');
|
|
184
|
+
|
|
185
|
+
expect(result).toMatchObject({ success: true, uiIntents: [{ type: 'show-theme-picker' }] });
|
|
186
|
+
expect(result.hostActions).toBeUndefined();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('/theme with a known id returns one appearance settings patch for the terminal to write', async () => {
|
|
190
|
+
const result = await find(allPortsGiven(), 'theme').execute(hostWithoutTerminal(), 'light');
|
|
191
|
+
|
|
192
|
+
expect(result.success).toBe(true);
|
|
193
|
+
expect(result.hostActions).toEqual([
|
|
194
|
+
{ type: 'appearance-settings-patch', patch: { theme: 'light' } },
|
|
195
|
+
]);
|
|
196
|
+
expect(result).toEqual(await inProcess(createThemeCommandModule(themeCatalogue), {}, 'light'));
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('without their ports, /keybindings and /theme still run on the terminal and answer as in-process', async () => {
|
|
200
|
+
const commands = createTerminalClientCommands({});
|
|
201
|
+
// The set does not shrink: a command missing from it would be sent to the daemon instead.
|
|
202
|
+
expect(commands.map((command) => command.name).sort()).toEqual(
|
|
203
|
+
allPortsGiven()
|
|
204
|
+
.map((command) => command.name)
|
|
205
|
+
.sort(),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const host = hostWithoutTerminal();
|
|
209
|
+
expect(await find(commands, 'keybindings').execute(host, '')).toEqual(
|
|
210
|
+
await inProcess(createKeybindingsCommandModule(undefined), host, ''),
|
|
211
|
+
);
|
|
212
|
+
expect(await find(commands, 'theme').execute(host, 'light')).toEqual(
|
|
213
|
+
await inProcess(createThemeCommandModule(undefined), host, 'light'),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3189: the commands that belong to the terminal the user sits at — the ones the default assembly
|
|
3
|
+
* marks `runner: 'client'`. A terminal attached to a workspace daemon runs these in its own process,
|
|
4
|
+
* on its own terminal and working directory, and sends nothing to the daemon.
|
|
5
|
+
*
|
|
6
|
+
* Each one is the in-process command's own execute function, so an attached terminal answers exactly
|
|
7
|
+
* as a standalone one does; only where it runs changes.
|
|
8
|
+
*/
|
|
9
|
+
import { selectCommandModules } from '@robota-sdk/agent-framework';
|
|
10
|
+
|
|
11
|
+
import { createEditorCommandModule } from '../editor/editor-command-module.js';
|
|
12
|
+
import { executeEditorCommand } from '../editor/editor-command.js';
|
|
13
|
+
import {
|
|
14
|
+
createKeybindingsCommandModule,
|
|
15
|
+
executeKeybindingsCommand,
|
|
16
|
+
} from '../keybindings/keybindings-command-module.js';
|
|
17
|
+
import { createShellCommandModule } from '../shell/shell-command-module.js';
|
|
18
|
+
import { executeShellCommand } from '../shell/shell-command.js';
|
|
19
|
+
import { createThemeCommandModule, executeThemeCommand } from '../theme/theme-command-module.js';
|
|
20
|
+
|
|
21
|
+
import type { IKeybindingsFilePort } from '../keybindings/keybindings-command-module.js';
|
|
22
|
+
import type {
|
|
23
|
+
ICommandHostTerminalHandoff,
|
|
24
|
+
ICommandHostWorkspace,
|
|
25
|
+
ICommandModule,
|
|
26
|
+
} from '@robota-sdk/agent-framework';
|
|
27
|
+
import type { ICommandResult, IThemeCataloguePort } from '@robota-sdk/agent-interface-command';
|
|
28
|
+
|
|
29
|
+
/** A command the terminal runs itself, with the terminal's own handoff and working directory. */
|
|
30
|
+
export interface ITerminalClientCommand {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
execute(
|
|
33
|
+
host: ICommandHostTerminalHandoff & ICommandHostWorkspace,
|
|
34
|
+
args: string,
|
|
35
|
+
): ICommandResult | Promise<ICommandResult>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The terminal's own capabilities — the same ones its in-process command modules are given. */
|
|
39
|
+
export interface ITerminalClientCommandOptions {
|
|
40
|
+
/** The shell `/shell` runs; absent → the platform shell, as for the in-process command. */
|
|
41
|
+
readonly shellExecutable?: string;
|
|
42
|
+
/** Product-owned prefix for `/editor`'s temporary directory; absent → the command's default. */
|
|
43
|
+
readonly editorTemporaryDirectoryPrefix?: string;
|
|
44
|
+
/** Absent → `/keybindings` answers where key bindings live, as the in-process command does. */
|
|
45
|
+
readonly keybindingsFile?: IKeybindingsFilePort;
|
|
46
|
+
/** Absent → `/theme` answers where themes live, as the in-process command does. */
|
|
47
|
+
readonly themeCatalogue?: IThemeCataloguePort;
|
|
48
|
+
/**
|
|
49
|
+
* The same module selection the default assembly is given (`agent-command-shell`, …): a module it
|
|
50
|
+
* leaves out is left out here too, so an attached terminal offers no command a standalone one
|
|
51
|
+
* would not. Absent → every client-run module.
|
|
52
|
+
*/
|
|
53
|
+
readonly enabledCommandModules?: readonly string[];
|
|
54
|
+
/** Applied after `enabledCommandModules`, as in the default assembly (deny > allow). */
|
|
55
|
+
readonly disabledCommandModules?: readonly string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type TClientExecute = ITerminalClientCommand['execute'];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Every client-run command the module selection keeps, whether or not its port is given. The set
|
|
62
|
+
* shrinks only with the selection, never with a missing port: the attached terminal routes by it, so
|
|
63
|
+
* a selected command left out would run on the daemon instead — and without its port the command
|
|
64
|
+
* still answers as its in-process twin does.
|
|
65
|
+
*/
|
|
66
|
+
export function createTerminalClientCommands(
|
|
67
|
+
options: ITerminalClientCommandOptions,
|
|
68
|
+
): readonly ITerminalClientCommand[] {
|
|
69
|
+
const {
|
|
70
|
+
shellExecutable,
|
|
71
|
+
editorTemporaryDirectoryPrefix,
|
|
72
|
+
keybindingsFile,
|
|
73
|
+
themeCatalogue,
|
|
74
|
+
enabledCommandModules,
|
|
75
|
+
disabledCommandModules,
|
|
76
|
+
} = options;
|
|
77
|
+
// Each client command is paired with its in-process module, so selection matches by the module's
|
|
78
|
+
// own name and the command keeps that module's command name.
|
|
79
|
+
const candidates: ReadonlyArray<readonly [ICommandModule, TClientExecute]> = [
|
|
80
|
+
[
|
|
81
|
+
createShellCommandModule(shellExecutable),
|
|
82
|
+
(host, args) => executeShellCommand(host, args, shellExecutable),
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
createEditorCommandModule(editorTemporaryDirectoryPrefix),
|
|
86
|
+
(host, args) => executeEditorCommand(host, args, editorTemporaryDirectoryPrefix),
|
|
87
|
+
],
|
|
88
|
+
[
|
|
89
|
+
createKeybindingsCommandModule(keybindingsFile),
|
|
90
|
+
(host) => executeKeybindingsCommand(keybindingsFile, host),
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
createThemeCommandModule(themeCatalogue),
|
|
94
|
+
(_host, args) => executeThemeCommand(themeCatalogue, args),
|
|
95
|
+
],
|
|
96
|
+
];
|
|
97
|
+
const selected = new Set(
|
|
98
|
+
selectCommandModules(
|
|
99
|
+
candidates.map(([module]) => module),
|
|
100
|
+
enabledCommandModules,
|
|
101
|
+
disabledCommandModules,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
return candidates
|
|
105
|
+
.filter(([module]) => selected.has(module))
|
|
106
|
+
.flatMap(([module, execute]) =>
|
|
107
|
+
(module.systemCommands ?? []).map((command) => ({ name: command.name, execute })),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -73,7 +73,7 @@ describe('/theme without a catalogue (SCREEN-2002 TC-09)', () => {
|
|
|
73
73
|
const result = executeThemeCommand(undefined, 'light');
|
|
74
74
|
|
|
75
75
|
expect(result.success).toBe(false);
|
|
76
|
-
expect(result.message).
|
|
76
|
+
expect(result.message).toMatch(/Run \/theme in the robota terminal/);
|
|
77
77
|
expect(result.hostActions).toBeUndefined();
|
|
78
78
|
});
|
|
79
79
|
});
|
|
@@ -16,7 +16,10 @@ const THEME_DESCRIPTION =
|
|
|
16
16
|
'List, preview or switch the terminal colour theme, and toggle syntax highlighting and motion';
|
|
17
17
|
const THEME_ARGUMENT_HINT = 'list | <theme-id> | syntax on|off | motion on|off';
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// A host without a terminal (the desktop app's sidecar) still answers, so the command is never
|
|
20
|
+
// "unknown" there — it says where themes live instead.
|
|
21
|
+
const THEME_UNAVAILABLE =
|
|
22
|
+
'Themes belong to the robota terminal, and this surface has none. Run /theme in the robota terminal.';
|
|
20
23
|
|
|
21
24
|
function formatToggle(value: boolean): string {
|
|
22
25
|
return value ? 'on' : 'off';
|
|
@@ -179,6 +182,9 @@ export function createThemeCommandEntry(): ICommand {
|
|
|
179
182
|
userInvocable: true,
|
|
180
183
|
// User-only: UI preference.
|
|
181
184
|
modelInvocable: false,
|
|
185
|
+
// Themes belong to the terminal the user sits at, so that terminal runs it, even when attached.
|
|
186
|
+
runner: 'client',
|
|
187
|
+
surfaces: ['terminal'],
|
|
182
188
|
};
|
|
183
189
|
}
|
|
184
190
|
|
|
@@ -190,7 +196,9 @@ export class ThemeCommandSource implements ICommandSource {
|
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
|
|
193
|
-
export function createThemeCommandModule(
|
|
199
|
+
export function createThemeCommandModule(
|
|
200
|
+
catalogue: IThemeCataloguePort | undefined,
|
|
201
|
+
): ICommandModule {
|
|
194
202
|
const entry = createThemeCommandEntry();
|
|
195
203
|
const command: ISystemCommand = {
|
|
196
204
|
name: entry.name,
|
|
@@ -200,6 +208,8 @@ export function createThemeCommandModule(catalogue: IThemeCataloguePort): IComma
|
|
|
200
208
|
requiresPermission: false,
|
|
201
209
|
userInvocable: true,
|
|
202
210
|
modelInvocable: false,
|
|
211
|
+
runner: entry.runner,
|
|
212
|
+
surfaces: entry.surfaces,
|
|
203
213
|
lifecycle: 'inline',
|
|
204
214
|
execute: (_context, args) => executeThemeCommand(catalogue, args),
|
|
205
215
|
};
|
|
@@ -21,8 +21,8 @@ afterEach(async () => {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
describe('user-local command', () => {
|
|
24
|
-
it('does not advertise a specific product from the neutral command entry', () => {
|
|
25
|
-
const module = createUserLocalCommandModule('
|
|
24
|
+
it('does not advertise a specific product from the neutral command entry', async () => {
|
|
25
|
+
const module = createUserLocalCommandModule(await createTempRoot('robota-selected-storage-'));
|
|
26
26
|
expect(module.commandSources?.[0]?.getCommands()[0]?.description).not.toMatch(/Robota/i);
|
|
27
27
|
});
|
|
28
28
|
|