@pixelbyte-software/pixcode 1.40.5 → 1.40.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{index-DROaSsD_.js → index-C8XyPjlN.js} +149 -149
- package/dist/index.html +1 -1
- package/dist/landing.html +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +87 -4
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
- package/dist-server/server/services/live-view.js +124 -2
- package/dist-server/server/services/live-view.js.map +1 -1
- package/dist-server/server/services/managed-runtimes.js +89 -33
- package/dist-server/server/services/managed-runtimes.js.map +1 -1
- package/dist-server/server/services/provider-models.js +16 -3
- package/dist-server/server/services/provider-models.js.map +1 -1
- package/dist-server/shared/modelConstants.js +2 -3
- package/dist-server/shared/modelConstants.js.map +1 -1
- package/package.json +1 -1
- package/scripts/smoke/live-view-integration.mjs +167 -1
- package/scripts/smoke/orchestration-model-sync.mjs +30 -0
- package/scripts/smoke/provider-models-opencode-live.mjs +66 -0
- package/server/modules/orchestration/workflows/workflow-runner.ts +108 -4
- package/server/services/live-view.js +128 -2
- package/server/services/managed-runtimes.js +88 -34
- package/server/services/provider-models.js +18 -3
- package/shared/modelConstants.js +2 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmod, mkdtemp, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { access, chmod, mkdtemp, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -9,6 +9,7 @@ const read = async (relativePath) => {
|
|
|
9
9
|
const { readFile } = await import('node:fs/promises');
|
|
10
10
|
return readFile(path.join(repoRoot, relativePath), 'utf8');
|
|
11
11
|
};
|
|
12
|
+
const fileExists = async (filePath) => access(filePath).then(() => true, () => false);
|
|
12
13
|
|
|
13
14
|
const appTypes = await read('src/types/app.ts');
|
|
14
15
|
assert.ok(
|
|
@@ -113,6 +114,12 @@ assert.ok(
|
|
|
113
114
|
managedRuntimes.includes("id === 'npm'") && managedRuntimes.includes('installNpmRuntime'),
|
|
114
115
|
'Managed runtimes should include a Pixcode-owned npm runner for JavaScript projects when npm is not on PATH.',
|
|
115
116
|
);
|
|
117
|
+
assert.ok(
|
|
118
|
+
managedRuntimes.includes('buildPowerShellExpandArchiveArgs')
|
|
119
|
+
&& managedRuntimes.includes('param([string]$archive, [string]$destination)')
|
|
120
|
+
&& !managedRuntimes.includes('LiteralPath $args[0]'),
|
|
121
|
+
'Windows zip extraction should pass archive paths through a PowerShell param block instead of unreliable $args indexing.',
|
|
122
|
+
);
|
|
116
123
|
|
|
117
124
|
const serverIndex = await read('server/index.js');
|
|
118
125
|
assert.ok(
|
|
@@ -134,6 +141,7 @@ const {
|
|
|
134
141
|
startLiveView,
|
|
135
142
|
stopLiveView,
|
|
136
143
|
} = await import('../../server/services/live-view.js');
|
|
144
|
+
const { ensureManagedRuntime, getManagedRuntimeStatus } = await import('../../server/services/managed-runtimes.js');
|
|
137
145
|
const workspace = await mkdtemp(path.join(tmpdir(), 'pixcode-live-view-smoke-'));
|
|
138
146
|
const staticProject = path.join(workspace, 'static');
|
|
139
147
|
const viteProject = path.join(workspace, 'vite');
|
|
@@ -217,6 +225,164 @@ assert.equal(phpSystemRuntimeTarget.available, true, 'PHP projects should stay r
|
|
|
217
225
|
assert.equal(phpSystemRuntimeTarget.command?.id, 'frankenphp-php-server', 'PHP projects should still use the Pixcode-managed runtime even when external php exists.');
|
|
218
226
|
assert.equal(phpSystemRuntimeTarget.managedRuntime?.id, 'frankenphp', 'PHP projects should prefer the Pixcode-owned FrankenPHP runtime instead of external php.');
|
|
219
227
|
|
|
228
|
+
const tar = await import('tar');
|
|
229
|
+
const npmPackageRoot = path.join(workspace, 'npm-package-root');
|
|
230
|
+
const npmPackageDir = path.join(npmPackageRoot, 'package');
|
|
231
|
+
await mkdir(path.join(npmPackageDir, 'bin'), { recursive: true });
|
|
232
|
+
await writeFile(path.join(npmPackageDir, 'package.json'), JSON.stringify({ name: 'npm', version: '10.0.0' }));
|
|
233
|
+
await writeFile(path.join(npmPackageDir, 'bin', 'npm-cli.js'), '#!/usr/bin/env node\nconsole.log("npm smoke");\n');
|
|
234
|
+
const npmTarball = path.join(workspace, 'npm-runtime.tgz');
|
|
235
|
+
await tar.c({ cwd: npmPackageRoot, file: npmTarball, gzip: true }, ['package']);
|
|
236
|
+
const npmTarballBuffer = await readFile(npmTarball);
|
|
237
|
+
const originalFetch = globalThis.fetch;
|
|
238
|
+
const metadataAcceptHeaders = [];
|
|
239
|
+
globalThis.fetch = async (url, options = {}) => {
|
|
240
|
+
const requestUrl = String(url);
|
|
241
|
+
const headers = options.headers || {};
|
|
242
|
+
const accept = typeof headers.get === 'function' ? headers.get('Accept') : headers.Accept;
|
|
243
|
+
if (requestUrl === 'https://registry.test/npm/latest') {
|
|
244
|
+
metadataAcceptHeaders.push(String(accept || ''));
|
|
245
|
+
if (!String(accept || '').includes('application/json')) {
|
|
246
|
+
return new Response(JSON.stringify({ error: 'not acceptable' }), { status: 406 });
|
|
247
|
+
}
|
|
248
|
+
return new Response(JSON.stringify({
|
|
249
|
+
version: '10.0.0',
|
|
250
|
+
dist: { tarball: 'https://registry.test/npm/-/npm-10.0.0.tgz' },
|
|
251
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
252
|
+
}
|
|
253
|
+
if (requestUrl === 'https://registry.test/npm/-/npm-10.0.0.tgz') {
|
|
254
|
+
return new Response(npmTarballBuffer, { status: 200 });
|
|
255
|
+
}
|
|
256
|
+
return originalFetch(url, options);
|
|
257
|
+
};
|
|
258
|
+
try {
|
|
259
|
+
const npmRuntime = await ensureManagedRuntime('npm', {
|
|
260
|
+
preferManaged: true,
|
|
261
|
+
env: {
|
|
262
|
+
...process.env,
|
|
263
|
+
PATH: '',
|
|
264
|
+
Path: '',
|
|
265
|
+
PIXCODE_MANAGED_RUNTIMES_HOME: path.join(workspace, 'managed-runtimes'),
|
|
266
|
+
PIXCODE_NPM_RUNTIME_REGISTRY: 'https://registry.test/npm/latest',
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
assert.equal(npmRuntime.status, 'installed', 'Managed npm runtime should install from npm registry metadata.');
|
|
270
|
+
assert.ok(
|
|
271
|
+
metadataAcceptHeaders.every((accept) => accept.includes('application/json')),
|
|
272
|
+
'Managed npm runtime metadata requests should use an npm-compatible JSON Accept header.',
|
|
273
|
+
);
|
|
274
|
+
} finally {
|
|
275
|
+
globalThis.fetch = originalFetch;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const depsProject = path.join(workspace, 'vite-needs-install');
|
|
279
|
+
await mkdir(depsProject, { recursive: true });
|
|
280
|
+
await writeFile(path.join(depsProject, 'package.json'), JSON.stringify({
|
|
281
|
+
scripts: { dev: 'vite --host 127.0.0.1' },
|
|
282
|
+
dependencies: { vite: '^7.0.0' },
|
|
283
|
+
}, null, 2));
|
|
284
|
+
const fakeNpmCli = path.join(workspace, 'fake-npm-cli.js');
|
|
285
|
+
await writeFile(fakeNpmCli, [
|
|
286
|
+
'#!/usr/bin/env node',
|
|
287
|
+
'const fs = require("node:fs");',
|
|
288
|
+
'const path = require("node:path");',
|
|
289
|
+
'if (process.argv[2] === "install") {',
|
|
290
|
+
' fs.mkdirSync(path.join(process.cwd(), "node_modules", ".bin"), { recursive: true });',
|
|
291
|
+
' fs.writeFileSync(path.join(process.cwd(), "node_modules", ".bin", process.platform === "win32" ? "vite.cmd" : "vite"), "ok");',
|
|
292
|
+
' process.exit(0);',
|
|
293
|
+
'}',
|
|
294
|
+
'process.exit(0);',
|
|
295
|
+
'',
|
|
296
|
+
].join('\n'));
|
|
297
|
+
if (process.platform !== 'win32') {
|
|
298
|
+
await chmod(fakeNpmCli, 0o755);
|
|
299
|
+
}
|
|
300
|
+
const { preparePackageDependencies } = await import('../../server/services/live-view.js');
|
|
301
|
+
const prepLogs = [];
|
|
302
|
+
await preparePackageDependencies(
|
|
303
|
+
depsProject,
|
|
304
|
+
{
|
|
305
|
+
id: 'npm-dev-vite',
|
|
306
|
+
label: 'Vite dev server',
|
|
307
|
+
framework: 'Vite',
|
|
308
|
+
packageManager: 'npm',
|
|
309
|
+
scriptName: 'dev',
|
|
310
|
+
command: process.execPath,
|
|
311
|
+
args: [fakeNpmCli, 'run', 'dev'],
|
|
312
|
+
displayCommand: 'npm run dev',
|
|
313
|
+
managedRuntime: { id: 'npm', status: 'installed' },
|
|
314
|
+
},
|
|
315
|
+
process.env,
|
|
316
|
+
(line) => prepLogs.push(line),
|
|
317
|
+
);
|
|
318
|
+
assert.ok(
|
|
319
|
+
await fileExists(path.join(depsProject, 'node_modules', '.bin', process.platform === 'win32' ? 'vite.cmd' : 'vite')),
|
|
320
|
+
'Live View should install missing Vite project dependencies before running npm dev.',
|
|
321
|
+
);
|
|
322
|
+
assert.ok(
|
|
323
|
+
prepLogs.some((line) => line.includes('Installing project dependencies')),
|
|
324
|
+
'Live View should log dependency preparation before launching package scripts.',
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const frankenPackageRoot = path.join(workspace, 'frankenphp-package-root');
|
|
328
|
+
const frankenPackageDir = path.join(frankenPackageRoot, 'package');
|
|
329
|
+
await mkdir(frankenPackageDir, { recursive: true });
|
|
330
|
+
await writeFile(path.join(frankenPackageDir, process.platform === 'win32' ? 'frankenphp.exe' : 'frankenphp'), '#!/bin/sh\nexit 0\n');
|
|
331
|
+
await writeFile(path.join(frankenPackageDir, 'sidecar-runtime.dll'), 'sidecar');
|
|
332
|
+
if (process.platform !== 'win32') {
|
|
333
|
+
await chmod(path.join(frankenPackageDir, 'frankenphp'), 0o755);
|
|
334
|
+
}
|
|
335
|
+
const frankenTarball = path.join(workspace, 'frankenphp-runtime.tgz');
|
|
336
|
+
await tar.c({ cwd: frankenPackageRoot, file: frankenTarball, gzip: true }, ['package']);
|
|
337
|
+
const frankenTarballBuffer = await readFile(frankenTarball);
|
|
338
|
+
globalThis.fetch = async (url) => {
|
|
339
|
+
if (String(url) === 'https://runtime.test/frankenphp-runtime.tgz') {
|
|
340
|
+
return new Response(frankenTarballBuffer, { status: 200 });
|
|
341
|
+
}
|
|
342
|
+
return originalFetch(url);
|
|
343
|
+
};
|
|
344
|
+
try {
|
|
345
|
+
const frankenHome = path.join(workspace, 'managed-frankenphp');
|
|
346
|
+
const frankenRuntime = await ensureManagedRuntime('frankenphp', {
|
|
347
|
+
preferManaged: true,
|
|
348
|
+
env: {
|
|
349
|
+
...process.env,
|
|
350
|
+
PIXCODE_MANAGED_RUNTIMES_HOME: frankenHome,
|
|
351
|
+
PIXCODE_FRANKENPHP_URL: 'https://runtime.test/frankenphp-runtime.tgz',
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
assert.equal(frankenRuntime.status, 'installed', 'Managed FrankenPHP runtime should install from the downloaded archive.');
|
|
355
|
+
assert.ok(
|
|
356
|
+
await fileExists(path.join(frankenHome, 'frankenphp', 'current', 'sidecar-runtime.dll')),
|
|
357
|
+
'Managed FrankenPHP install should preserve sidecar DLLs/files next to the executable.',
|
|
358
|
+
);
|
|
359
|
+
} finally {
|
|
360
|
+
globalThis.fetch = originalFetch;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const brokenRuntimeHome = path.join(workspace, 'broken-frankenphp');
|
|
364
|
+
const brokenCurrent = path.join(brokenRuntimeHome, 'frankenphp', 'current');
|
|
365
|
+
await mkdir(brokenCurrent, { recursive: true });
|
|
366
|
+
const brokenExecutable = path.join(brokenCurrent, process.platform === 'win32' ? 'frankenphp.cmd' : 'frankenphp');
|
|
367
|
+
await writeFile(brokenExecutable, process.platform === 'win32' ? '@echo off\r\nexit /b 1\r\n' : '#!/bin/sh\nexit 1\n');
|
|
368
|
+
if (process.platform !== 'win32') {
|
|
369
|
+
await chmod(brokenExecutable, 0o755);
|
|
370
|
+
}
|
|
371
|
+
await writeFile(path.join(brokenRuntimeHome, 'frankenphp', 'pixcode-runtime.json'), JSON.stringify({
|
|
372
|
+
id: 'frankenphp',
|
|
373
|
+
label: 'Pixcode PHP runtime',
|
|
374
|
+
executablePath: brokenExecutable,
|
|
375
|
+
version: 'broken',
|
|
376
|
+
}, null, 2));
|
|
377
|
+
const brokenStatus = await getManagedRuntimeStatus('frankenphp', {
|
|
378
|
+
preferManaged: true,
|
|
379
|
+
env: {
|
|
380
|
+
...process.env,
|
|
381
|
+
PIXCODE_MANAGED_RUNTIMES_HOME: brokenRuntimeHome,
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
assert.equal(brokenStatus.status, 'missing', 'Broken managed FrankenPHP manifests should be treated as missing so Pixcode can reinstall them.');
|
|
385
|
+
|
|
220
386
|
const staticSession = await startLiveView('static-smoke', staticProject);
|
|
221
387
|
assert.equal(staticSession.status, 'running', 'Static Live View should start without a child process.');
|
|
222
388
|
assert.match(staticSession.sharePath, /^\/live\/[a-f0-9]{24}\/$/, 'Live View should expose a random public share path.');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
const page = readFileSync('src/components/orchestration/OrchestrationPage.tsx', 'utf8');
|
|
7
|
+
const runner = readFileSync('server/modules/orchestration/workflows/workflow-runner.ts', 'utf8');
|
|
8
|
+
|
|
9
|
+
assert.ok(
|
|
10
|
+
page.includes("from '../../hooks/useProviderModels'") || page.includes('from "@/hooks/useProviderModels"'),
|
|
11
|
+
'Orchestration page should use the same live provider model catalog hook as chat.',
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
assert.ok(
|
|
15
|
+
page.includes('providerModelCatalogs') && page.includes('sanitizeAgentModel'),
|
|
16
|
+
'Orchestration agents should sanitize stale selected models against the live catalog before starting runs.',
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
assert.ok(
|
|
20
|
+
runner.includes('resolveWorkflowModel') && runner.includes('modelCatalogsByProvider'),
|
|
21
|
+
'Workflow runner should validate provider models server-side before submitting A2A tasks.',
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
assert.ok(
|
|
25
|
+
runner.includes('Original user request')
|
|
26
|
+
&& runner.indexOf('Original user request') < runner.lastIndexOf('workspaceContextPrompt(workspaceTarget)'),
|
|
27
|
+
'Workflow runner should place a labeled original user request before workspace context so agents do not answer the context header.',
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
console.log('orchestration model sync smoke passed');
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { mkdtemp } from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
const tempHome = await mkdtemp(path.join(os.tmpdir(), 'pixcode-provider-models-'));
|
|
9
|
+
process.env.HOME = tempHome;
|
|
10
|
+
process.env.OPENCODE_MODELS_URL = 'https://models.dev/test-api.json';
|
|
11
|
+
|
|
12
|
+
const calls = [];
|
|
13
|
+
globalThis.fetch = async (url) => {
|
|
14
|
+
calls.push(String(url));
|
|
15
|
+
return {
|
|
16
|
+
ok: true,
|
|
17
|
+
status: 200,
|
|
18
|
+
async json() {
|
|
19
|
+
return {
|
|
20
|
+
opencode: {
|
|
21
|
+
name: 'OpenCode Zen',
|
|
22
|
+
models: {
|
|
23
|
+
'big-pickle': {
|
|
24
|
+
name: 'Big Pickle',
|
|
25
|
+
cost: { input: 0, output: 0 },
|
|
26
|
+
limit: { context: 128000 },
|
|
27
|
+
},
|
|
28
|
+
'fresh-free': {
|
|
29
|
+
name: 'Fresh Free',
|
|
30
|
+
cost: { input: 0, output: 0 },
|
|
31
|
+
limit: { context: 64000 },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const { getProviderModels } = await import('../../server/services/provider-models.js');
|
|
41
|
+
|
|
42
|
+
const staleStatic = [
|
|
43
|
+
{ value: 'opencode/hy3-preview-free', label: 'Stale Hy3 Preview' },
|
|
44
|
+
{ value: 'opencode/ling-2.6-flash-free', label: 'Stale Ling 2.6 Flash' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const refreshed = await getProviderModels('opencode', {
|
|
48
|
+
forceRefresh: true,
|
|
49
|
+
staticList: staleStatic,
|
|
50
|
+
});
|
|
51
|
+
const refreshedValues = refreshed.models.map((model) => model.value);
|
|
52
|
+
assert.equal(calls.length, 1, 'force refresh should fetch the OpenCode live catalog once');
|
|
53
|
+
assert.ok(refreshedValues.includes('opencode/fresh-free'), 'live OpenCode models should be returned');
|
|
54
|
+
assert.ok(!refreshedValues.includes('opencode/hy3-preview-free'), 'stale static OpenCode models must not be merged into a successful live catalog');
|
|
55
|
+
assert.ok(!refreshedValues.includes('opencode/ling-2.6-flash-free'), 'stale static OpenCode models must not survive a successful live refresh');
|
|
56
|
+
|
|
57
|
+
const cached = await getProviderModels('opencode', {
|
|
58
|
+
staticList: staleStatic,
|
|
59
|
+
});
|
|
60
|
+
const cachedValues = cached.models.map((model) => model.value);
|
|
61
|
+
assert.equal(calls.length, 1, 'fresh cache should be used without another network request');
|
|
62
|
+
assert.ok(cachedValues.includes('opencode/fresh-free'), 'cached live OpenCode models should be returned');
|
|
63
|
+
assert.ok(!cachedValues.includes('opencode/hy3-preview-free'), 'cached live OpenCode catalog must stay free of stale static models');
|
|
64
|
+
assert.ok(!cachedValues.includes('opencode/ling-2.6-flash-free'), 'cached live OpenCode catalog must stay free of stale static models');
|
|
65
|
+
|
|
66
|
+
console.log('provider OpenCode live model catalog smoke passed');
|
|
@@ -13,6 +13,17 @@ import {
|
|
|
13
13
|
workspaceTargetMetadata,
|
|
14
14
|
} from '@/modules/orchestration/workflows/workspace-target.js';
|
|
15
15
|
import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
|
|
16
|
+
// @ts-ignore — plain-JS service
|
|
17
|
+
import { getProviderModels } from '@/services/provider-models.js';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
CLAUDE_MODELS,
|
|
21
|
+
CODEX_MODELS,
|
|
22
|
+
CURSOR_MODELS,
|
|
23
|
+
GEMINI_MODELS,
|
|
24
|
+
OPENCODE_MODELS,
|
|
25
|
+
QWEN_MODELS,
|
|
26
|
+
} from '../../../../shared/modelConstants.js';
|
|
16
27
|
|
|
17
28
|
const TERMINAL = new Set(['completed', 'failed', 'canceled']);
|
|
18
29
|
const SKIPPED = 'skipped';
|
|
@@ -106,6 +117,34 @@ type AgentAssignment = {
|
|
|
106
117
|
|
|
107
118
|
type KnownAgentRole = typeof KNOWN_AGENT_ROLES[number];
|
|
108
119
|
type AgentRole = string;
|
|
120
|
+
type ProviderId = 'claude' | 'cursor' | 'codex' | 'gemini' | 'qwen' | 'opencode';
|
|
121
|
+
type ProviderModel = {
|
|
122
|
+
value: string;
|
|
123
|
+
label?: string;
|
|
124
|
+
source?: 'static' | 'api';
|
|
125
|
+
free?: boolean;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const adapterProviderMap: Record<string, ProviderId | undefined> = {
|
|
129
|
+
'claude-code': 'claude',
|
|
130
|
+
cursor: 'cursor',
|
|
131
|
+
codex: 'codex',
|
|
132
|
+
gemini: 'gemini',
|
|
133
|
+
qwen: 'qwen',
|
|
134
|
+
opencode: 'opencode',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const modelCatalogsByProvider: Record<ProviderId, {
|
|
138
|
+
staticList: ProviderModel[];
|
|
139
|
+
defaultModel?: string;
|
|
140
|
+
}> = {
|
|
141
|
+
claude: { staticList: CLAUDE_MODELS.OPTIONS, defaultModel: CLAUDE_MODELS.DEFAULT },
|
|
142
|
+
cursor: { staticList: CURSOR_MODELS.OPTIONS, defaultModel: CURSOR_MODELS.DEFAULT },
|
|
143
|
+
codex: { staticList: CODEX_MODELS.OPTIONS, defaultModel: CODEX_MODELS.DEFAULT },
|
|
144
|
+
gemini: { staticList: GEMINI_MODELS.OPTIONS, defaultModel: GEMINI_MODELS.DEFAULT },
|
|
145
|
+
qwen: { staticList: QWEN_MODELS.OPTIONS, defaultModel: QWEN_MODELS.DEFAULT },
|
|
146
|
+
opencode: { staticList: OPENCODE_MODELS.OPTIONS, defaultModel: OPENCODE_MODELS.DEFAULT },
|
|
147
|
+
};
|
|
109
148
|
|
|
110
149
|
function readAgentRole(value: unknown): AgentRole | undefined {
|
|
111
150
|
return typeof value === 'string' && value.trim() && value.trim() !== 'auto'
|
|
@@ -133,6 +172,44 @@ function readBoolean(value: unknown): boolean | undefined {
|
|
|
133
172
|
return typeof value === 'boolean' ? value : undefined;
|
|
134
173
|
}
|
|
135
174
|
|
|
175
|
+
function modelValueSet(models: ProviderModel[]): Set<string> {
|
|
176
|
+
return new Set(models.map((model) => model.value).filter(Boolean));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function preferredFallbackModel(models: ProviderModel[], defaultModel?: string): string | undefined {
|
|
180
|
+
const values = modelValueSet(models);
|
|
181
|
+
if (defaultModel && values.has(defaultModel)) return defaultModel;
|
|
182
|
+
return models.find((model) => model.source === 'api' && model.free)?.value
|
|
183
|
+
?? models.find((model) => model.source === 'api')?.value
|
|
184
|
+
?? models.find((model) => model.free)?.value
|
|
185
|
+
?? models[0]?.value
|
|
186
|
+
?? defaultModel;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function resolveWorkflowModel(adapterId: string, requestedModel?: string): Promise<string | undefined> {
|
|
190
|
+
const provider = adapterProviderMap[adapterId];
|
|
191
|
+
if (!provider) return requestedModel;
|
|
192
|
+
|
|
193
|
+
const catalog = modelCatalogsByProvider[provider];
|
|
194
|
+
if (!requestedModel) return catalog.defaultModel;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const result = await getProviderModels(provider, {
|
|
198
|
+
staticList: catalog.staticList,
|
|
199
|
+
});
|
|
200
|
+
const models = Array.isArray(result?.models) ? result.models as ProviderModel[] : [];
|
|
201
|
+
if (modelValueSet(models).has(requestedModel)) {
|
|
202
|
+
return requestedModel;
|
|
203
|
+
}
|
|
204
|
+
return preferredFallbackModel(models, catalog.defaultModel) ?? requestedModel;
|
|
205
|
+
} catch {
|
|
206
|
+
const staticValues = modelValueSet(catalog.staticList);
|
|
207
|
+
return staticValues.has(requestedModel)
|
|
208
|
+
? requestedModel
|
|
209
|
+
: preferredFallbackModel(catalog.staticList, catalog.defaultModel) ?? requestedModel;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
136
213
|
function readIsolation(value: unknown): 'host' | 'worktree' | 'docker' | undefined {
|
|
137
214
|
return value === 'host' || value === 'worktree' || value === 'docker' ? value : undefined;
|
|
138
215
|
}
|
|
@@ -1408,15 +1485,42 @@ class WorkflowRunner {
|
|
|
1408
1485
|
|
|
1409
1486
|
const inputContext = node.inputs.map((input) => outputs.get(input)).filter(Boolean).join('\n\n');
|
|
1410
1487
|
const workspaceTarget = resolveWorkflowWorkspace(run.metadata);
|
|
1411
|
-
const prompt = [
|
|
1412
|
-
|
|
1413
|
-
.
|
|
1488
|
+
const prompt = [
|
|
1489
|
+
'Original user request (primary task; answer this directly even if the workspace is empty):',
|
|
1490
|
+
run.input?.trim() || '(No original user request was provided.)',
|
|
1491
|
+
inputContext
|
|
1492
|
+
? `Upstream workflow context from prior agents:\n${inputContext}`
|
|
1493
|
+
: '',
|
|
1494
|
+
`Current workflow step instructions:\n${node.prompt}`,
|
|
1495
|
+
workspaceContextPrompt(workspaceTarget),
|
|
1496
|
+
].filter(Boolean).join('\n\n');
|
|
1414
1497
|
const settings = getMetadataRecord(run.metadata, 'settings');
|
|
1415
1498
|
const projectPath = workspaceTarget.projectPath;
|
|
1416
1499
|
const isolation = readIsolation(settings.isolation) ?? node.isolation ?? 'host';
|
|
1417
1500
|
const keepAfterCompletion = readBoolean(settings.keepWorkspace) ?? true;
|
|
1418
1501
|
const baseRef = readString(settings.baseRef) ?? 'HEAD';
|
|
1419
1502
|
const effectivePermissionMode = resolveNodePermissionMode(node, workspaceTarget);
|
|
1503
|
+
const effectiveModel = await resolveWorkflowModel(node.adapterId, node.model);
|
|
1504
|
+
if (effectiveModel !== node.model) {
|
|
1505
|
+
nodeRun.model = effectiveModel;
|
|
1506
|
+
const modelFallbackEvents = Array.isArray(run.metadata?.modelFallbackEvents)
|
|
1507
|
+
? run.metadata.modelFallbackEvents
|
|
1508
|
+
: [];
|
|
1509
|
+
run.metadata = {
|
|
1510
|
+
...run.metadata,
|
|
1511
|
+
modelFallbackEvents: [
|
|
1512
|
+
...modelFallbackEvents,
|
|
1513
|
+
{
|
|
1514
|
+
nodeId: node.id,
|
|
1515
|
+
adapterId: node.adapterId,
|
|
1516
|
+
requestedModel: node.model,
|
|
1517
|
+
effectiveModel,
|
|
1518
|
+
changedAt: Date.now(),
|
|
1519
|
+
},
|
|
1520
|
+
],
|
|
1521
|
+
};
|
|
1522
|
+
workflowStore.setRun(run);
|
|
1523
|
+
}
|
|
1420
1524
|
let body: { id?: string; error?: { message?: string } };
|
|
1421
1525
|
try {
|
|
1422
1526
|
const submit = await fetch(`${localA2ABaseUrl()}/tasks`, {
|
|
@@ -1436,7 +1540,7 @@ class WorkflowRunner {
|
|
|
1436
1540
|
agentInstanceId: node.agentInstanceId,
|
|
1437
1541
|
agentLabel: node.agentLabel,
|
|
1438
1542
|
assignment: node.assignment,
|
|
1439
|
-
model:
|
|
1543
|
+
model: effectiveModel,
|
|
1440
1544
|
permissionMode: effectivePermissionMode,
|
|
1441
1545
|
toolsSettings: node.toolsSettings,
|
|
1442
1546
|
projectPath,
|
|
@@ -208,6 +208,120 @@ function buildManagedPackageCommand(command, runtimeStatus) {
|
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
function packageBinCandidates(binName) {
|
|
212
|
+
return process.platform === 'win32'
|
|
213
|
+
? [binName, `${binName}.cmd`, `${binName}.ps1`, `${binName}.exe`]
|
|
214
|
+
: [binName];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function expectedPackageBin(command) {
|
|
218
|
+
if (command.framework === 'Vite' || command.id === 'npm-dev-vite') return 'vite';
|
|
219
|
+
if (command.framework === 'Next.js' || command.id === 'npm-dev-next') return 'next';
|
|
220
|
+
if (command.framework === 'Nuxt' || command.id === 'npm-dev-nuxt') return 'nuxt';
|
|
221
|
+
if (command.framework === 'Astro' || command.id === 'npm-dev-astro') return 'astro';
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function packageDependenciesReady(projectPath, command) {
|
|
226
|
+
if (!command?.scriptName && !command?.packageManager) return true;
|
|
227
|
+
if (!(await dirExists(path.join(projectPath, 'node_modules')))) return false;
|
|
228
|
+
|
|
229
|
+
const binName = expectedPackageBin(command);
|
|
230
|
+
if (!binName) return true;
|
|
231
|
+
|
|
232
|
+
const binDir = path.join(projectPath, 'node_modules', '.bin');
|
|
233
|
+
for (const candidate of packageBinCandidates(binName)) {
|
|
234
|
+
if (await fileExists(path.join(binDir, candidate))) return true;
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function packageInstallInvocation(command) {
|
|
240
|
+
if (!command || command.packageManager !== 'npm') return null;
|
|
241
|
+
|
|
242
|
+
if (
|
|
243
|
+
command.managedRuntime?.id === 'npm'
|
|
244
|
+
&& command.args?.[0]
|
|
245
|
+
&& String(command.args[0]).endsWith('npm-cli.js')
|
|
246
|
+
) {
|
|
247
|
+
return {
|
|
248
|
+
command: command.command,
|
|
249
|
+
args: [command.args[0], 'install', '--no-audit', '--no-fund'],
|
|
250
|
+
displayCommand: 'npm install --no-audit --no-fund',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (command.command === 'npm' || path.basename(command.command || '').startsWith('npm')) {
|
|
255
|
+
return {
|
|
256
|
+
command: command.command,
|
|
257
|
+
args: ['install', '--no-audit', '--no-fund'],
|
|
258
|
+
displayCommand: 'npm install --no-audit --no-fund',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function runPrepProcess(command, args, options = {}) {
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
let settled = false;
|
|
268
|
+
const child = spawn(command, args, {
|
|
269
|
+
cwd: options.cwd,
|
|
270
|
+
env: options.env,
|
|
271
|
+
shell: shouldUseShell({ command }),
|
|
272
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
273
|
+
windowsHide: true,
|
|
274
|
+
});
|
|
275
|
+
const timeoutMs = Number.parseInt(process.env.PIXCODE_LIVE_VIEW_INSTALL_TIMEOUT_MS || '', 10) || 300000;
|
|
276
|
+
const finish = (callback, value) => {
|
|
277
|
+
if (settled) return;
|
|
278
|
+
settled = true;
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
callback(value);
|
|
281
|
+
};
|
|
282
|
+
const timer = setTimeout(() => {
|
|
283
|
+
try {
|
|
284
|
+
child.kill();
|
|
285
|
+
} catch {
|
|
286
|
+
// Process may already be gone.
|
|
287
|
+
}
|
|
288
|
+
finish(reject, new Error(`Dependency install timed out after ${Math.round(timeoutMs / 1000)}s.`));
|
|
289
|
+
}, timeoutMs);
|
|
290
|
+
|
|
291
|
+
child.stdout.on('data', (chunk) => {
|
|
292
|
+
chunk.toString().split(/\r?\n/).forEach((line) => options.onLog?.(line));
|
|
293
|
+
});
|
|
294
|
+
child.stderr.on('data', (chunk) => {
|
|
295
|
+
chunk.toString().split(/\r?\n/).forEach((line) => options.onLog?.(line));
|
|
296
|
+
});
|
|
297
|
+
child.on('error', (error) => finish(reject, error));
|
|
298
|
+
child.on('exit', (code, signal) => {
|
|
299
|
+
if (code === 0) {
|
|
300
|
+
finish(resolve);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
finish(reject, new Error(`Dependency install exited with ${signal || `code ${code}`}.`));
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function preparePackageDependencies(projectPath, command, env = process.env, onLog = () => {}) {
|
|
309
|
+
if (!command || command.packageManager !== 'npm') return false;
|
|
310
|
+
if (await packageDependenciesReady(projectPath, command)) return false;
|
|
311
|
+
|
|
312
|
+
const install = packageInstallInvocation(command);
|
|
313
|
+
if (!install) return false;
|
|
314
|
+
|
|
315
|
+
onLog(`Installing project dependencies: ${install.displayCommand}`);
|
|
316
|
+
await runPrepProcess(install.command, install.args, {
|
|
317
|
+
cwd: projectPath,
|
|
318
|
+
env,
|
|
319
|
+
onLog,
|
|
320
|
+
});
|
|
321
|
+
onLog('Project dependencies are ready.');
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
|
|
211
325
|
function buildManagedPhpCommand(runtimeStatus) {
|
|
212
326
|
const executable = runtimeStatus?.executablePath || 'frankenphp';
|
|
213
327
|
return {
|
|
@@ -718,6 +832,20 @@ export async function startLiveView(projectName, projectPath, options = {}) {
|
|
|
718
832
|
BROWSER: 'none',
|
|
719
833
|
NEXT_TELEMETRY_DISABLED: '1',
|
|
720
834
|
};
|
|
835
|
+
|
|
836
|
+
sessionsByProject.set(projectName, session);
|
|
837
|
+
sessionsByShareId.set(shareId, session);
|
|
838
|
+
|
|
839
|
+
try {
|
|
840
|
+
await preparePackageDependencies(projectPath, command, env, (line) => appendLog(session, line));
|
|
841
|
+
} catch (error) {
|
|
842
|
+
session.status = 'error';
|
|
843
|
+
session.stoppedAt = new Date().toISOString();
|
|
844
|
+
session.error = error instanceof Error ? error.message : String(error);
|
|
845
|
+
appendLog(session, session.error);
|
|
846
|
+
return publicSession(session);
|
|
847
|
+
}
|
|
848
|
+
|
|
721
849
|
const child = spawn(command.command, command.args, {
|
|
722
850
|
cwd: projectPath,
|
|
723
851
|
env,
|
|
@@ -726,8 +854,6 @@ export async function startLiveView(projectName, projectPath, options = {}) {
|
|
|
726
854
|
});
|
|
727
855
|
|
|
728
856
|
session.child = child;
|
|
729
|
-
sessionsByProject.set(projectName, session);
|
|
730
|
-
sessionsByShareId.set(shareId, session);
|
|
731
857
|
|
|
732
858
|
child.stdout.on('data', (chunk) => {
|
|
733
859
|
chunk.toString().split(/\r?\n/).forEach((line) => appendLog(session, line));
|