@pixelbyte-software/pixcode 1.41.4 → 1.42.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.
Files changed (24) hide show
  1. package/dist/assets/index-CHa1760s.css +32 -0
  2. package/dist/assets/{index-CIEN0bZ-.js → index-CkOamyD3.js} +52 -52
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/modules/orchestration/workflows/handoff-artifact.js +123 -0
  5. package/dist-server/server/modules/orchestration/workflows/handoff-artifact.js.map +1 -0
  6. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +68 -18
  7. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
  8. package/dist-server/server/modules/orchestration/workflows/workflow-trace.js +4 -0
  9. package/dist-server/server/modules/orchestration/workflows/workflow-trace.js.map +1 -1
  10. package/dist-server/server/routes/live-view.js +44 -8
  11. package/dist-server/server/routes/live-view.js.map +1 -1
  12. package/dist-server/server/services/live-view.js +86 -6
  13. package/dist-server/server/services/live-view.js.map +1 -1
  14. package/package.json +2 -1
  15. package/scripts/smoke/handoff-artifact-protocol.mjs +50 -0
  16. package/scripts/smoke/live-view-environment.mjs +92 -0
  17. package/server/modules/orchestration/a2a/types.ts +1 -0
  18. package/server/modules/orchestration/workflows/handoff-artifact.ts +175 -0
  19. package/server/modules/orchestration/workflows/workflow-runner.ts +78 -18
  20. package/server/modules/orchestration/workflows/workflow-trace.ts +2 -0
  21. package/server/modules/orchestration/workflows/workflow.types.ts +3 -0
  22. package/server/routes/live-view.js +45 -8
  23. package/server/services/live-view.js +90 -6
  24. package/dist/assets/index-BC8CXTJj.css +0 -32
