@pixelbyte-software/pixcode 1.48.6 → 1.49.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.
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ const hermesHome = process.env.HERMES_HOME || path.join(os.homedir(), '.hermes');
7
+ const configPath = path.join(hermesHome, 'config.yaml');
8
+ const appRoot = process.env.PIXCODE_APP_ROOT || process.cwd();
9
+ const baseUrl = process.env.PIXCODE_BASE_URL;
10
+ const apiKey = process.env.PIXCODE_API_KEY;
11
+
12
+ if (!baseUrl || !apiKey) {
13
+ process.stderr.write('PIXCODE_BASE_URL and PIXCODE_API_KEY are required for Pixcode MCP setup.\n');
14
+ process.exit(1);
15
+ }
16
+
17
+ const mcpServerPath = path.join(appRoot, 'scripts', 'hermes', 'pixcode-mcp-server.mjs');
18
+ const block = [
19
+ ' pixcode:',
20
+ ' command: "node"',
21
+ ' args:',
22
+ ` - "${mcpServerPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`,
23
+ ' env:',
24
+ ` PIXCODE_BASE_URL: "${baseUrl.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`,
25
+ ` PIXCODE_API_KEY: "${apiKey.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`,
26
+ ' enabled: true',
27
+ ' tools:',
28
+ ' include:',
29
+ ' - pixcode_list_projects',
30
+ ' - pixcode_get_provider_status',
31
+ ' - pixcode_open_cli_terminal',
32
+ ' resources: false',
33
+ ' prompts: false',
34
+ ].join('\n');
35
+
36
+ function findRootKeyEnd(lines, startIndex) {
37
+ for (let index = startIndex + 1; index < lines.length; index += 1) {
38
+ if (/^\S[^:]*:\s*(?:#.*)?$/.test(lines[index])) {
39
+ return index;
40
+ }
41
+ }
42
+ return lines.length;
43
+ }
44
+
45
+ function findNestedKeyEnd(lines, startIndex, parentEnd) {
46
+ for (let index = startIndex + 1; index < parentEnd; index += 1) {
47
+ if (/^ \S[^:]*:\s*(?:#.*)?$/.test(lines[index])) {
48
+ return index;
49
+ }
50
+ }
51
+ return parentEnd;
52
+ }
53
+
54
+ function upsertPixcodeMcpConfig(rawConfig) {
55
+ const lines = rawConfig.split(/\r?\n/);
56
+ const mcpIndex = lines.findIndex((line) => /^mcp_servers:\s*(?:#.*)?$/.test(line));
57
+
58
+ if (mcpIndex === -1) {
59
+ const prefix = rawConfig.trim() ? `${rawConfig.replace(/\s*$/, '')}\n\n` : '';
60
+ return `${prefix}mcp_servers:\n${block}\n`;
61
+ }
62
+
63
+ const mcpEnd = findRootKeyEnd(lines, mcpIndex);
64
+ const pixcodeIndex = lines.findIndex((line, index) => (
65
+ index > mcpIndex && index < mcpEnd && /^ pixcode:\s*(?:#.*)?$/.test(line)
66
+ ));
67
+
68
+ if (pixcodeIndex === -1) {
69
+ lines.splice(mcpIndex + 1, 0, block);
70
+ return `${lines.join('\n').replace(/\s*$/, '')}\n`;
71
+ }
72
+
73
+ const pixcodeEnd = findNestedKeyEnd(lines, pixcodeIndex, mcpEnd);
74
+ lines.splice(pixcodeIndex, pixcodeEnd - pixcodeIndex, block);
75
+ return `${lines.join('\n').replace(/\s*$/, '')}\n`;
76
+ }
77
+
78
+ fs.mkdirSync(hermesHome, { recursive: true });
79
+ const previous = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
80
+ const next = upsertPixcodeMcpConfig(previous);
81
+
82
+ if (previous !== next) {
83
+ fs.writeFileSync(configPath, next);
84
+ process.stdout.write(`Pixcode MCP configured in ${configPath}\n`);
85
+ } else {
86
+ process.stdout.write(`Pixcode MCP already configured in ${configPath}\n`);
87
+ }
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ import readline from 'node:readline';
3
+
4
+ const baseUrl = (process.env.PIXCODE_BASE_URL || '').replace(/\/$/, '');
5
+ const apiKey = process.env.PIXCODE_API_KEY || '';
6
+
7
+ const tools = [
8
+ {
9
+ name: 'pixcode_list_projects',
10
+ description: 'List Pixcode workspaces/projects visible to this user, including display name, path, and file count when available.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {},
14
+ additionalProperties: false,
15
+ },
16
+ },
17
+ {
18
+ name: 'pixcode_get_provider_status',
19
+ description: 'Get install/auth/version status for one Pixcode CLI provider.',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ provider: {
24
+ type: 'string',
25
+ enum: ['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode'],
26
+ },
27
+ },
28
+ required: ['provider'],
29
+ additionalProperties: false,
30
+ },
31
+ },
32
+ {
33
+ name: 'pixcode_open_cli_terminal',
34
+ description: 'Ask the open Pixcode workbench to open a visible CLI terminal for a provider in a project.',
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ provider: {
39
+ type: 'string',
40
+ enum: ['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode'],
41
+ },
42
+ projectPath: {
43
+ type: 'string',
44
+ description: 'Absolute project path. Omit to use the currently selected Pixcode project.',
45
+ },
46
+ prompt: {
47
+ type: 'string',
48
+ description: 'Optional short reason shown to Pixcode for audit/display.',
49
+ },
50
+ },
51
+ required: ['provider'],
52
+ additionalProperties: false,
53
+ },
54
+ },
55
+ ];
56
+
57
+ function send(payload) {
58
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
59
+ }
60
+
61
+ function textResult(text) {
62
+ return {
63
+ content: [
64
+ {
65
+ type: 'text',
66
+ text,
67
+ },
68
+ ],
69
+ };
70
+ }
71
+
72
+ async function pixcodeFetch(endpoint, options = {}) {
73
+ if (!baseUrl || !apiKey) {
74
+ throw new Error('Pixcode MCP is missing PIXCODE_BASE_URL or PIXCODE_API_KEY.');
75
+ }
76
+
77
+ const response = await fetch(`${baseUrl}${endpoint}`, {
78
+ ...options,
79
+ headers: {
80
+ Authorization: `Bearer ${apiKey}`,
81
+ 'Content-Type': 'application/json',
82
+ ...(options.headers || {}),
83
+ },
84
+ });
85
+ const text = await response.text();
86
+ let body = null;
87
+ try {
88
+ body = text ? JSON.parse(text) : null;
89
+ } catch {
90
+ body = text;
91
+ }
92
+
93
+ if (!response.ok) {
94
+ throw new Error(`Pixcode API ${endpoint} failed with HTTP ${response.status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`);
95
+ }
96
+
97
+ return body;
98
+ }
99
+
100
+ async function callTool(name, args = {}) {
101
+ if (name === 'pixcode_list_projects') {
102
+ const projects = await pixcodeFetch('/api/projects');
103
+ const normalized = (Array.isArray(projects) ? projects : []).map((project) => ({
104
+ name: project.name,
105
+ displayName: project.displayName,
106
+ path: project.fullPath || project.path,
107
+ fileCount: project.fileCount ?? null,
108
+ }));
109
+ return textResult(JSON.stringify(normalized, null, 2));
110
+ }
111
+
112
+ if (name === 'pixcode_get_provider_status') {
113
+ const provider = String(args.provider || '');
114
+ const body = await pixcodeFetch(`/api/providers/${encodeURIComponent(provider)}/auth/status?refresh=1`);
115
+ return textResult(JSON.stringify(body?.data ?? body, null, 2));
116
+ }
117
+
118
+ if (name === 'pixcode_open_cli_terminal') {
119
+ const body = await pixcodeFetch('/api/orchestration/hermes/terminal-launches', {
120
+ method: 'POST',
121
+ body: JSON.stringify({
122
+ provider: args.provider,
123
+ projectPath: args.projectPath || null,
124
+ prompt: args.prompt || null,
125
+ }),
126
+ });
127
+ return textResult(JSON.stringify(body?.event ?? body, null, 2));
128
+ }
129
+
130
+ throw new Error(`Unknown Pixcode MCP tool: ${name}`);
131
+ }
132
+
133
+ async function handleMessage(message) {
134
+ if (message.method === 'initialize') {
135
+ send({
136
+ jsonrpc: '2.0',
137
+ id: message.id,
138
+ result: {
139
+ protocolVersion: message.params?.protocolVersion || '2024-11-05',
140
+ capabilities: {
141
+ tools: {},
142
+ },
143
+ serverInfo: {
144
+ name: 'pixcode-mcp',
145
+ version: '1.0.0',
146
+ },
147
+ },
148
+ });
149
+ return;
150
+ }
151
+
152
+ if (message.method === 'tools/list') {
153
+ send({
154
+ jsonrpc: '2.0',
155
+ id: message.id,
156
+ result: { tools },
157
+ });
158
+ return;
159
+ }
160
+
161
+ if (message.method === 'tools/call') {
162
+ try {
163
+ const result = await callTool(message.params?.name, message.params?.arguments || {});
164
+ send({
165
+ jsonrpc: '2.0',
166
+ id: message.id,
167
+ result,
168
+ });
169
+ } catch (error) {
170
+ send({
171
+ jsonrpc: '2.0',
172
+ id: message.id,
173
+ error: {
174
+ code: -32000,
175
+ message: error instanceof Error ? error.message : String(error),
176
+ },
177
+ });
178
+ }
179
+ return;
180
+ }
181
+
182
+ if (typeof message.id !== 'undefined') {
183
+ send({
184
+ jsonrpc: '2.0',
185
+ id: message.id,
186
+ error: {
187
+ code: -32601,
188
+ message: `Method not found: ${message.method}`,
189
+ },
190
+ });
191
+ }
192
+ }
193
+
194
+ const rl = readline.createInterface({
195
+ input: process.stdin,
196
+ crlfDelay: Number.POSITIVE_INFINITY,
197
+ });
198
+
199
+ rl.on('line', (line) => {
200
+ if (!line.trim()) return;
201
+
202
+ void (async () => {
203
+ try {
204
+ await handleMessage(JSON.parse(line));
205
+ } catch (error) {
206
+ send({
207
+ jsonrpc: '2.0',
208
+ id: null,
209
+ error: {
210
+ code: -32700,
211
+ message: error instanceof Error ? error.message : String(error),
212
+ },
213
+ });
214
+ }
215
+ })();
216
+ });
@@ -65,20 +65,51 @@ assert.match(
65
65
 
66
66
  assert.match(
67
67
  updater,
68
- /npm[\s\S]*install[\s\S]*--no-audit[\s\S]*--no-fund/,
69
- 'Safe updater should reinstall dependencies after updating source files.',
68
+ /shouldRunNpmInstall/,
69
+ 'Safe updater should decide whether dependency reconciliation is needed from changed files.',
70
70
  );
71
71
 
72
72
  assert.match(
73
73
  updater,
74
- /npm[\s\S]*run[\s\S]*build/,
75
- 'Safe updater should rebuild source installs after updating source files.',
74
+ /Dependencies unchanged; skipping npm install\./,
75
+ 'Safe updater should skip npm install when package manifests did not change.',
76
76
  );
77
77
 
78
- const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'pixcode-git-update-'));
79
- const origin = path.join(tempRoot, 'origin.git');
80
- const source = path.join(tempRoot, 'source');
81
- const install = path.join(tempRoot, 'install');
78
+ assert.match(
79
+ updater,
80
+ /shouldRunBuild/,
81
+ 'Safe updater should decide whether source rebuild is needed from changed files.',
82
+ );
83
+
84
+ assert.match(
85
+ updater,
86
+ /Build inputs unchanged; skipping build\./,
87
+ 'Safe updater should skip build when only non-runtime files changed.',
88
+ );
89
+
90
+ function makeTempRepo(name) {
91
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), `pixcode-git-update-${name}-`));
92
+ const origin = path.join(tempRoot, 'origin.git');
93
+ const source = path.join(tempRoot, 'source');
94
+ const install = path.join(tempRoot, 'install');
95
+
96
+ fs.mkdirSync(source, { recursive: true });
97
+ run('git', ['init', '--bare', origin], tempRoot);
98
+ run('git', ['init', '-b', 'main'], source);
99
+ writePackage(source, '1.0.0');
100
+ fs.mkdirSync(path.join(source, 'src'), { recursive: true });
101
+ fs.writeFileSync(path.join(source, 'src', 'app.js'), 'old\n');
102
+ fs.writeFileSync(path.join(source, 'README.md'), 'old docs\n');
103
+ fs.writeFileSync(path.join(source, 'tracked.txt'), 'old\n');
104
+ run('git', ['add', '.'], source);
105
+ run('git', ['commit', '-m', 'initial'], source);
106
+ run('git', ['remote', 'add', 'origin', origin], source);
107
+ run('git', ['push', '-u', 'origin', 'main'], source);
108
+ run('git', ['symbolic-ref', 'HEAD', 'refs/heads/main'], origin);
109
+ run('git', ['clone', origin, install], tempRoot);
110
+
111
+ return { origin, source, install };
112
+ }
82
113
 
