@pixelbyte-software/pixcode 1.41.2 → 1.41.3

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,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));
@@ -55,6 +55,7 @@ export type {
55
55
  WorkflowNodeStatus,
56
56
  WorkflowRun,
57
57
  WorkflowRunStatus,
58
+ WorkflowTraceEvent,
58
59
  } from './workflows/workflow.types.js';
59
60
  export type {
60
61
  ExecResult,
@@ -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
+ }