@pixelbyte-software/pixcode 1.41.2 → 1.41.4
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-BC8CXTJj.css +32 -0
- package/dist/assets/{index-BC6Knu5B.js → index-CIEN0bZ-.js} +169 -169
- package/dist/index.html +2 -2
- package/dist-server/server/modules/orchestration/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-trace.js +236 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-trace.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +12 -0
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -1
- package/dist-server/server/services/live-view.js +19 -94
- package/dist-server/server/services/live-view.js.map +1 -1
- package/dist-server/server/services/runtime-manager.js +312 -0
- package/dist-server/server/services/runtime-manager.js.map +1 -0
- package/package.json +1 -1
- package/scripts/smoke/live-view-diagnostics.mjs +2 -2
- package/scripts/smoke/live-view-integration.mjs +58 -46
- package/scripts/smoke/runtime-manager.mjs +99 -0
- package/scripts/smoke/workflow-trace-timeline.mjs +46 -0
- package/server/modules/orchestration/index.ts +1 -0
- package/server/modules/orchestration/workflows/workflow-runner.ts +1 -0
- package/server/modules/orchestration/workflows/workflow-trace.ts +270 -0
- package/server/modules/orchestration/workflows/workflow.routes.ts +14 -0
- package/server/modules/orchestration/workflows/workflow.types.ts +21 -0
- package/server/services/live-view.js +20 -101
- package/server/services/runtime-manager.js +323 -0
- package/dist/assets/index-LZgOC7Q_.css +0 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixelbyte-software/pixcode",
|
|
3
|
-
"version": "1.41.
|
|
3
|
+
"version": "1.41.4",
|
|
4
4
|
"description": "Self-hosted AI coding agent control room for Claude Code, Cursor CLI, OpenAI Codex, Gemini CLI, Qwen Code, and OpenCode with chat, files, shell, Git, orchestration, API keys, Telegram, MCP, plugins, themes, and desktop/server deployment.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist-server/server/index.js",
|
|
@@ -39,8 +39,8 @@ assert.match(payload.errorDetail, /spawn php ENOENT/);
|
|
|
39
39
|
assert.ok(payload.diagnostics.command.includes('php -S'), 'Payload should include the attempted PHP command.');
|
|
40
40
|
assert.ok(payload.diagnostics.logs.some((line) => line.includes('spawn php ENOENT')), 'Payload should include process logs.');
|
|
41
41
|
assert.ok(
|
|
42
|
-
payload.suggestions.some((suggestion) => /php/i.test(suggestion) && /
|
|
43
|
-
'PHP failures should
|
|
42
|
+
payload.suggestions.some((suggestion) => /php/i.test(suggestion) && /local PHP runtime/i.test(suggestion)),
|
|
43
|
+
'PHP failures should explain that Pixcode can prepare a local PHP runtime.',
|
|
44
44
|
);
|
|
45
45
|
assert.notEqual(payload.error, 'Live View session not found.', 'Existing failed sessions must not be hidden as missing sessions.');
|
|
46
46
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { access, chmod, mkdtemp, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import net from 'node:net';
|
|
2
3
|
import { tmpdir } from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -10,6 +11,13 @@ const read = async (relativePath) => {
|
|
|
10
11
|
return readFile(path.join(repoRoot, relativePath), 'utf8');
|
|
11
12
|
};
|
|
12
13
|
const fileExists = async (filePath) => access(filePath).then(() => true, () => false);
|
|
14
|
+
const canBindLoopback = () => new Promise((resolve) => {
|
|
15
|
+
const server = net.createServer();
|
|
16
|
+
server.once('error', () => resolve(false));
|
|
17
|
+
server.listen(0, '127.0.0.1', () => {
|
|
18
|
+
server.close(() => resolve(true));
|
|
19
|
+
});
|
|
20
|
+
});
|
|
13
21
|
|
|
14
22
|
const appTypes = await read('src/types/app.ts');
|
|
15
23
|
assert.ok(
|
|
@@ -386,53 +394,57 @@ const brokenStatus = await getManagedRuntimeStatus('frankenphp', {
|
|
|
386
394
|
});
|
|
387
395
|
assert.equal(brokenStatus.status, 'missing', 'Broken managed FrankenPHP manifests should be treated as missing so Pixcode can reinstall them.');
|
|
388
396
|
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
'
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
process.env.PIXCODE_MANAGED_RUNTIMES_HOME
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
397
|
+
if (await canBindLoopback()) {
|
|
398
|
+
const phpRuntimeEnvHome = path.join(workspace, 'php-runtime-env');
|
|
399
|
+
const phpRuntimeCurrent = path.join(phpRuntimeEnvHome, 'frankenphp', 'current');
|
|
400
|
+
await mkdir(phpRuntimeCurrent, { recursive: true });
|
|
401
|
+
const phpRuntimeExecutable = path.join(phpRuntimeCurrent, process.platform === 'win32' ? 'frankenphp.cmd' : 'frankenphp');
|
|
402
|
+
await writeFile(phpRuntimeExecutable, [
|
|
403
|
+
'#!/usr/bin/env node',
|
|
404
|
+
'const http = require("node:http");',
|
|
405
|
+
'const path = require("node:path");',
|
|
406
|
+
'const runtimeDir = __dirname;',
|
|
407
|
+
'if (process.argv.includes("version")) process.exit(0);',
|
|
408
|
+
'const pathValue = process.env.Path || process.env.PATH || "";',
|
|
409
|
+
'if (!pathValue.split(path.delimiter).includes(runtimeDir)) {',
|
|
410
|
+
' console.error("runtime path missing from PATH");',
|
|
411
|
+
' process.exit(1);',
|
|
412
|
+
'}',
|
|
413
|
+
'const port = Number(process.env.PORT || 0);',
|
|
414
|
+
'http.createServer((req, res) => res.end("php runtime ok")).listen(port, "127.0.0.1");',
|
|
415
|
+
'',
|
|
416
|
+
].join('\n'));
|
|
417
|
+
if (process.platform !== 'win32') {
|
|
418
|
+
await chmod(phpRuntimeExecutable, 0o755);
|
|
419
|
+
}
|
|
420
|
+
await writeFile(path.join(phpRuntimeEnvHome, 'frankenphp', 'pixcode-runtime.json'), JSON.stringify({
|
|
421
|
+
id: 'frankenphp',
|
|
422
|
+
label: 'Pixcode PHP runtime',
|
|
423
|
+
executablePath: phpRuntimeExecutable,
|
|
424
|
+
version: 'path-env-smoke',
|
|
425
|
+
}, null, 2));
|
|
426
|
+
const previousRuntimeHome = process.env.PIXCODE_MANAGED_RUNTIMES_HOME;
|
|
427
|
+
process.env.PIXCODE_MANAGED_RUNTIMES_HOME = phpRuntimeEnvHome;
|
|
428
|
+
try {
|
|
429
|
+
const phpRuntimeSession = await startLiveView('php-runtime-env-smoke', phpProject);
|
|
430
|
+
assert.equal(phpRuntimeSession.status, 'running', 'Managed PHP Live View should start with the runtime directory on PATH.');
|
|
431
|
+
await stopLiveView('php-runtime-env-smoke');
|
|
432
|
+
} finally {
|
|
433
|
+
if (previousRuntimeHome === undefined) {
|
|
434
|
+
delete process.env.PIXCODE_MANAGED_RUNTIMES_HOME;
|
|
435
|
+
} else {
|
|
436
|
+
process.env.PIXCODE_MANAGED_RUNTIMES_HOME = previousRuntimeHome;
|
|
437
|
+
}
|
|
428
438
|
}
|
|
429
|
-
}
|
|
430
439
|
|
|
431
|
-
const staticSession = await startLiveView('static-smoke', staticProject);
|
|
432
|
-
assert.equal(staticSession.status, 'running', 'Static Live View should start without a child process.');
|
|
433
|
-
assert.match(staticSession.sharePath, /^\/live\/[a-f0-9]{24}\/$/, 'Live View should expose a random public share path.');
|
|
434
|
-
const staticState = await getLiveViewState('static-smoke', staticProject);
|
|
435
|
-
assert.equal(staticState.session?.shareId, staticSession.shareId, 'Live View state should retain the active share session.');
|
|
436
|
-
await stopLiveView('static-smoke');
|
|
440
|
+
const staticSession = await startLiveView('static-smoke', staticProject);
|
|
441
|
+
assert.equal(staticSession.status, 'running', 'Static Live View should start without a child process.');
|
|
442
|
+
assert.match(staticSession.sharePath, /^\/live\/[a-f0-9]{24}\/$/, 'Live View should expose a random public share path.');
|
|
443
|
+
const staticState = await getLiveViewState('static-smoke', staticProject);
|
|
444
|
+
assert.equal(staticState.session?.shareId, staticSession.shareId, 'Live View state should retain the active share session.');
|
|
445
|
+
await stopLiveView('static-smoke');
|
|
446
|
+
} else {
|
|
447
|
+
console.warn('Skipping Live View launch smoke because this sandbox cannot bind 127.0.0.1.');
|
|
448
|
+
}
|
|
437
449
|
|
|
438
450
|
console.log('live view integration smoke passed');
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const root = process.cwd();
|
|
8
|
+
|
|
9
|
+
function read(relativePath) {
|
|
10
|
+
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const managerSource = read('server/services/runtime-manager.js');
|
|
14
|
+
assert.match(managerSource, /RUNTIME_DEFINITIONS/, 'Runtime manager should define a central runtime registry.');
|
|
15
|
+
assert.match(managerSource, /node/, 'Runtime manager should include Node.js.');
|
|
16
|
+
assert.match(managerSource, /php/, 'Runtime manager should include PHP.');
|
|
17
|
+
assert.match(managerSource, /python/, 'Runtime manager should include Python.');
|
|
18
|
+
assert.match(managerSource, /go/, 'Runtime manager should include Go.');
|
|
19
|
+
assert.match(managerSource, /java/, 'Runtime manager should include Java.');
|
|
20
|
+
assert.match(managerSource, /rust/, 'Runtime manager should include Rust.');
|
|
21
|
+
assert.match(managerSource, /discoverRuntime/, 'Runtime manager should expose runtime discovery.');
|
|
22
|
+
assert.match(managerSource, /resolveLiveViewRuntime/, 'Runtime manager should expose Live View runtime resolution.');
|
|
23
|
+
|
|
24
|
+
const liveViewSource = read('server/services/live-view.js');
|
|
25
|
+
assert.match(liveViewSource, /resolveLiveViewRuntime/, 'Live View should route runtime checks through the runtime manager.');
|
|
26
|
+
assert.match(liveViewSource, /runtime:\s*session\.runtime/, 'Live View public session payload should expose runtime diagnostics.');
|
|
27
|
+
assert.ok(
|
|
28
|
+
liveViewSource.includes('target.runtime'),
|
|
29
|
+
'Live View start should keep the runtime manager result on the session.',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
discoverRuntime,
|
|
34
|
+
resolveLiveViewRuntime,
|
|
35
|
+
runtimeManager,
|
|
36
|
+
} = await import('../../server/services/runtime-manager.js');
|
|
37
|
+
|
|
38
|
+
assert.equal(typeof runtimeManager.discover, 'function', 'Runtime manager should expose a discover method.');
|
|
39
|
+
|
|
40
|
+
const nodeRuntime = await discoverRuntime('node');
|
|
41
|
+
assert.equal(nodeRuntime.id, 'node');
|
|
42
|
+
assert.equal(nodeRuntime.status, 'available', 'The current Node runtime should be detected as available.');
|
|
43
|
+
assert.ok(nodeRuntime.path, 'Node runtime should include an executable path.');
|
|
44
|
+
assert.match(nodeRuntime.version || '', /\d+\.\d+\.\d+/, 'Node runtime should include a version.');
|
|
45
|
+
|
|
46
|
+
const missingPython = await discoverRuntime('python', {
|
|
47
|
+
strictPath: true,
|
|
48
|
+
env: {
|
|
49
|
+
...process.env,
|
|
50
|
+
PATH: '',
|
|
51
|
+
Path: '',
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
assert.equal(missingPython.status, 'missing', 'Missing Python should produce a missing runtime status.');
|
|
55
|
+
assert.match(missingPython.diagnostic.message, /Python/i, 'Missing Python diagnostics should name the runtime.');
|
|
56
|
+
assert.match(missingPython.diagnostic.action, /python/i, 'Missing Python diagnostics should include an actionable install command.');
|
|
57
|
+
|
|
58
|
+
const nodeLiveRuntime = await resolveLiveViewRuntime({
|
|
59
|
+
id: 'npm-dev-vite',
|
|
60
|
+
label: 'Vite dev server',
|
|
61
|
+
framework: 'Vite',
|
|
62
|
+
command: 'npm',
|
|
63
|
+
args: ['run', 'dev'],
|
|
64
|
+
displayCommand: 'npm run dev',
|
|
65
|
+
packageManager: 'npm',
|
|
66
|
+
}, {
|
|
67
|
+
env: {
|
|
68
|
+
...process.env,
|
|
69
|
+
PATH: '',
|
|
70
|
+
Path: '',
|
|
71
|
+
},
|
|
72
|
+
preferManaged: true,
|
|
73
|
+
});
|
|
74
|
+
assert.equal(nodeLiveRuntime.runtime.id, 'node', 'JavaScript Live View commands should resolve to the Node runtime.');
|
|
75
|
+
assert.equal(nodeLiveRuntime.managedRuntime?.id, 'npm', 'JavaScript Live View commands should keep the managed npm hook.');
|
|
76
|
+
assert.equal(nodeLiveRuntime.available, true, 'Managed npm should keep JavaScript projects runnable when npm is missing.');
|
|
77
|
+
assert.match(nodeLiveRuntime.reason, /Node package runner/i, 'Managed npm diagnostics should explain the package runner.');
|
|
78
|
+
|
|
79
|
+
const phpLiveRuntime = await resolveLiveViewRuntime({
|
|
80
|
+
id: 'php-built-in',
|
|
81
|
+
label: 'PHP built-in server',
|
|
82
|
+
framework: 'PHP',
|
|
83
|
+
command: 'php',
|
|
84
|
+
args: ['-S', '127.0.0.1:$PORT', '-t', '.'],
|
|
85
|
+
displayCommand: 'php -S 127.0.0.1:$PORT -t .',
|
|
86
|
+
}, {
|
|
87
|
+
env: {
|
|
88
|
+
...process.env,
|
|
89
|
+
PATH: '',
|
|
90
|
+
Path: '',
|
|
91
|
+
},
|
|
92
|
+
preferManaged: true,
|
|
93
|
+
});
|
|
94
|
+
assert.equal(phpLiveRuntime.runtime.id, 'php', 'PHP Live View commands should resolve to the PHP runtime.');
|
|
95
|
+
assert.equal(phpLiveRuntime.managedRuntime?.id, 'frankenphp', 'PHP Live View commands should keep the managed FrankenPHP hook.');
|
|
96
|
+
assert.equal(phpLiveRuntime.available, true, 'Managed PHP should keep PHP projects runnable when php is missing.');
|
|
97
|
+
assert.match(phpLiveRuntime.reason, /PHP runtime/i, 'Managed PHP diagnostics should explain the Pixcode runtime.');
|
|
98
|
+
|
|
99
|
+
console.log('runtime manager smoke passed');
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
|
|
8
|
+
function read(relativePath) {
|
|
9
|
+
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function assert(condition, message) {
|
|
13
|
+
if (!condition) {
|
|
14
|
+
throw new Error(message);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const types = read('server/modules/orchestration/workflows/workflow.types.ts');
|
|
19
|
+
assert(types.includes('WorkflowTraceEvent'), 'Workflow trace event type is missing.');
|
|
20
|
+
assert(types.includes("type: 'run' | 'node' | 'provider' | 'message' | 'artifact' | 'file' | 'error'"), 'Trace event type taxonomy is missing.');
|
|
21
|
+
assert(types.includes("severity: 'info' | 'warning' | 'error'"), 'Trace event severity taxonomy is missing.');
|
|
22
|
+
|
|
23
|
+
const traceService = read('server/modules/orchestration/workflows/workflow-trace.ts');
|
|
24
|
+
assert(traceService.includes('export function buildWorkflowTrace'), 'Workflow trace builder is missing.');
|
|
25
|
+
assert(traceService.includes('redactTraceText'), 'Trace text redaction helper is missing.');
|
|
26
|
+
assert(traceService.includes('file-diff'), 'Trace builder must surface file edit artifacts.');
|
|
27
|
+
assert(traceService.includes('durationMs'), 'Trace builder must include event durations.');
|
|
28
|
+
assert(traceService.includes('workflow.trace.runStarted'), 'Trace builder must emit stable trace title keys.');
|
|
29
|
+
|
|
30
|
+
const routes = read('server/modules/orchestration/workflows/workflow.routes.ts');
|
|
31
|
+
assert(routes.includes("'/workflows/runs/:runId/trace'"), 'Workflow trace API endpoint is missing.');
|
|
32
|
+
assert(routes.includes('buildWorkflowTrace(run)'), 'Workflow trace route must use the shared trace builder.');
|
|
33
|
+
|
|
34
|
+
const panel = read('src/components/orchestration/workflows/WorkflowRunPanel.tsx');
|
|
35
|
+
assert(panel.includes('WorkflowTraceEvent'), 'Workflow run panel must type trace events.');
|
|
36
|
+
assert(panel.includes('loadTrace'), 'Workflow run panel must load trace events from the API.');
|
|
37
|
+
assert(panel.includes('traceFilters'), 'Workflow run panel must expose trace filters.');
|
|
38
|
+
assert(panel.includes('traceTimelineId'), 'Workflow trace timeline tab is missing.');
|
|
39
|
+
assert(panel.includes('orchestration.traceTimeline'), 'Workflow run panel must render the trace timeline label.');
|
|
40
|
+
|
|
41
|
+
const en = read('src/i18n/locales/en/common.json');
|
|
42
|
+
const tr = read('src/i18n/locales/tr/common.json');
|
|
43
|
+
assert(en.includes('"traceTimeline"'), 'English trace timeline translation is missing.');
|
|
44
|
+
assert(tr.includes('"traceTimeline"'), 'Turkish trace timeline translation is missing.');
|
|
45
|
+
|
|
46
|
+
console.log(JSON.stringify({ ok: true, checked: 'workflow trace timeline' }, null, 2));
|
|
@@ -1086,6 +1086,7 @@ function nodeRunFromNode(node: WorkflowNode): WorkflowNodeRun {
|
|
|
1086
1086
|
agentInstanceId: node.agentInstanceId,
|
|
1087
1087
|
agentLabel: node.agentLabel,
|
|
1088
1088
|
assignment: node.assignment,
|
|
1089
|
+
promptPreview: node.prompt,
|
|
1089
1090
|
model: node.model,
|
|
1090
1091
|
permissionMode: node.permissionMode,
|
|
1091
1092
|
timeoutMs: node.timeoutMs,
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
WorkflowNodeRun,
|
|
5
|
+
WorkflowRun,
|
|
6
|
+
WorkflowTraceEvent,
|
|
7
|
+
} from '@/modules/orchestration/workflows/workflow.types.js';
|
|
8
|
+
|
|
9
|
+
const MAX_TRACE_TEXT_CHARS = 2_400;
|
|
10
|
+
const TERMINAL_STATES = new Set(['completed', 'failed', 'canceled', 'skipped']);
|
|
11
|
+
|
|
12
|
+
function traceId(parts: Array<string | number | undefined>): string {
|
|
13
|
+
return parts.filter((part) => part !== undefined && part !== '').join(':');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function durationMs(startedAt?: number, finishedAt?: number): number | undefined {
|
|
17
|
+
if (!startedAt || !finishedAt) return undefined;
|
|
18
|
+
return Math.max(0, finishedAt - startedAt);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readString(value: unknown): string | undefined {
|
|
22
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function redactionValues(run: WorkflowRun): string[] {
|
|
26
|
+
const metadata = run.metadata ?? {};
|
|
27
|
+
const workspaceTarget = metadata.workspaceTarget && typeof metadata.workspaceTarget === 'object'
|
|
28
|
+
? metadata.workspaceTarget as Record<string, unknown>
|
|
29
|
+
: {};
|
|
30
|
+
|
|
31
|
+
return [
|
|
32
|
+
os.homedir(),
|
|
33
|
+
readString(metadata.projectPath),
|
|
34
|
+
readString(metadata.selectedProjectPath),
|
|
35
|
+
readString(workspaceTarget.path),
|
|
36
|
+
readString(workspaceTarget.projectPath),
|
|
37
|
+
readString(workspaceTarget.selectedProjectPath),
|
|
38
|
+
].filter((value): value is string => Boolean(value && value.length > 2));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function escapeRegExp(value: string): string {
|
|
42
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function redactTraceText(value: string | undefined, run: WorkflowRun, maxLength = MAX_TRACE_TEXT_CHARS): string | undefined {
|
|
46
|
+
if (!value?.trim()) return undefined;
|
|
47
|
+
|
|
48
|
+
let text = value;
|
|
49
|
+
for (const secret of redactionValues(run)) {
|
|
50
|
+
text = text.replace(new RegExp(escapeRegExp(secret), 'g'), '[workspace]');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
text = text
|
|
54
|
+
.replace(/\b(?:sk|ghp|github_pat|glpat|npm)_[A-Za-z0-9_=-]{12,}\b/gu, '[redacted-token]')
|
|
55
|
+
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/gu, '[redacted-email]')
|
|
56
|
+
.trim();
|
|
57
|
+
|
|
58
|
+
if (text.length <= maxLength) return text;
|
|
59
|
+
return `${text.slice(0, maxLength - 32).trimEnd()}\n...[trace truncated]`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function eventBase(
|
|
63
|
+
node: WorkflowNodeRun | undefined,
|
|
64
|
+
): Pick<WorkflowTraceEvent, 'actor' | 'adapterId' | 'agentInstanceId' | 'agentLabel' | 'model' | 'nodeId'> {
|
|
65
|
+
if (!node) {
|
|
66
|
+
return { actor: 'Pixcode' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
actor: node.agentLabel || node.adapterId || node.nodeId,
|
|
71
|
+
nodeId: node.nodeId,
|
|
72
|
+
adapterId: node.adapterId,
|
|
73
|
+
agentInstanceId: node.agentInstanceId,
|
|
74
|
+
agentLabel: node.agentLabel,
|
|
75
|
+
model: node.model,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function pushEvent(
|
|
80
|
+
events: WorkflowTraceEvent[],
|
|
81
|
+
event: WorkflowTraceEvent,
|
|
82
|
+
): void {
|
|
83
|
+
events.push(event);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function artifactTitleKey(type: string): string {
|
|
87
|
+
if (type === 'file-diff') return 'workflow.trace.fileChanged';
|
|
88
|
+
if (type === 'preview-url') return 'workflow.trace.previewReady';
|
|
89
|
+
if (type === 'command-output') return 'workflow.trace.commandOutput';
|
|
90
|
+
return 'workflow.trace.artifact';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function artifactType(type: string): WorkflowTraceEvent['type'] {
|
|
94
|
+
return type === 'file-diff' ? 'file' : 'artifact';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function artifactTitle(type: string): string {
|
|
98
|
+
if (type === 'file-diff') return 'File changes captured';
|
|
99
|
+
if (type === 'preview-url') return 'Preview output captured';
|
|
100
|
+
if (type === 'command-output') return 'Command output captured';
|
|
101
|
+
return 'Artifact captured';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function artifactSummary(
|
|
105
|
+
artifact: NonNullable<WorkflowNodeRun['artifacts']>[number],
|
|
106
|
+
run: WorkflowRun,
|
|
107
|
+
): string | undefined {
|
|
108
|
+
if (artifact.text?.trim()) {
|
|
109
|
+
return redactTraceText(artifact.text, run);
|
|
110
|
+
}
|
|
111
|
+
if (artifact.data && Object.keys(artifact.data).length > 0) {
|
|
112
|
+
return redactTraceText(JSON.stringify(artifact.data, null, 2), run);
|
|
113
|
+
}
|
|
114
|
+
return artifact.type;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function nodeTimestamp(run: WorkflowRun, node: WorkflowNodeRun, index: number): number {
|
|
118
|
+
return node.startedAt ?? run.startedAt + index;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function buildWorkflowTrace(run: WorkflowRun): WorkflowTraceEvent[] {
|
|
122
|
+
const events: WorkflowTraceEvent[] = [];
|
|
123
|
+
|
|
124
|
+
pushEvent(events, {
|
|
125
|
+
id: traceId([run.id, 'run-started']),
|
|
126
|
+
type: 'run',
|
|
127
|
+
severity: 'info',
|
|
128
|
+
status: run.status,
|
|
129
|
+
timestamp: run.startedAt,
|
|
130
|
+
durationMs: durationMs(run.startedAt, run.finishedAt),
|
|
131
|
+
actor: 'Pixcode',
|
|
132
|
+
title: 'Workflow run started',
|
|
133
|
+
titleKey: 'workflow.trace.runStarted',
|
|
134
|
+
summary: redactTraceText(run.input, run),
|
|
135
|
+
metadata: {
|
|
136
|
+
workflowId: run.workflowId,
|
|
137
|
+
contextId: run.contextId,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
run.nodeRuns.forEach((node, index) => {
|
|
142
|
+
const base = eventBase(node);
|
|
143
|
+
const timestamp = nodeTimestamp(run, node, index);
|
|
144
|
+
const nodeDuration = durationMs(node.startedAt, node.finishedAt);
|
|
145
|
+
|
|
146
|
+
pushEvent(events, {
|
|
147
|
+
id: traceId([run.id, node.nodeId, 'node']),
|
|
148
|
+
type: 'node',
|
|
149
|
+
severity: node.status === 'failed' ? 'error' : 'info',
|
|
150
|
+
status: node.status,
|
|
151
|
+
timestamp,
|
|
152
|
+
durationMs: nodeDuration,
|
|
153
|
+
...base,
|
|
154
|
+
title: TERMINAL_STATES.has(node.status) ? 'Workflow step finished' : 'Workflow step started',
|
|
155
|
+
titleKey: TERMINAL_STATES.has(node.status) ? 'workflow.trace.nodeFinished' : 'workflow.trace.nodeStarted',
|
|
156
|
+
summary: redactTraceText(node.assignment, run),
|
|
157
|
+
metadata: {
|
|
158
|
+
stage: node.stage,
|
|
159
|
+
internal: node.internal,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (node.promptPreview) {
|
|
164
|
+
pushEvent(events, {
|
|
165
|
+
id: traceId([run.id, node.nodeId, 'prompt']),
|
|
166
|
+
type: 'message',
|
|
167
|
+
severity: 'info',
|
|
168
|
+
status: node.status,
|
|
169
|
+
timestamp: timestamp + 1,
|
|
170
|
+
...base,
|
|
171
|
+
title: 'Prompt prepared',
|
|
172
|
+
titleKey: 'workflow.trace.prompt',
|
|
173
|
+
summary: redactTraceText(node.promptPreview, run),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (node.adapterId || node.model) {
|
|
178
|
+
pushEvent(events, {
|
|
179
|
+
id: traceId([run.id, node.nodeId, 'provider']),
|
|
180
|
+
type: 'provider',
|
|
181
|
+
severity: node.status === 'failed' ? 'error' : 'info',
|
|
182
|
+
status: node.a2aTaskId ? 'submitted' : node.status,
|
|
183
|
+
timestamp: timestamp + 2,
|
|
184
|
+
durationMs: nodeDuration,
|
|
185
|
+
...base,
|
|
186
|
+
title: 'Provider call',
|
|
187
|
+
titleKey: 'workflow.trace.providerCall',
|
|
188
|
+
summary: [node.adapterId, node.model].filter(Boolean).join(' / '),
|
|
189
|
+
metadata: {
|
|
190
|
+
a2aTaskId: node.a2aTaskId,
|
|
191
|
+
permissionMode: node.permissionMode,
|
|
192
|
+
timeoutMs: node.timeoutMs,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
(node.messages ?? [])
|
|
198
|
+
.filter((message) => message.role !== 'user' && message.text.trim())
|
|
199
|
+
.forEach((message, messageIndex) => {
|
|
200
|
+
pushEvent(events, {
|
|
201
|
+
id: traceId([run.id, node.nodeId, 'message', messageIndex]),
|
|
202
|
+
type: 'message',
|
|
203
|
+
severity: 'info',
|
|
204
|
+
status: node.status,
|
|
205
|
+
timestamp: message.createdAt ?? timestamp + 10 + messageIndex,
|
|
206
|
+
...base,
|
|
207
|
+
title: 'Agent message',
|
|
208
|
+
titleKey: 'workflow.trace.agentMessage',
|
|
209
|
+
summary: redactTraceText(message.text, run),
|
|
210
|
+
metadata: {
|
|
211
|
+
role: message.role,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
(node.artifacts ?? []).forEach((artifact, artifactIndex) => {
|
|
217
|
+
pushEvent(events, {
|
|
218
|
+
id: traceId([run.id, node.nodeId, 'artifact', artifactIndex]),
|
|
219
|
+
type: artifactType(artifact.type),
|
|
220
|
+
severity: 'info',
|
|
221
|
+
status: node.status,
|
|
222
|
+
timestamp: node.finishedAt ?? timestamp + 20 + artifactIndex,
|
|
223
|
+
...base,
|
|
224
|
+
title: artifactTitle(artifact.type),
|
|
225
|
+
titleKey: artifactTitleKey(artifact.type),
|
|
226
|
+
summary: artifactSummary(artifact, run),
|
|
227
|
+
metadata: {
|
|
228
|
+
artifactType: artifact.type,
|
|
229
|
+
artifactMetadata: artifact.metadata,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (node.error) {
|
|
235
|
+
pushEvent(events, {
|
|
236
|
+
id: traceId([run.id, node.nodeId, 'error']),
|
|
237
|
+
type: 'error',
|
|
238
|
+
severity: 'error',
|
|
239
|
+
status: node.status,
|
|
240
|
+
timestamp: node.finishedAt ?? timestamp + 30,
|
|
241
|
+
durationMs: nodeDuration,
|
|
242
|
+
...base,
|
|
243
|
+
title: 'Step error',
|
|
244
|
+
titleKey: 'workflow.trace.error',
|
|
245
|
+
summary: redactTraceText(node.error, run),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (run.finishedAt) {
|
|
251
|
+
pushEvent(events, {
|
|
252
|
+
id: traceId([run.id, 'run-finished']),
|
|
253
|
+
type: 'run',
|
|
254
|
+
severity: run.status === 'failed' ? 'error' : 'info',
|
|
255
|
+
status: run.status,
|
|
256
|
+
timestamp: run.finishedAt,
|
|
257
|
+
durationMs: durationMs(run.startedAt, run.finishedAt),
|
|
258
|
+
actor: 'Pixcode',
|
|
259
|
+
title: 'Workflow run finished',
|
|
260
|
+
titleKey: 'workflow.trace.runFinished',
|
|
261
|
+
summary: redactTraceText(readString(run.metadata?.error), run),
|
|
262
|
+
metadata: {
|
|
263
|
+
workflowId: run.workflowId,
|
|
264
|
+
contextId: run.contextId,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return events.sort((a, b) => a.timestamp - b.timestamp || a.id.localeCompare(b.id));
|
|
270
|
+
}
|
|
@@ -3,6 +3,7 @@ import express from 'express';
|
|
|
3
3
|
|
|
4
4
|
import { workflowRunner } from '@/modules/orchestration/workflows/workflow-runner.js';
|
|
5
5
|
import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
|
|
6
|
+
import { buildWorkflowTrace } from '@/modules/orchestration/workflows/workflow-trace.js';
|
|
6
7
|
import { findPixcodeAppRoot } from '@/modules/orchestration/workflows/workspace-target.js';
|
|
7
8
|
|
|
8
9
|
const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'canceled']);
|
|
@@ -120,6 +121,19 @@ export function createWorkflowRouter(): Router {
|
|
|
120
121
|
});
|
|
121
122
|
});
|
|
122
123
|
|
|
124
|
+
router.get('/workflows/runs/:runId/trace', (req, res) => {
|
|
125
|
+
const run = workflowStore.getRun(req.params.runId);
|
|
126
|
+
if (!run) {
|
|
127
|
+
res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
res.json({
|
|
132
|
+
runId: run.id,
|
|
133
|
+
trace: buildWorkflowTrace(run),
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
123
137
|
router.get('/workflows/runs/:runId', (req, res) => {
|
|
124
138
|
const run = workflowStore.getRun(req.params.runId);
|
|
125
139
|
if (!run) {
|
|
@@ -35,6 +35,7 @@ export interface WorkflowNodeRun {
|
|
|
35
35
|
agentInstanceId?: string;
|
|
36
36
|
agentLabel?: string;
|
|
37
37
|
assignment?: string;
|
|
38
|
+
promptPreview?: string;
|
|
38
39
|
model?: string;
|
|
39
40
|
permissionMode?: string;
|
|
40
41
|
timeoutMs?: number;
|
|
@@ -70,3 +71,23 @@ export interface WorkflowRun {
|
|
|
70
71
|
finishedAt?: number;
|
|
71
72
|
metadata?: Record<string, unknown>;
|
|
72
73
|
}
|
|
74
|
+
|
|
75
|
+
export interface WorkflowTraceEvent {
|
|
76
|
+
id: string;
|
|
77
|
+
type: 'run' | 'node' | 'provider' | 'message' | 'artifact' | 'file' | 'error';
|
|
78
|
+
severity: 'info' | 'warning' | 'error';
|
|
79
|
+
status: WorkflowRunStatus | WorkflowNodeStatus | 'submitted';
|
|
80
|
+
timestamp: number;
|
|
81
|
+
durationMs?: number;
|
|
82
|
+
actor: string;
|
|
83
|
+
title: string;
|
|
84
|
+
titleKey?: string;
|
|
85
|
+
summary?: string;
|
|
86
|
+
detail?: string;
|
|
87
|
+
nodeId?: string;
|
|
88
|
+
adapterId?: string;
|
|
89
|
+
agentInstanceId?: string;
|
|
90
|
+
agentLabel?: string;
|
|
91
|
+
model?: string;
|
|
92
|
+
metadata?: Record<string, unknown>;
|
|
93
|
+
}
|