@@ -0,0 +1,175 @@
1
+ export const PIXCODE_HANDOFF_PROTOCOL = 'pixcode.handoff.v1' as const;
2
+
3
+ export type WorkflowHandoffTaskStatus =
4
+ | 'ready'
5
+ | 'completed'
6
+ | 'blocked'
7
+ | 'failed'
8
+ | 'needs-review';
9
+
10
+ export interface WorkflowHandoffArtifact {
11
+ protocol: typeof PIXCODE_HANDOFF_PROTOCOL;
12
+ taskStatus: WorkflowHandoffTaskStatus;
13
+ contextSummary: string;
14
+ taskResult: string;
15
+ changedFiles: string[];
16
+ blockers: string[];
17
+ risks: string[];
18
+ nextAction: string;
19
+ nextInstructions: string;
20
+ producedBy?: {
21
+ workflowRunId?: string;
22
+ nodeId?: string;
23
+ agentLabel?: string;
24
+ stage?: string;
25
+ };
26
+ createdAt: string;
27
+ }
28
+
29
+ export type HandoffArtifactParseResult =
30
+ | { ok: true; artifact: WorkflowHandoffArtifact }
31
+ | { ok: false; error: string };
32
+
33
+ type HandoffArtifactMetadata = {
34
+ workflowRunId?: string;
35
+ nodeId?: string;
36
+ agentLabel?: string;
37
+ stage?: string;
38
+ };
39
+
40
+ const VALID_TASK_STATUSES = new Set<WorkflowHandoffTaskStatus>([
41
+ 'ready',
42
+ 'completed',
43
+ 'blocked',
44
+ 'failed',
45
+ 'needs-review',
46
+ ]);
47
+
48
+ function extractJsonCandidate(text: string): string | null {
49
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim();
50
+ if (fenced) return fenced;
51
+
52
+ const start = text.indexOf('{');
53
+ const end = text.lastIndexOf('}');
54
+ if (start === -1 || end === -1 || end <= start) return null;
55
+ return text.slice(start, end + 1);
56
+ }
57
+
58
+ function readRequiredString(record: Record<string, unknown>, key: string): string | null {
59
+ const value = record[key];
60
+ if (typeof value !== 'string' || !value.trim()) return null;
61
+ return value.trim();
62
+ }
63
+
64
+ function sanitizeChangedFile(filePath: string): string {
65
+ const normalized = filePath.trim().replaceAll('\\', '/');
66
+ if (!normalized) return '';
67
+ if (!normalized.startsWith('/') && !/^[a-zA-Z]:\//.test(normalized)) return normalized;
68
+ return normalized.split('/').filter(Boolean).slice(-4).join('/');
69
+ }
70
+
71
+ function readStringArray(record: Record<string, unknown>, key: string): string[] | null {
72
+ const value = record[key];
73
+ if (!Array.isArray(value)) return null;
74
+ return value
75
+ .filter((item): item is string => typeof item === 'string')
76
+ .map((item) => key === 'changedFiles' ? sanitizeChangedFile(item) : item.trim())
77
+ .filter(Boolean)
78
+ .slice(0, 40);
79
+ }
80
+
81
+ export function parseHandoffArtifact(
82
+ text: string,
83
+ metadata: HandoffArtifactMetadata = {},
84
+ ): HandoffArtifactParseResult {
85
+ const candidate = extractJsonCandidate(text);
86
+ if (!candidate) {
87
+ return { ok: false, error: 'Invalid handoff artifact: expected one JSON object.' };
88
+ }
89
+
90
+ let parsed: unknown;
91
+ try {
92
+ parsed = JSON.parse(candidate);
93
+ } catch (error) {
94
+ return {
95
+ ok: false,
96
+ error: `Invalid handoff artifact: JSON parse failed (${error instanceof Error ? error.message : String(error)}).`,
97
+ };
98
+ }
99
+
100
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
101
+ return { ok: false, error: 'Invalid handoff artifact: payload must be an object.' };
102
+ }
103
+
104
+ const record = parsed as Record<string, unknown>;
105
+ if (record.protocol !== PIXCODE_HANDOFF_PROTOCOL) {
106
+ return { ok: false, error: `Invalid handoff artifact: protocol must be ${PIXCODE_HANDOFF_PROTOCOL}.` };
107
+ }
108
+
109
+ const taskStatus = record.taskStatus;
110
+ if (typeof taskStatus !== 'string' || !VALID_TASK_STATUSES.has(taskStatus as WorkflowHandoffTaskStatus)) {
111
+ return { ok: false, error: 'Invalid handoff artifact: taskStatus is missing or unsupported.' };
112
+ }
113
+
114
+ const contextSummary = readRequiredString(record, 'contextSummary');
115
+ const taskResult = readRequiredString(record, 'taskResult');
116
+ const changedFiles = readStringArray(record, 'changedFiles');
117
+ const blockers = readStringArray(record, 'blockers');
118
+ const risks = readStringArray(record, 'risks');
119
+ const nextAction = readRequiredString(record, 'nextAction');
120
+ const nextInstructions = readRequiredString(record, 'nextInstructions');
121
+
122
+ if (!contextSummary || !taskResult || !changedFiles || !blockers || !risks || !nextAction || !nextInstructions) {
123
+ return {
124
+ ok: false,
125
+ error: 'Invalid handoff artifact: required fields are protocol, taskStatus, contextSummary, taskResult, changedFiles, blockers, risks, nextAction, and nextInstructions.',
126
+ };
127
+ }
128
+
129
+ return {
130
+ ok: true,
131
+ artifact: {
132
+ protocol: PIXCODE_HANDOFF_PROTOCOL,
133
+ taskStatus: taskStatus as WorkflowHandoffTaskStatus,
134
+ contextSummary,
135
+ taskResult,
136
+ changedFiles,
137
+ blockers,
138
+ risks,
139
+ nextAction,
140
+ nextInstructions,
141
+ producedBy: {
142
+ workflowRunId: metadata.workflowRunId,
143
+ nodeId: metadata.nodeId,
144
+ agentLabel: metadata.agentLabel,
145
+ stage: metadata.stage,
146
+ },
147
+ createdAt: new Date().toISOString(),
148
+ },
149
+ };
150
+ }
151
+
152
+ export function formatHandoffArtifactForContext(artifact: WorkflowHandoffArtifact): string {
153
+ return [
154
+ `Pixcode handoff artifact (${PIXCODE_HANDOFF_PROTOCOL})`,
155
+ JSON.stringify(artifact, null, 2),
156
+ ].join('\n');
157
+ }
158
+
159
+ export function handoffArtifactToWorkflowArtifact(artifact: WorkflowHandoffArtifact): {
160
+ type: 'handoff-artifact';
161
+ data: Record<string, unknown>;
162
+ metadata: Record<string, unknown>;
163
+ } {
164
+ return {
165
+ type: 'handoff-artifact',
166
+ data: artifact as unknown as Record<string, unknown>,
167
+ metadata: {
168
+ protocol: artifact.protocol,
169
+ taskStatus: artifact.taskStatus,
170
+ changedFileCount: artifact.changedFiles.length,
171
+ blockerCount: artifact.blockers.length,
172
+ nextAction: artifact.nextAction,
173
+ },
174
+ };
175
+ }
@@ -6,6 +6,12 @@ import type {
6
6
  WorkflowNodeRun,
7
7
  WorkflowRun,
8
8
  } from '@/modules/orchestration/workflows/workflow.types.js';