83
114
  function run(command, args, cwd) {
84
115
  const result = spawnSync(command, args, {
@@ -99,22 +130,24 @@ function run(command, args, cwd) {
99
130
  `${command} ${args.join(' ')} failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
100
131
  );
101
132
 
102
- return result.stdout.trim();
133
+ return `${result.stdout}${result.stderr}`.trim();
103
134
  }
104
135
 
105
- function writePackage(version) {
136
+ function writePackage(root, version, dependencies = {}) {
106
137
  fs.writeFileSync(
107
- path.join(source, 'package.json'),
138
+ path.join(root, 'package.json'),
108
139
  JSON.stringify({
109
140
  name: 'pixcode-update-smoke',
110
141
  version,
111
142
  scripts: {
143
+ preinstall: 'node -e "require(\\"node:fs\\").writeFileSync(\\"install-ran.txt\\", \\"install\\")"',
112
144
  build: 'node -e "require(\\"node:fs\\").writeFileSync(\\"built.txt\\", \\"built\\")"',
113
145
  },
146
+ dependencies,
114
147
  }, null, 2),
115
148
  );
116
149
  fs.writeFileSync(
117
- path.join(source, 'package-lock.json'),
150
+ path.join(root, 'package-lock.json'),
118
151
  JSON.stringify({
119
152
  name: 'pixcode-update-smoke',
120
153
  version,
@@ -130,47 +163,93 @@ function writePackage(version) {
130
163
  );
131
164
  }
132
165
 
133
- fs.mkdirSync(source, { recursive: true });
134
- run('git', ['init', '--bare', origin], tempRoot);
135
- run('git', ['init', '-b', 'main'], source);
136
- writePackage('1.0.0');
137
- fs.writeFileSync(path.join(source, 'tracked.txt'), 'old\n');
138
- run('git', ['add', '.'], source);
139
- run('git', ['commit', '-m', 'initial'], source);
140
- run('git', ['remote', 'add', 'origin', origin], source);
141
- run('git', ['push', '-u', 'origin', 'main'], source);
142
- run('git', ['symbolic-ref', 'HEAD', 'refs/heads/main'], origin);
143
- run('git', ['clone', origin, install], tempRoot);
144
-
145
- writePackage('1.0.1');
146
- fs.writeFileSync(path.join(source, 'tracked.txt'), 'new\n');
147
- run('git', ['add', '.'], source);
148
- run('git', ['commit', '-m', 'update'], source);
149
- run('git', ['push', 'origin', 'main'], source);
150
-
151
- fs.writeFileSync(path.join(install, 'tracked.txt'), 'local dirty change\n');
152
- fs.writeFileSync(path.join(install, 'untracked.txt'), 'local untracked change\n');
153
- run(process.execPath, [updaterPath], install);
154
-
155
- assert.equal(
156
- JSON.parse(fs.readFileSync(path.join(install, 'package.json'), 'utf8')).version,
157
- '1.0.1',
158
- 'Safe updater should fast-forward the install checkout.',
159
- );
160
- assert.equal(
161
- fs.readFileSync(path.join(install, 'tracked.txt'), 'utf8'),
162
- 'new\n',
163
- 'Safe updater should apply the remote tracked file after stashing local edits.',
164
- );
165
- assert.match(
166
- run('git', ['stash', 'list'], install),
167
- /pixcode-auto-update-/,
168
- 'Safe updater should leave local dirty files recoverable in git stash.',
169
- );
170
- assert.equal(
171
- fs.readFileSync(path.join(install, 'built.txt'), 'utf8'),
172
- 'built',
173
- 'Safe updater should run the repository build after installing dependencies.',
174
- );
166
+ {
167
+ const { source, install } = makeTempRepo('deps');
168
+
169
+ fs.mkdirSync(path.join(source, 'local-dep'), { recursive: true });
170
+ fs.writeFileSync(
171
+ path.join(source, 'local-dep', 'package.json'),
172
+ JSON.stringify({ name: 'pixcode-smoke-local-dep', version: '1.0.0' }, null, 2),
173
+ );
174
+ writePackage(source, '1.0.1', { 'pixcode-smoke-local-dep': 'file:./local-dep' });
175
+ fs.writeFileSync(path.join(source, 'tracked.txt'), 'new\n');
176
+ run('git', ['add', '.'], source);
177
+ run('git', ['commit', '-m', 'dependency update'], source);
178
+ run('git', ['push', 'origin', 'main'], source);
179
+
180
+ fs.writeFileSync(path.join(install, 'tracked.txt'), 'local dirty change\n');
181
+ fs.writeFileSync(path.join(install, 'untracked.txt'), 'local untracked change\n');
182
+ run(process.execPath, [updaterPath], install);
183
+
184
+ assert.equal(
185
+ JSON.parse(fs.readFileSync(path.join(install, 'package.json'), 'utf8')).version,
186
+ '1.0.1',
187
+ 'Safe updater should fast-forward the install checkout.',
188
+ );
189
+ assert.equal(
190
+ fs.readFileSync(path.join(install, 'tracked.txt'), 'utf8'),
191
+ 'new\n',
192
+ 'Safe updater should apply the remote tracked file after stashing local edits.',
193
+ );
194
+ assert.match(
195
+ run('git', ['stash', 'list'], install),
196
+ /pixcode-auto-update-/,
197
+ 'Safe updater should leave local dirty files recoverable in git stash.',
198
+ );
199
+ assert.equal(
200
+ fs.readFileSync(path.join(install, 'install-ran.txt'), 'utf8'),
201
+ 'install',
202
+ 'Dependency updates should run npm install.',
203
+ );
204
+ assert.equal(
205
+ fs.readFileSync(path.join(install, 'built.txt'), 'utf8'),
206
+ 'built',
207
+ 'Safe updater should run the repository build after dependency updates.',
208
+ );
209
+ }
210
+
211
+ {
212
+ const { source, install } = makeTempRepo('source');
213
+
214
+ fs.writeFileSync(path.join(source, 'src', 'app.js'), 'new source\n');
215
+ run('git', ['add', '.'], source);
216
+ run('git', ['commit', '-m', 'source update'], source);
217
+ run('git', ['push', 'origin', 'main'], source);
218
+
219
+ run(process.execPath, [updaterPath], install);
220
+
221
+ assert.equal(
222
+ fs.existsSync(path.join(install, 'install-ran.txt')),
223
+ false,
224
+ 'Source-only updates should skip npm install.',
225
+ );
226
+ assert.equal(
227
+ fs.readFileSync(path.join(install, 'built.txt'), 'utf8'),
228
+ 'built',
229
+ 'Source-only updates should produce a fresh build output.',
230
+ );
231
+ }
232
+
233
+ {
234
+ const { source, install } = makeTempRepo('docs');
235
+
236
+ fs.writeFileSync(path.join(source, 'README.md'), 'new docs\n');
237
+ run('git', ['add', '.'], source);
238
+ run('git', ['commit', '-m', 'docs update'], source);
239
+ run('git', ['push', 'origin', 'main'], source);
240
+
241
+ run(process.execPath, [updaterPath], install);
242
+
243
+ assert.equal(
244
+ fs.existsSync(path.join(install, 'install-ran.txt')),
245
+ false,
246
+ 'Docs-only updates should skip npm install.',
247
+ );
248
+ assert.equal(
249
+ fs.existsSync(path.join(install, 'built.txt')),
250
+ false,
251
+ 'Docs-only updates should not create build output.',
252
+ );
253
+ }
175
254
 
176
255
  console.log('git install update smoke passed');
@@ -18,6 +18,7 @@ const serverIndex = read('server/index.js');
18
18
  const hermesRoutes = read('server/modules/orchestration/hermes/hermes.routes.ts');
19
19
  const shellTerminal = read('src/components/shell/hooks/useShellTerminal.ts');
20
20
  const gitPanelHeader = read('src/components/git-panel/view/GitPanelHeader.tsx');
21
+ const themeContext = read('src/contexts/ThemeContext.jsx');
21
22
 
22
23
  assert.match(
23
24
  preferenceHook,
@@ -52,13 +53,25 @@ assert.match(workbench, /onClose=\{closeTerminal\}/, 'Closing the workbench term
52
53
  assert.match(workbench, /WorkbenchCliPanelToolbar/, 'CLI terminal should keep history and new-session actions visible.');
53
54
  assert.match(workbench, /WORKBENCH_CLI_STATE_STORAGE_KEY/, 'CLI terminal should remember per-project open state across workspace switches.');
54
55
  assert.match(workbench, /function WorkbenchBottomTerminal/, 'Terminal activity should render as a bottom plain-shell panel.');
56
+ assert.match(workbench, /BOTTOM_TERMINAL_MIN_HEIGHT/, 'Bottom terminal should support height resizing.');
57
+ assert.match(workbench, /isBottomTerminalMinimized/, 'Bottom terminal should support minimizing without closing.');
55
58
  assert.match(workbench, /isPlainShell/, 'Bottom terminal should open the selected project folder without starting the selected AI CLI.');
56
- assert.match(workbench, /startHermesAgent/, 'Right CLI panel should launch Hermes Agent in the active project.');
59
+ assert.match(workbench, /HERMES_AGENT_START_COMMAND/, 'Hermes Agent should launch from the bottom terminal through a server-side sentinel.');
60
+ assert.doesNotMatch(workbench, /Project-scoped agent terminal\. Installs Hermes when missing/, 'Right CLI panel should not show the old Hermes card.');
61
+ assert.match(workbench, /shrinkCliPanel/, 'Right CLI panel should expose a shrink action.');
62
+ assert.match(workbench, /expandCliPanel/, 'Right CLI panel should expose an expand action.');
63
+ assert.match(workbench, /vscodeWorkbench\.welcome\.openProject/, 'Workbench welcome should expose a simple Open Project action.');
64
+ assert.match(workbench, /vscodeWorkbench\.welcome\.cloneProject/, 'Workbench welcome should expose a simple Clone action.');
65
+ assert.match(workbench, /vscodeWorkbench\.welcome\.startHermes/, 'Workbench welcome should expose a Hermes start action.');
66
+ assert.match(workbench, /DarkModeToggle/, 'Workbench welcome should expose a dark-mode toggle.');
67
+ assert.match(themeContext, /return true;/, 'Pixcode should default new installs to dark mode.');
57
68
  assert.match(workbench, /openNewCliSessionPicker/, 'CLI terminal plus should return to provider selection before starting a fresh session.');
58
69
  assert.match(workbench, /terminateCurrentCliSession\(selectedProvider\)/, 'CLI terminal plus should terminate the existing provider PTY before showing selection.');
59
70
  assert.match(workbench, /forceNewSession=\{terminalLaunch\.forceNewSession\}/, 'Fresh CLI sessions should bypass the cached default PTY.');
60
71
  assert.match(serverIndex, /\/api\/shell\/sessions\/terminate/, 'Backend should expose an authenticated endpoint to terminate cached provider PTYs immediately.');
61
72
  assert.match(serverIndex, /isPlainShell && !initialCommand/, 'Backend should spawn an interactive plain shell when no terminal command is provided.');
73
+ assert.match(serverIndex, /pixcode:hermes:start/, 'Backend should expand Hermes terminal sentinels on the server host.');
74
+ assert.doesNotMatch(serverIndex, /iex \(irm https:\/\/raw\.githubusercontent\.com\/NousResearch\/hermes-agent\/main\/scripts\/install\.ps1\)/, 'Windows Hermes install should avoid the old inline iex pattern.');
62
75
  assert.doesNotMatch(shellTerminal, /new WebglAddon\(\)/, 'Workbench terminal should use the stable xterm renderer.');
63
76
  assert.match(workbench, /setActivityPanel\('explorer'\)/, 'Selecting a project should return the side panel to Explorer.');
64
77
  assert.match(gitPanelHeader, /compact/, 'Workbench Source Control should have compact icon-only controls.');
@@ -80,6 +93,7 @@ assert.doesNotMatch(workbench, /tabs\.orchestration/, 'Workbench menus should no
80
93
  assert.match(serverIndex, /app\.use\('\/hermes', createHermesTaskRouter\(\)\)/, 'Internal task router should be mounted behind Hermes.');
81
94
  assert.doesNotMatch(serverIndex, /app\.use\('\/a2a'/, 'Server should not expose the old A2A route.');
82
95
  assert.match(hermesRoutes, /createHermesRouter/, 'Hermes should have a dedicated orchestration API router.');
96
+ assert.match(hermesRoutes, /terminal-launches/, 'Hermes MCP should be able to request visible Pixcode CLI terminal launches.');
83
97
  assert.match(serverIndex, /forceNewSession/, 'Shell backend should support explicit fresh-session launches from the workbench.');
84
98
  assert.match(serverIndex, /killProviderPtySessions/, 'Shell backend should terminate old provider PTYs when a fresh CLI session is requested.');
85
99
 
@@ -51,6 +51,15 @@ assert.match(
51
51
  'Workbench center should show a project landing page instead of a blank editor when no project is selected.',
52
52
  );
53
53
 
54
+ for (const token of [
55
+ 'vscodeWorkbench.welcome.openProject',
56
+ 'vscodeWorkbench.welcome.cloneProject',
57
+ 'vscodeWorkbench.welcome.startHermes',
58
+ 'DarkModeToggle',
59
+ ]) {
60
+ assert.match(workbench, new RegExp(token.replaceAll('.', '\\.')), `Workbench welcome should include ${token}.`);
61
+ }
62
+
54
63
  assert.match(
55
64
  workbench,
56
65
  /function WorkbenchCliPanel/,
@@ -244,6 +253,10 @@ assert.match(
244
253
  'Terminal activity should open a VS Code-style bottom terminal instead of the provider CLI picker.',
245
254
  );
246
255
 
256
+ for (const token of ['BOTTOM_TERMINAL_MIN_HEIGHT', 'isBottomTerminalMinimized', 'shrinkCliPanel', 'expandCliPanel']) {
257
+ assert.match(workbench, new RegExp(token), `Workbench should include ${token}.`);
258
+ }
259
+
247
260
  assert.match(
248
261
  workbench,
249
262
  /isPlainShell/,
@@ -252,8 +265,14 @@ assert.match(
252
265
 
253
266
  assert.match(
254
267
  workbench,
255
- /startHermesAgent/,
256
- 'Right CLI panel should offer Hermes Agent as a project-scoped control agent.',
268
+ /HERMES_AGENT_START_COMMAND/,
269
+ 'Hermes Agent should launch through the bottom terminal with a server-side command sentinel.',
270
+ );
271
+
272
+ assert.doesNotMatch(
273
+ workbench,
274
+ /Project-scoped agent terminal\. Installs Hermes when missing/,
275
+ 'Right CLI picker should not show the old Hermes install card.',
257
276
  );
258
277
 
259
278
  assert.match(