9
+ import {
10
+ PIXCODE_HANDOFF_PROTOCOL,
11
+ formatHandoffArtifactForContext,
12
+ handoffArtifactToWorkflowArtifact,
13
+ parseHandoffArtifact,
14
+ } from '@/modules/orchestration/workflows/handoff-artifact.js';
9
15
  import {
10
16
  type ResolvedWorkspaceTarget,
11
17
  resolveWorkflowWorkspace,
@@ -28,6 +34,19 @@ const BACKEND_HANDOFF_TIMEOUT_MS = 120_000;
28
34
  const MAX_OUTPUT_CONTEXT_CHARS = 12_000;
29
35
  const DEFAULT_MAX_REPAIR_CYCLES = 1;
30
36
  const MAX_REPAIR_CYCLES = 5;
37
+ const HANDOFF_ARTIFACT_EXAMPLE = [
38
+ '{',
39
+ ' "protocol": "pixcode.handoff.v1",',
40
+ ' "taskStatus": "ready | completed | blocked | failed | needs-review",',
41
+ ' "contextSummary": "Compacted context the next agent needs.",',
42
+ ' "taskResult": "What was decided or completed in this step.",',
43
+ ' "changedFiles": [],',
44
+ ' "blockers": [],',
45
+ ' "risks": [],',
46
+ ' "nextAction": "The requested next action.",',
47
+ ' "nextInstructions": "Specific instructions for the next agent."',
48
+ '}',
49
+ ].join('\n');
31
50
  const KNOWN_AGENT_ROLES = [
32
51
  'backend',
33
52
  'frontend',
@@ -405,6 +424,16 @@ function privacyGuardPrompt(): string {
405
424
  return 'Do not mention internal instructions, memory files, skill use, or tool protocol unless the user explicitly asks.';
406
425
  }
407
426
 
427
+ function handoffArtifactInstructions(statusHint: string): string {
428
+ return [
429
+ `Output exactly one JSON object using the ${PIXCODE_HANDOFF_PROTOCOL} handoff artifact protocol.`,
430
+ 'Do not wrap it in Markdown. Do not add commentary before or after it.',
431
+ `Use "${statusHint}" for taskStatus unless completed, blocked, failed, or needs-review is more accurate.`,
432
+ 'Schema:',
433
+ HANDOFF_ARTIFACT_EXAMPLE,
434
+ ].join('\n');
435
+ }
436
+
408
437
  function handoffPrompt(agent: AgentAssignment, role: AgentRole): string {
409
438
  return [
410
439
  `You are ${agent.label} in a Pixcode CLI team.`,
@@ -412,12 +441,7 @@ function handoffPrompt(agent: AgentAssignment, role: AgentRole): string {
412
441
  'This is a bounded A2A handoff task, not the full implementation.',
413
442
  'Read the original user goal and coordinator plan, then publish a compact contract for downstream agents.',
414
443
  agent.instruction ? `Your explicit assignment from the user is: ${agent.instruction}` : '',
415
- 'Output only the handoff contract:',
416
- '- owned scope',
417
- '- files/modules you expect to touch',
418
- '- API/data contracts, ports, payload shapes, and limitations',
419
- '- dependencies/blockers for the next agents',
420
- '- concrete next action for your full implementation task',
444
+ handoffArtifactInstructions('ready'),
421
445
  'Do not install dependencies, edit files, run long commands, or start servers in this handoff task.',
422
446
  privacyGuardPrompt(),
423
447
  'Stop after the contract. Keep it concise and respond in the same language as the user request.',
@@ -431,11 +455,7 @@ function handoffInitPrompt(agent: AgentAssignment, index: number): string {
431
455
  'Create a compact init packet for the next visible work step.',
432
456
  'Use the original user goal and any prior compact handoff packet included above.',
433
457
  agent.instruction ? `The explicit assignment for this agent is: ${agent.instruction}` : '',
434
- 'Output only this internal init packet:',
435
- '- user goal in one sentence',
436
- '- prior agent handoff summary, if present',
437
- '- this agent responsibility',
438
- '- exact constraints and blockers this agent must respect',
458
+ handoffArtifactInstructions('ready'),
439
459
  privacyGuardPrompt(),
440
460
  'Do not perform the task yet. Do not mention that this is hidden from the user.',
441
461
  'Respond in the same language as the user request.',
@@ -462,12 +482,7 @@ function handoffCompactPrompt(agent: AgentAssignment, index: number): string {
462
482
  `You are compacting ${agent.label}'s strict handoff output for the next Pixcode agent.`,
463
483
  `This is internal compact step ${index + 1}.`,
464
484
  'Read the prior visible work output included above and create a compact handoff packet.',
465
- 'Output only this internal compact packet:',
466
- '- Ben ne yaptım / What I did',
467
- '- Dokunduğum alanlar / Touched areas',
468
- '- Kanıt, komut veya çıktı / Evidence, commands, outputs',
469
- '- Sonraki ajan şunu bilsin / What the next agent must know',
470
- '- Bloker veya risk / Blockers or risks',
485
+ handoffArtifactInstructions('completed'),
471
486
  privacyGuardPrompt(),
472
487
  'Do not include raw logs unless they are essential. Keep it concise and actionable.',
473
488
  'Respond in the same language as the user request.',
@@ -487,6 +502,18 @@ function compactOutputForContext(text: string): string {
487
502
  ].join('');
488
503
  }
489
504
 
505
+ function requiresHandoffArtifact(node: WorkflowNode): boolean {
506
+ return node.stage === 'handoff' || node.stage === 'handoff_init' || node.stage === 'handoff_compact';
507
+ }
508
+
509
+ function handoffArtifactSource(result: TaskResult): string {
510
+ const structured = result.artifacts.find((artifact) => artifact.type === 'handoff-artifact' && artifact.data);
511
+ if (structured?.data) {
512
+ return JSON.stringify(structured.data);
513
+ }
514
+ return result.text;
515
+ }
516
+
490
517
  function isExternalDirectoryPermissionError(value: unknown): boolean {
491
518
  const text = String(value ?? '').toLocaleLowerCase('en');
492
519
  return (
@@ -1688,7 +1715,40 @@ class WorkflowRunner {
1688
1715
  throw new WorkflowCanceledError();
1689
1716
  }
1690
1717
  if (result.state === 'completed') {
1691
- outputs.set(node.id, compactOutputForContext(result.text));
1718
+ let outputForContext = result.text;
1719
+ if (requiresHandoffArtifact(node)) {
1720
+ const handoffParse = parseHandoffArtifact(handoffArtifactSource(result), {
1721
+ workflowRunId: run.id,
1722
+ nodeId: node.id,
1723
+ agentLabel: node.agentLabel,
1724
+ stage: node.stage,
1725
+ });
1726
+ if (!handoffParse.ok) {
1727
+ const visibleHandoffError = handoffParse.error.startsWith('Invalid handoff artifact')
1728
+ ? handoffParse.error
1729
+ : `Invalid handoff artifact: ${handoffParse.error}`;
1730
+ nodeRun.status = 'failed';
1731
+ nodeRun.error = visibleHandoffError;
1732
+ workflowStore.setRun(run);
1733
+ if (await this.runFallbackAfterFailure(node, workflow, run, outputs, started, completed, visibleHandoffError)) {
1734
+ return;
1735
+ }
1736
+ if (node.onFail === 'continue') {
1737
+ completed.add(node.id);
1738
+ return;
1739
+ }
1740
+ throw new Error(visibleHandoffError);
1741
+ }
1742
+
1743
+ nodeRun.handoffArtifact = handoffParse.artifact;
1744
+ nodeRun.artifacts = [
1745
+ ...(nodeRun.artifacts ?? []).filter((artifact) => artifact.type !== 'handoff-artifact'),
1746
+ handoffArtifactToWorkflowArtifact(handoffParse.artifact),
1747
+ ];
1748
+ outputForContext = formatHandoffArtifactForContext(handoffParse.artifact);
1749
+ }
1750
+
1751
+ outputs.set(node.id, compactOutputForContext(outputForContext));
1692
1752
  completed.add(node.id);
1693
1753
  nodeRun.status = 'completed';
1694
1754
  workflowStore.setRun(run);
@@ -87,6 +87,7 @@ function artifactTitleKey(type: string): string {
87
87
  if (type === 'file-diff') return 'workflow.trace.fileChanged';
88
88
  if (type === 'preview-url') return 'workflow.trace.previewReady';
89
89
  if (type === 'command-output') return 'workflow.trace.commandOutput';
90
+ if (type === 'handoff-artifact') return 'workflow.trace.handoffArtifact';
90
91
  return 'workflow.trace.artifact';
91
92
  }
92
93
 
@@ -98,6 +99,7 @@ function artifactTitle(type: string): string {
98
99
  if (type === 'file-diff') return 'File changes captured';
99
100
  if (type === 'preview-url') return 'Preview output captured';
100
101
  if (type === 'command-output') return 'Command output captured';
102
+ if (type === 'handoff-artifact') return 'Handoff artifact captured';
101
103
  return 'Artifact captured';
102
104
  }
103
105
 
@@ -1,3 +1,5 @@
1
+ import type { WorkflowHandoffArtifact } from '@/modules/orchestration/workflows/handoff-artifact.js';
2
+
1
3
  export type WorkflowRunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'canceled';
2
4
  export type WorkflowNodeStatus = WorkflowRunStatus | 'skipped';
3
5
 
@@ -47,6 +49,7 @@ export interface WorkflowNodeRun {
47
49
  finishedAt?: number;
48
50
  error?: string;
49
51
  outputText?: string;
52
+ handoffArtifact?: WorkflowHandoffArtifact;
50
53
  messages?: Array<{
51
54
  role: string;
52
55
  text: string;
@@ -31,6 +31,29 @@ function buildUrls(req, session) {
31
31
  };
32
32
  }
33
33
 
34
+ function buildEnvironmentTunnel(tunnel, urls) {
35
+ const active = Boolean(tunnel?.running && urls?.external);
36
+ return {
37
+ status: active ? 'active' : 'local-only',
38
+ url: active ? urls.external : null,
39
+ localUrl: urls?.local || null,
40
+ preferredUrl: urls?.preferred || urls?.local || null,
41
+ };
42
+ }
43
+
44
+ function attachEnvironmentRuntimeState(environment, urls, tunnel) {
45
+ if (!environment) return environment;
46
+ return {
47
+ ...environment,
48
+ urls,
49
+ tunnel: buildEnvironmentTunnel(tunnel, urls),
50
+ diagnostics: {
51
+ ...environment.diagnostics,
52
+ publicTunnelReady: Boolean(tunnel?.running && urls?.external),
53
+ },
54
+ };
55
+ }
56
+
34
57
  function escapeHtml(value) {
35
58
  return String(value ?? '')
36
59
  .replaceAll('&', '&amp;')
@@ -202,10 +225,13 @@ router.get('/:projectName/status', async (req, res) => {
202
225
  const { projectName } = req.params;
203
226
  const projectPath = await resolveProjectPath(projectName);
204
227
  const state = await getLiveViewState(projectName, projectPath);
228
+ const urls = buildUrls(req, state.session);
229
+ const tunnel = getTunnelState();
205
230
  res.json({
206
231
  ...state,
207
- urls: buildUrls(req, state.session),
208
- tunnel: getTunnelState(),
232
+ urls,
233
+ tunnel,
234
+ environment: attachEnvironmentRuntimeState(state.environment, urls, tunnel),
209
235
  });
210
236
  } catch (error) {
211
237
  res.status(error.statusCode || 500).json({ error: error.message || 'Failed to read Live View state' });
@@ -217,11 +243,15 @@ router.post('/:projectName/start', async (req, res) => {
217
243
  const { projectName } = req.params;
218
244
  const projectPath = await resolveProjectPath(projectName);
219
245
  const session = await startLiveView(projectName, projectPath, req.body || {});
246
+ const state = await getLiveViewState(projectName, projectPath);
247
+ const urls = buildUrls(req, session);
248
+ const tunnel = getTunnelState();
220
249
  res.json({
221
250
  success: true,
222
251
  session,
223
- urls: buildUrls(req, session),
224
- tunnel: getTunnelState(),
252
+ urls,
253
+ tunnel,
254
+ environment: attachEnvironmentRuntimeState(state.environment, urls, tunnel),
225
255
  });
226
256
  } catch (error) {
227
257
  const status = error.code === 'LIVE_VIEW_NOT_AVAILABLE' ? 422 : 500;
@@ -234,11 +264,15 @@ router.post('/:projectName/restart', async (req, res) => {
234
264
  const { projectName } = req.params;
235
265
  const projectPath = await resolveProjectPath(projectName);
236
266
  const session = await restartLiveView(projectName, projectPath, req.body || {});
267
+ const state = await getLiveViewState(projectName, projectPath);
268
+ const urls = buildUrls(req, session);
269
+ const tunnel = getTunnelState();
237
270
  res.json({
238
271
  success: true,
239
272
  session,
240
- urls: buildUrls(req, session),
241
- tunnel: getTunnelState(),
273
+ urls,
274
+ tunnel,
275
+ environment: attachEnvironmentRuntimeState(state.environment, urls, tunnel),
242
276
  });
243
277
  } catch (error) {
244
278
  res.status(500).json({ error: error.message || 'Failed to restart Live View' });
@@ -248,11 +282,14 @@ router.post('/:projectName/restart', async (req, res) => {
248
282
  router.post('/:projectName/stop', async (req, res) => {
249
283
  try {
250
284
  const session = await stopLiveView(req.params.projectName);
285
+ const urls = buildUrls(req, session);
286
+ const tunnel = getTunnelState();
251
287
  res.json({
252
288
  success: true,
253
289
  session,
254
- urls: buildUrls(req, session),
255
- tunnel: getTunnelState(),
290
+ urls,
291
+ tunnel,
292
+ environment: attachEnvironmentRuntimeState(null, urls, tunnel),
256
293
  });
257
294
  } catch (error) {
258
295
  res.status(500).json({ error: error.message || 'Failed to stop Live View' });
@@ -294,6 +294,91 @@ function buildManagedPhpCommand(runtimeStatus) {
294
294
  };
295
295
  }
296
296
 
297
+ function publicCommand(command) {
298
+ if (!command) return null;
299
+ return {
300
+ id: command.id,
301
+ label: command.label,
302
+ displayCommand: command.displayCommand,
303
+ custom: command.custom === true || command.id === 'custom',
304
+ };
305
+ }
306
+
307
+ function liveViewEnvironmentMode(target, session) {
308
+ const kind = session?.kind || target?.kind || 'none';
309
+ if (kind === 'static') return 'static';
310
+ if (kind === 'process') return 'local-process';
311
+ return 'unavailable';
312
+ }
313
+
314
+ function liveViewEnvironmentStatus(target, session) {
315
+ if (session?.status) return session.status;
316
+ if (target?.available) return 'ready';
317
+ return 'unavailable';
318
+ }
319
+
320
+ function liveViewEnvironmentCommand(target, session) {
321
+ return publicCommand(session?.command) || publicCommand(target?.command);
322
+ }
323
+
324
+ function liveViewEnvironmentLogs(session) {
325
+ return Array.isArray(session?.log) ? session.log.slice(-40) : [];
326
+ }
327
+
328
+ function liveViewEnvironmentRuntime(target, session) {
329
+ return session?.runtime || target?.runtime || null;
330
+ }
331
+
332
+ function liveViewEnvironmentManagedRuntime(target, session) {
333
+ return session?.managedRuntime || target?.managedRuntime || target?.command?.managedRuntime || null;
334
+ }
335
+
336
+ export function buildLiveViewEnvironment({ target = null, session = null } = {}) {
337
+ const mode = liveViewEnvironmentMode(target, session);
338
+ const status = liveViewEnvironmentStatus(target, session);
339
+ const command = liveViewEnvironmentCommand(target, session);
340
+ const framework = session?.framework || target?.framework || null;
341
+ const label = session?.label || target?.label || framework || 'Live View';
342
+ const runtime = liveViewEnvironmentRuntime(target, session);
343
+ const managedRuntime = liveViewEnvironmentManagedRuntime(target, session);
344
+ const logs = liveViewEnvironmentLogs(session);
345
+ const reason = session?.error || target?.reason || null;
346
+
347
+ return {
348
+ id: mode === 'unavailable' ? 'live-view-unavailable' : `live-view-${mode}`,
349
+ mode,
350
+ status,
351
+ framework,
352
+ label,
353
+ command,
354
+ runtime,
355
+ managedRuntime,
356
+ port: session?.port ?? null,
357
+ upstreamUrl: session?.upstreamUrl ?? null,
358
+ sharePath: session?.sharePath ?? null,
359
+ logs,
360
+ diagnostics: {
361
+ runnerKind: session?.kind || target?.kind || 'none',
362
+ targetAvailable: Boolean(target?.available || session),
363
+ reason,
364
+ error: session?.error || null,
365
+ exitCode: session?.exitCode ?? null,
366
+ exitSignal: session?.exitSignal ?? null,
367
+ spawnErrorCode: session?.spawnErrorCode ?? null,
368
+ startedAt: session?.startedAt || null,
369
+ stoppedAt: session?.stoppedAt || null,
370
+ readyTimeoutMs: READY_TIMEOUT_MS,
371
+ staticServing: mode === 'static',
372
+ customCommand: command?.custom === true,
373
+ publicTunnelReady: false,
374
+ },
375
+ tunnel: {
376
+ status: 'local-only',
377
+ url: null,
378
+ },
379
+ };
380
+ }
381
+
297
382
  function detectPackageCommand(packageJson, packageManager) {
298
383
  const scripts = packageJson.scripts || {};
299
384
  const devScript = String(scripts.dev || '');
@@ -648,11 +733,7 @@ function publicSession(session) {
648
733
  kind: session.kind,
649
734
  framework: session.framework,
650
735
  label: session.label,
651
- command: session.command ? {
652
- id: session.command.id,
653
- label: session.command.label,
654
- displayCommand: session.command.displayCommand,
655
- } : null,
736
+ command: publicCommand(session.command),
656
737
  runtime: session.runtime || null,
657
738
  managedRuntime: session.managedRuntime || null,
658
739
  port: session.port,
@@ -670,9 +751,11 @@ function publicSession(session) {
670
751
  export async function getLiveViewState(projectName, projectPath) {
671
752
  const target = await detectLiveViewTarget(projectPath);
672
753
  const session = sessionsByProject.get(projectName) ?? null;
754
+ const publicLiveViewSession = publicSession(session);
673
755
  return {
674
756
  target,
675
- session: publicSession(session),
757
+ session: publicLiveViewSession,
758
+ environment: buildLiveViewEnvironment({ target, session: publicLiveViewSession }),
676
759
  };
677
760
  }
678
761
 
@@ -697,6 +780,7 @@ export async function startLiveView(projectName, projectPath, options = {}) {
697
780
  command: customCommand,
698
781
  args: [],
699
782
  displayCommand: customCommand,
783
+ custom: true,
700
784
  shell: true,
701
785
  },
702
786
  }