bosun 0.41.8 → 0.41.10
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/.env.example +1 -1
- package/README.md +23 -1
- package/agent/agent-event-bus.mjs +31 -2
- package/agent/agent-pool.mjs +275 -40
- package/agent/agent-prompts.mjs +9 -1
- package/agent/agent-supervisor.mjs +22 -0
- package/agent/autofix.mjs +1 -1
- package/agent/primary-agent.mjs +115 -5
- package/cli.mjs +3 -2
- package/config/config.mjs +47 -33
- package/config/context-shredding-config.mjs +1 -1
- package/config/repo-root.mjs +41 -33
- package/desktop/main.mjs +350 -25
- package/desktop/preload.cjs +8 -0
- package/desktop/preload.mjs +19 -0
- package/entrypoint.mjs +332 -0
- package/git/sdk-conflict-resolver.mjs +1 -1
- package/infra/health-status.mjs +72 -0
- package/infra/library-manager.mjs +58 -1
- package/infra/maintenance.mjs +1 -2
- package/infra/monitor.mjs +26 -8
- package/infra/session-tracker.mjs +30 -3
- package/package.json +12 -4
- package/server/bosun-mcp-server.mjs +1004 -0
- package/server/setup-web-server.mjs +288 -259
- package/server/ui-server.mjs +1323 -26
- package/shell/claude-shell.mjs +14 -1
- package/shell/codex-config.mjs +1 -1
- package/shell/codex-model-profiles.mjs +170 -30
- package/shell/codex-shell.mjs +63 -18
- package/shell/opencode-providers.mjs +20 -8
- package/task/task-executor.mjs +28 -0
- package/task/task-store.mjs +13 -4
- package/telegram/telegram-sentinel.mjs +54 -3
- package/tools/list-todos.mjs +7 -1
- package/ui/app.js +3 -2
- package/ui/components/agent-selector.js +127 -0
- package/ui/components/session-list.js +15 -10
- package/ui/demo-defaults.js +334 -336
- package/ui/modules/router.js +2 -0
- package/ui/modules/state.js +13 -5
- package/ui/tabs/chat.js +3 -0
- package/ui/tabs/library.js +284 -52
- package/ui/tabs/tasks.js +5 -13
- package/ui/tabs/workflows.js +766 -3
- package/workflow/workflow-engine.mjs +246 -5
- package/workflow/workflow-nodes/definitions.mjs +37 -0
- package/workflow/workflow-nodes.mjs +1014 -184
- package/workflow/workflow-templates.mjs +0 -5
- package/workflow-templates/_helpers.mjs +253 -0
- package/workflow-templates/agents.mjs +199 -226
- package/workflow-templates/github.mjs +106 -16
- package/workflow-templates/sub-workflows.mjs +233 -0
- package/workflow-templates/task-execution.mjs +125 -471
- package/workflow-templates/task-lifecycle.mjs +11 -48
- package/workspace/command-diagnostics.mjs +460 -0
- package/workspace/context-cache.mjs +396 -28
- package/workspace/worktree-manager.mjs +1 -1
package/ui/tabs/workflows.js
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
serializeGraphSnapshot,
|
|
31
31
|
undoHistory,
|
|
32
32
|
} from "./workflow-canvas-utils.mjs";
|
|
33
|
+
import { createSession } from "../components/session-list.js";
|
|
34
|
+
import { buildSessionApiPath, resolveSessionWorkspaceHint } from "../modules/session-api.js";
|
|
33
35
|
import { Card, Badge, EmptyState } from "../components/shared.js";
|
|
34
36
|
import {
|
|
35
37
|
Typography, Box, Stack, Card as MuiCard, CardContent, Button, IconButton, Chip,
|
|
@@ -61,7 +63,7 @@ const selectedNodeId = signal(null);
|
|
|
61
63
|
const selectedEdgeId = signal(null);
|
|
62
64
|
const draggingNode = signal(null);
|
|
63
65
|
const connectingFrom = signal(null);
|
|
64
|
-
const viewMode = signal("list"); // "list" | "canvas" | "runs"
|
|
66
|
+
const viewMode = signal("list"); // "list" | "canvas" | "runs" | "code"
|
|
65
67
|
const WORKFLOW_RUN_PAGE_SIZE = 50;
|
|
66
68
|
const WORKFLOW_RUN_MAX_FETCH = 5000;
|
|
67
69
|
const WORKFLOW_LIVE_POLL_MS = 3000;
|
|
@@ -176,6 +178,370 @@ export function openWorkflowRunsView(workflowId, runId = null) {
|
|
|
176
178
|
}
|
|
177
179
|
}
|
|
178
180
|
|
|
181
|
+
const WORKFLOW_COPILOT_MAX_CHARS = 5000;
|
|
182
|
+
|
|
183
|
+
function formatWorkflowCopilotBlock(value, maxChars = WORKFLOW_COPILOT_MAX_CHARS) {
|
|
184
|
+
try {
|
|
185
|
+
const json = JSON.stringify(value, null, 2);
|
|
186
|
+
if (json.length <= maxChars) return json;
|
|
187
|
+
const omitted = json.length - maxChars;
|
|
188
|
+
return `${json.slice(0, maxChars)}\n\n[truncated ${omitted} chars]`;
|
|
189
|
+
} catch {
|
|
190
|
+
const text = String(value ?? "");
|
|
191
|
+
if (text.length <= maxChars) return text;
|
|
192
|
+
const omitted = text.length - maxChars;
|
|
193
|
+
return `${text.slice(0, maxChars)}\n\n[truncated ${omitted} chars]`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function summarizeWorkflowNodes(workflow, limit = 20) {
|
|
198
|
+
const nodes = Array.isArray(workflow?.nodes) ? workflow.nodes : [];
|
|
199
|
+
if (!nodes.length) return "No nodes defined.";
|
|
200
|
+
const lines = nodes.slice(0, limit).map((node, index) => {
|
|
201
|
+
const nodeId = String(node?.id || `node-${index + 1}`).trim();
|
|
202
|
+
const nodeType = String(node?.type || "unknown").trim();
|
|
203
|
+
const nodeName =
|
|
204
|
+
String(node?.name || node?.label || node?.title || "").trim() || null;
|
|
205
|
+
const configKeys = Object.keys(node?.config || {}).slice(0, 6);
|
|
206
|
+
const configSummary = configKeys.length ? ` config keys: ${configKeys.join(", ")}` : "";
|
|
207
|
+
return `${index + 1}. ${nodeId} [${nodeType}]${nodeName ? ` - ${nodeName}` : ""}${configSummary}`;
|
|
208
|
+
});
|
|
209
|
+
if (nodes.length > limit) {
|
|
210
|
+
lines.push(`... ${nodes.length - limit} more node(s) omitted`);
|
|
211
|
+
}
|
|
212
|
+
return lines.join("\n");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function summarizeWorkflowEdges(workflow, limit = 24) {
|
|
216
|
+
const edges = Array.isArray(workflow?.edges) ? workflow.edges : [];
|
|
217
|
+
if (!edges.length) return "No edges defined.";
|
|
218
|
+
const lines = edges.slice(0, limit).map((edge, index) => {
|
|
219
|
+
const from = String(edge?.source || edge?.from || "?").trim() || "?";
|
|
220
|
+
const to = String(edge?.target || edge?.to || "?").trim() || "?";
|
|
221
|
+
const fromPort = String(edge?.sourcePort || edge?.fromPort || "").trim();
|
|
222
|
+
const toPort = String(edge?.targetPort || edge?.toPort || "").trim();
|
|
223
|
+
const portSummary = fromPort || toPort ? ` (${fromPort || "default"} -> ${toPort || "default"})` : "";
|
|
224
|
+
return `${index + 1}. ${from} -> ${to}${portSummary}`;
|
|
225
|
+
});
|
|
226
|
+
if (edges.length > limit) {
|
|
227
|
+
lines.push(`... ${edges.length - limit} more edge(s) omitted`);
|
|
228
|
+
}
|
|
229
|
+
return lines.join("\n");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function summarizeWorkflowNodeLinks(workflow, nodeId, direction = "incoming", limit = 10) {
|
|
233
|
+
const safeNodeId = String(nodeId || "").trim();
|
|
234
|
+
if (!safeNodeId) return direction === "outgoing" ? "No downstream edges." : "No upstream edges.";
|
|
235
|
+
const edges = Array.isArray(workflow?.edges) ? workflow.edges : [];
|
|
236
|
+
const relevant = edges.filter((edge) => {
|
|
237
|
+
const sourceId = String(edge?.source || edge?.from || "").trim();
|
|
238
|
+
const targetId = String(edge?.target || edge?.to || "").trim();
|
|
239
|
+
return direction === "outgoing" ? sourceId === safeNodeId : targetId === safeNodeId;
|
|
240
|
+
});
|
|
241
|
+
if (!relevant.length) return direction === "outgoing" ? "No downstream edges." : "No upstream edges.";
|
|
242
|
+
const lines = relevant.slice(0, limit).map((edge, index) => {
|
|
243
|
+
const sourceId = String(edge?.source || edge?.from || "?").trim() || "?";
|
|
244
|
+
const targetId = String(edge?.target || edge?.to || "?").trim() || "?";
|
|
245
|
+
const sourcePort = String(edge?.sourcePort || edge?.fromPort || "").trim() || "default";
|
|
246
|
+
const targetPort = String(edge?.targetPort || edge?.toPort || "").trim() || "default";
|
|
247
|
+
return `${index + 1}. ${sourceId}:${sourcePort} -> ${targetId}:${targetPort}`;
|
|
248
|
+
});
|
|
249
|
+
if (relevant.length > limit) {
|
|
250
|
+
lines.push(`... ${relevant.length - limit} more ${direction} edge(s) omitted`);
|
|
251
|
+
}
|
|
252
|
+
return lines.join("\n");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function summarizeRunNodeStatuses(run, limit = 25) {
|
|
256
|
+
const nodeStatuses = buildNodeStatusesFromRunDetail(run);
|
|
257
|
+
const entries = Object.entries(nodeStatuses || {});
|
|
258
|
+
if (!entries.length) return "No node status data recorded.";
|
|
259
|
+
const sorted = entries.sort((a, b) => {
|
|
260
|
+
const rankDiff = getNodeStatusRank(a[1]) - getNodeStatusRank(b[1]);
|
|
261
|
+
if (rankDiff !== 0) return rankDiff;
|
|
262
|
+
return String(a[0]).localeCompare(String(b[0]));
|
|
263
|
+
});
|
|
264
|
+
const lines = sorted.slice(0, limit).map(([nodeId, status], index) => (
|
|
265
|
+
`${index + 1}. ${nodeId}: ${status || "unknown"}`
|
|
266
|
+
));
|
|
267
|
+
if (sorted.length > limit) {
|
|
268
|
+
lines.push(`... ${sorted.length - limit} more node status entries omitted`);
|
|
269
|
+
}
|
|
270
|
+
return lines.join("\n");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function summarizeRunNodeOutputs(run, limit = 12) {
|
|
274
|
+
const outputs = run?.detail?.nodeOutputs && typeof run.detail.nodeOutputs === "object"
|
|
275
|
+
? run.detail.nodeOutputs
|
|
276
|
+
: {};
|
|
277
|
+
const entries = Object.entries(outputs);
|
|
278
|
+
if (!entries.length) return "No node outputs recorded.";
|
|
279
|
+
const lines = entries.slice(0, limit).map(([nodeId, output], index) => {
|
|
280
|
+
const summary = String(output?.summary || "").trim();
|
|
281
|
+
const narrative = String(output?.narrative || "").trim();
|
|
282
|
+
if (summary || narrative) {
|
|
283
|
+
const parts = [summary, narrative].filter(Boolean);
|
|
284
|
+
return `${index + 1}. ${nodeId}: ${parts.join(" | ")}`;
|
|
285
|
+
}
|
|
286
|
+
return `${index + 1}. ${nodeId}: ${formatWorkflowCopilotBlock(output, 500)}`;
|
|
287
|
+
});
|
|
288
|
+
if (entries.length > limit) {
|
|
289
|
+
lines.push(`... ${entries.length - limit} more node output entries omitted`);
|
|
290
|
+
}
|
|
291
|
+
return lines.join("\n");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function buildWorkflowExplainPrompt(workflow) {
|
|
295
|
+
const workflowId = String(workflow?.id || "").trim() || "(unknown)";
|
|
296
|
+
const workflowName = String(workflow?.name || workflowId).trim() || workflowId;
|
|
297
|
+
const description = String(workflow?.description || "").trim() || "None provided.";
|
|
298
|
+
const variables = workflow?.variables && typeof workflow.variables === "object"
|
|
299
|
+
? workflow.variables
|
|
300
|
+
: {};
|
|
301
|
+
return [
|
|
302
|
+
"You are helping inside Bosun with a workflow authoring review.",
|
|
303
|
+
"Explain this workflow in plain English, identify the riskiest nodes or missing guardrails, and suggest the smallest high-leverage improvements.",
|
|
304
|
+
"",
|
|
305
|
+
"Return:",
|
|
306
|
+
"1. A concise summary of what the workflow is trying to do",
|
|
307
|
+
"2. The critical nodes or transitions that matter most",
|
|
308
|
+
"3. Failure risks, ambiguity, or missing validation/retry/observability",
|
|
309
|
+
"4. Concrete next edits Bosun should make",
|
|
310
|
+
"",
|
|
311
|
+
"Workflow Context",
|
|
312
|
+
`- Name: ${workflowName}`,
|
|
313
|
+
`- ID: ${workflowId}`,
|
|
314
|
+
`- Enabled: ${workflow?.enabled === false ? "no" : "yes"}`,
|
|
315
|
+
`- Core workflow: ${workflow?.core === true ? "yes" : "no"}`,
|
|
316
|
+
`- Description: ${description}`,
|
|
317
|
+
`- Node count: ${Array.isArray(workflow?.nodes) ? workflow.nodes.length : 0}`,
|
|
318
|
+
`- Edge count: ${Array.isArray(workflow?.edges) ? workflow.edges.length : 0}`,
|
|
319
|
+
"",
|
|
320
|
+
"Variables",
|
|
321
|
+
formatWorkflowCopilotBlock(variables, 2500),
|
|
322
|
+
"",
|
|
323
|
+
"Node Summary",
|
|
324
|
+
summarizeWorkflowNodes(workflow),
|
|
325
|
+
"",
|
|
326
|
+
"Edge Summary",
|
|
327
|
+
summarizeWorkflowEdges(workflow),
|
|
328
|
+
"",
|
|
329
|
+
"Raw Workflow Snapshot",
|
|
330
|
+
formatWorkflowCopilotBlock({
|
|
331
|
+
id: workflow?.id,
|
|
332
|
+
name: workflow?.name,
|
|
333
|
+
description: workflow?.description,
|
|
334
|
+
enabled: workflow?.enabled,
|
|
335
|
+
core: workflow?.core,
|
|
336
|
+
metadata: workflow?.metadata || {},
|
|
337
|
+
}, 2500),
|
|
338
|
+
].join("\n");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildWorkflowNodePrompt(workflow, node, nodeTypeMap = new Map()) {
|
|
342
|
+
if (!node) return "";
|
|
343
|
+
const workflowId = String(workflow?.id || "").trim() || "(unknown)";
|
|
344
|
+
const workflowName = String(workflow?.name || workflowId).trim() || workflowId;
|
|
345
|
+
const nodeType = String(node?.type || "unknown").trim() || "unknown";
|
|
346
|
+
const typeInfo = nodeTypeMap.get(nodeType) || null;
|
|
347
|
+
const schemaKeys = Object.keys(typeInfo?.schema?.properties || {});
|
|
348
|
+
return [
|
|
349
|
+
"You are helping inside Bosun with workflow node authoring.",
|
|
350
|
+
"Explain what this node does, how it interacts with adjacent nodes, what is risky or underspecified, and which exact config edits Bosun should make next.",
|
|
351
|
+
"",
|
|
352
|
+
"Return:",
|
|
353
|
+
"1. Node purpose",
|
|
354
|
+
"2. Upstream/downstream interaction notes",
|
|
355
|
+
"3. Risks, missing validation, or bad defaults",
|
|
356
|
+
"4. Concrete config or graph edits",
|
|
357
|
+
"",
|
|
358
|
+
"Workflow Context",
|
|
359
|
+
`- Workflow: ${workflowName}`,
|
|
360
|
+
`- Workflow ID: ${workflowId}`,
|
|
361
|
+
`- Node count: ${Array.isArray(workflow?.nodes) ? workflow.nodes.length : 0}`,
|
|
362
|
+
`- Edge count: ${Array.isArray(workflow?.edges) ? workflow.edges.length : 0}`,
|
|
363
|
+
"",
|
|
364
|
+
"Node Context",
|
|
365
|
+
`- Node ID: ${String(node?.id || "").trim() || "(unknown)"}`,
|
|
366
|
+
`- Label: ${String(node?.label || node?.name || "").trim() || "(none)"}`,
|
|
367
|
+
`- Type: ${nodeType}`,
|
|
368
|
+
`- Category: ${nodeType.split(".")[0] || "unknown"}`,
|
|
369
|
+
`- Description: ${String(typeInfo?.description || "").trim() || "None provided."}`,
|
|
370
|
+
`- Schema keys: ${schemaKeys.length ? schemaKeys.join(", ") : "None"}`,
|
|
371
|
+
"",
|
|
372
|
+
"Upstream Edges",
|
|
373
|
+
summarizeWorkflowNodeLinks(workflow, node?.id, "incoming"),
|
|
374
|
+
"",
|
|
375
|
+
"Downstream Edges",
|
|
376
|
+
summarizeWorkflowNodeLinks(workflow, node?.id, "outgoing"),
|
|
377
|
+
"",
|
|
378
|
+
"Node Config",
|
|
379
|
+
formatWorkflowCopilotBlock(node?.config || {}, 3500),
|
|
380
|
+
"",
|
|
381
|
+
"Raw Node Snapshot",
|
|
382
|
+
formatWorkflowCopilotBlock(node, 3500),
|
|
383
|
+
].join("\n");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function buildRunCopilotPrompt(run, intent = "ask") {
|
|
387
|
+
const workflowName = String(run?.workflowName || getWorkflowNameById(run?.workflowId) || run?.workflowId || "Unknown Workflow").trim();
|
|
388
|
+
const status = String(run?.status || "unknown").trim() || "unknown";
|
|
389
|
+
const errors = Array.isArray(run?.detail?.errors) ? run.detail.errors : [];
|
|
390
|
+
const logs = Array.isArray(run?.detail?.logs) ? run.detail.logs : [];
|
|
391
|
+
const failed = intent === "fix" || status === "failed";
|
|
392
|
+
const request = failed
|
|
393
|
+
? "Analyze why this workflow run failed. Identify the root cause, name the most likely failing node or nodes, propose the smallest concrete fix, and say whether Bosun should retry from failed state or rerun from the beginning."
|
|
394
|
+
: "Explain what happened in this workflow run, call out unusual or risky behavior, and suggest the next debugging or hardening steps.";
|
|
395
|
+
return [
|
|
396
|
+
"You are helping inside Bosun with workflow run analysis.",
|
|
397
|
+
request,
|
|
398
|
+
"",
|
|
399
|
+
"Return:",
|
|
400
|
+
"1. Short diagnosis",
|
|
401
|
+
"2. Evidence from the run",
|
|
402
|
+
failed ? "3. Concrete fix plan" : "3. Recommended next steps",
|
|
403
|
+
failed ? "4. Retry advice: retry from failed, rerun from start, or do not retry yet" : "4. Risks or follow-up checks",
|
|
404
|
+
"",
|
|
405
|
+
"Run Context",
|
|
406
|
+
`- Workflow: ${workflowName}`,
|
|
407
|
+
`- Workflow ID: ${String(run?.workflowId || "").trim() || "(unknown)"}`,
|
|
408
|
+
`- Run ID: ${String(run?.runId || "").trim() || "(unknown)"}`,
|
|
409
|
+
`- Status: ${status}`,
|
|
410
|
+
`- Started: ${formatDate(run?.startedAt)}`,
|
|
411
|
+
`- Finished: ${run?.endedAt ? formatDate(run.endedAt) : "Running"}`,
|
|
412
|
+
`- Duration: ${formatDuration(run?.duration)}`,
|
|
413
|
+
`- Active nodes: ${Number(run?.activeNodeCount || 0)}`,
|
|
414
|
+
`- Error count: ${Number(run?.errorCount || errors.length)}`,
|
|
415
|
+
`- Log count: ${Number(run?.logCount || logs.length)}`,
|
|
416
|
+
"",
|
|
417
|
+
"Node Statuses",
|
|
418
|
+
summarizeRunNodeStatuses(run),
|
|
419
|
+
"",
|
|
420
|
+
"Node Output Summaries",
|
|
421
|
+
summarizeRunNodeOutputs(run),
|
|
422
|
+
"",
|
|
423
|
+
"Errors",
|
|
424
|
+
formatWorkflowCopilotBlock(errors.slice(0, 8), 3500),
|
|
425
|
+
"",
|
|
426
|
+
"Recent Logs",
|
|
427
|
+
formatWorkflowCopilotBlock(logs.slice(-40), 4000),
|
|
428
|
+
].join("\n");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function buildRunNodeCopilotPrompt(run, nodeId, opts = {}) {
|
|
432
|
+
const safeNodeId = String(nodeId || "").trim();
|
|
433
|
+
if (!safeNodeId) return "";
|
|
434
|
+
const workflow = opts?.workflow || null;
|
|
435
|
+
const node = Array.isArray(workflow?.nodes)
|
|
436
|
+
? workflow.nodes.find((entry) => String(entry?.id || "").trim() === safeNodeId) || null
|
|
437
|
+
: null;
|
|
438
|
+
const nodeStatuses = buildNodeStatusesFromRunDetail(run);
|
|
439
|
+
const nodeOutputs = run?.detail?.nodeOutputs && typeof run.detail.nodeOutputs === "object"
|
|
440
|
+
? run.detail.nodeOutputs
|
|
441
|
+
: {};
|
|
442
|
+
const errors = Array.isArray(run?.detail?.errors) ? run.detail.errors : [];
|
|
443
|
+
const relatedErrors = errors.filter((entry) => formatWorkflowCopilotBlock(entry, 500).includes(safeNodeId));
|
|
444
|
+
const failed = String(opts?.intent || "").trim().toLowerCase() === "fix"
|
|
445
|
+
|| String(nodeStatuses[safeNodeId] || "").trim().toLowerCase() === "failed";
|
|
446
|
+
return [
|
|
447
|
+
"You are helping inside Bosun with workflow run node analysis.",
|
|
448
|
+
failed
|
|
449
|
+
? "Diagnose why this node failed or behaved incorrectly, identify the root cause, and propose the smallest concrete fix Bosun should make."
|
|
450
|
+
: "Explain what happened in this node during the run, what inputs or outputs matter, and what Bosun should inspect next.",
|
|
451
|
+
"",
|
|
452
|
+
"Return:",
|
|
453
|
+
"1. Short diagnosis",
|
|
454
|
+
"2. Evidence from this node",
|
|
455
|
+
failed ? "3. Concrete fix plan" : "3. Recommended next checks",
|
|
456
|
+
failed ? "4. Retry advice for this node or run" : "4. Risks or follow-up notes",
|
|
457
|
+
"",
|
|
458
|
+
"Run Context",
|
|
459
|
+
`- Workflow ID: ${String(run?.workflowId || workflow?.id || "").trim() || "(unknown)"}`,
|
|
460
|
+
`- Run ID: ${String(run?.runId || "").trim() || "(unknown)"}`,
|
|
461
|
+
`- Run status: ${String(run?.status || "unknown").trim() || "unknown"}`,
|
|
462
|
+
`- Started: ${formatDate(run?.startedAt)}`,
|
|
463
|
+
`- Finished: ${run?.endedAt ? formatDate(run.endedAt) : "Running"}`,
|
|
464
|
+
"",
|
|
465
|
+
"Node Context",
|
|
466
|
+
`- Node ID: ${safeNodeId}`,
|
|
467
|
+
`- Node label: ${String(node?.label || node?.name || "").trim() || "(none)"}`,
|
|
468
|
+
`- Node type: ${String(node?.type || "").trim() || "(unknown)"}`,
|
|
469
|
+
`- Node status: ${String(nodeStatuses[safeNodeId] || "unknown").trim() || "unknown"}`,
|
|
470
|
+
"",
|
|
471
|
+
"Node Config",
|
|
472
|
+
formatWorkflowCopilotBlock(node?.config || {}, 2500),
|
|
473
|
+
"",
|
|
474
|
+
"Node Output",
|
|
475
|
+
formatWorkflowCopilotBlock(nodeOutputs[safeNodeId] ?? null, 3500),
|
|
476
|
+
"",
|
|
477
|
+
"Node Errors",
|
|
478
|
+
formatWorkflowCopilotBlock(relatedErrors.length ? relatedErrors : errors.slice(0, 8), 3000),
|
|
479
|
+
].join("\n");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function fetchWorkflowCopilotPrompt(endpoint, fetchOptions = {}) {
|
|
483
|
+
const safeEndpoint = String(endpoint || "").trim();
|
|
484
|
+
if (!safeEndpoint) return null;
|
|
485
|
+
try {
|
|
486
|
+
const data = await apiFetch(safeEndpoint, fetchOptions);
|
|
487
|
+
const prompt = String(data?.prompt || "").trim();
|
|
488
|
+
return prompt || null;
|
|
489
|
+
} catch {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function startWorkflowCopilotSession({
|
|
495
|
+
endpoint = "",
|
|
496
|
+
fetchOptions = {},
|
|
497
|
+
fallbackPrompt = "",
|
|
498
|
+
title = "",
|
|
499
|
+
successToast = "",
|
|
500
|
+
} = {}) {
|
|
501
|
+
const prompt = await fetchWorkflowCopilotPrompt(endpoint, fetchOptions) || String(fallbackPrompt || "").trim();
|
|
502
|
+
return openWorkflowCopilotChat(prompt, { title, successToast });
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function openWorkflowCopilotChat(prompt, opts = {}) {
|
|
506
|
+
const content = String(prompt || "").trim();
|
|
507
|
+
if (!content) {
|
|
508
|
+
showToast("Workflow copilot prompt was empty", "error");
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
const successToast = String(opts?.successToast || "Opened workflow copilot chat").trim();
|
|
512
|
+
try {
|
|
513
|
+
const created = await createSession({
|
|
514
|
+
type: "primary",
|
|
515
|
+
reuseFresh: false,
|
|
516
|
+
...(opts?.title ? { title: String(opts.title) } : {}),
|
|
517
|
+
});
|
|
518
|
+
const session = created?.session || null;
|
|
519
|
+
const sessionId = String(session?.id || "").trim();
|
|
520
|
+
if (!sessionId) throw new Error("Session creation failed");
|
|
521
|
+
const messagePath = buildSessionApiPath(sessionId, "message", {
|
|
522
|
+
workspace: resolveSessionWorkspaceHint(session, "active"),
|
|
523
|
+
});
|
|
524
|
+
if (!messagePath) throw new Error("Session path unavailable");
|
|
525
|
+
await apiFetch(messagePath, {
|
|
526
|
+
method: "POST",
|
|
527
|
+
body: JSON.stringify({ content }),
|
|
528
|
+
});
|
|
529
|
+
const navigated = navigateTo("chat", {
|
|
530
|
+
params: { sessionId },
|
|
531
|
+
forceRefresh: true,
|
|
532
|
+
});
|
|
533
|
+
if (!navigated) {
|
|
534
|
+
showToast("Workflow copilot session started. Open Chat after resolving unsaved changes.", "info");
|
|
535
|
+
return sessionId;
|
|
536
|
+
}
|
|
537
|
+
showToast(successToast, "success");
|
|
538
|
+
return sessionId;
|
|
539
|
+
} catch (err) {
|
|
540
|
+
showToast(`Failed to start workflow copilot: ${err.message || "Unknown error"}`, "error");
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
179
545
|
/* ═══════════════════════════════════════════════════════════════
|
|
180
546
|
* API Helpers
|
|
181
547
|
* ═══════════════════════════════════════════════════════════════ */
|
|
@@ -235,6 +601,32 @@ async function saveWorkflow(def) {
|
|
|
235
601
|
}
|
|
236
602
|
}
|
|
237
603
|
|
|
604
|
+
async function exportWorkflow(workflow) {
|
|
605
|
+
if (!workflow?.id) return;
|
|
606
|
+
try {
|
|
607
|
+
const bundle = await apiFetch(`/api/workflows/${encodeURIComponent(workflow.id)}/export`);
|
|
608
|
+
if (!bundle?.files) throw new Error("No export data received");
|
|
609
|
+
|
|
610
|
+
const content = JSON.stringify({
|
|
611
|
+
_bosunExport: true,
|
|
612
|
+
projectName: bundle.projectName,
|
|
613
|
+
metadata: bundle.metadata,
|
|
614
|
+
files: bundle.files,
|
|
615
|
+
}, null, 2);
|
|
616
|
+
|
|
617
|
+
const blob = new Blob([content], { type: "application/json" });
|
|
618
|
+
const url = URL.createObjectURL(blob);
|
|
619
|
+
const a = document.createElement("a");
|
|
620
|
+
a.href = url;
|
|
621
|
+
a.download = `${bundle.projectName || "workflow"}-export.json`;
|
|
622
|
+
a.click();
|
|
623
|
+
URL.revokeObjectURL(url);
|
|
624
|
+
showToast("Workflow exported successfully", "success");
|
|
625
|
+
} catch (err) {
|
|
626
|
+
showToast("Export failed: " + (err.message || err), "error");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
238
630
|
async function deleteWorkflow(id) {
|
|
239
631
|
try {
|
|
240
632
|
await apiFetch(`/api/workflows/${id}`, { method: "DELETE" });
|
|
@@ -1564,6 +1956,44 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
1564
1956
|
const normalizeNodesForCanvas = useCallback((nodeList = []) => (
|
|
1565
1957
|
(Array.isArray(nodeList) ? nodeList : []).map((node) => ensureNodePortMetadata(node))
|
|
1566
1958
|
), [ensureNodePortMetadata]);
|
|
1959
|
+
const createWorkflowSnapshotForCopilot = useCallback(() => ({
|
|
1960
|
+
...(workflow || {}),
|
|
1961
|
+
nodes: normalizeNodesForCanvas(nodesRef.current || []),
|
|
1962
|
+
edges: Array.isArray(edgesRef.current) ? [...edgesRef.current] : [],
|
|
1963
|
+
}), [normalizeNodesForCanvas, workflow]);
|
|
1964
|
+
const openWorkflowCopilotFromCanvas = useCallback(async ({
|
|
1965
|
+
intent = "explain",
|
|
1966
|
+
nodeId = "",
|
|
1967
|
+
title = "",
|
|
1968
|
+
successToast = "",
|
|
1969
|
+
} = {}) => {
|
|
1970
|
+
const snapshot = createWorkflowSnapshotForCopilot();
|
|
1971
|
+
const safeWorkflowId = String(snapshot?.id || workflow?.id || "").trim();
|
|
1972
|
+
const safeNodeId = String(nodeId || "").trim();
|
|
1973
|
+
const fallbackNode = safeNodeId
|
|
1974
|
+
? (snapshot.nodes || []).find((entry) => String(entry?.id || "").trim() === safeNodeId) || null
|
|
1975
|
+
: null;
|
|
1976
|
+
const fallbackPrompt = safeNodeId
|
|
1977
|
+
? buildWorkflowNodePrompt(snapshot, fallbackNode, nodeTypeMap)
|
|
1978
|
+
: buildWorkflowExplainPrompt(snapshot);
|
|
1979
|
+
if (!safeWorkflowId) {
|
|
1980
|
+
return openWorkflowCopilotChat(fallbackPrompt, { title, successToast });
|
|
1981
|
+
}
|
|
1982
|
+
return startWorkflowCopilotSession({
|
|
1983
|
+
endpoint: `/api/workflows/${encodeURIComponent(safeWorkflowId)}/copilot-context`,
|
|
1984
|
+
fetchOptions: {
|
|
1985
|
+
method: "POST",
|
|
1986
|
+
body: JSON.stringify({
|
|
1987
|
+
intent,
|
|
1988
|
+
...(safeNodeId ? { nodeId: safeNodeId } : {}),
|
|
1989
|
+
workflow: snapshot,
|
|
1990
|
+
}),
|
|
1991
|
+
},
|
|
1992
|
+
fallbackPrompt,
|
|
1993
|
+
title,
|
|
1994
|
+
successToast,
|
|
1995
|
+
});
|
|
1996
|
+
}, [createWorkflowSnapshotForCopilot, nodeTypeMap, workflow]);
|
|
1567
1997
|
useEffect(() => { selectedNodeIdsRef.current = selectedNodeIds; }, [selectedNodeIds]);
|
|
1568
1998
|
useEffect(() => {
|
|
1569
1999
|
nodesRef.current = nodes;
|
|
@@ -2634,6 +3064,18 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
2634
3064
|
<${Button} variant="text" size="small" onClick=${returnToWorkflowList}>
|
|
2635
3065
|
← Back to Workflows
|
|
2636
3066
|
<//>
|
|
3067
|
+
<${Button}
|
|
3068
|
+
variant="outlined"
|
|
3069
|
+
size="small"
|
|
3070
|
+
onClick=${() => openWorkflowCopilotFromCanvas({
|
|
3071
|
+
intent: "explain",
|
|
3072
|
+
title: `Explain workflow ${workflow?.name || workflow?.id || ""}`.trim(),
|
|
3073
|
+
successToast: "Opened workflow explanation chat",
|
|
3074
|
+
})}
|
|
3075
|
+
>
|
|
3076
|
+
<span class="btn-icon">${resolveIcon("bot")}</span>
|
|
3077
|
+
Explain With Bosun
|
|
3078
|
+
<//>
|
|
2637
3079
|
<${Button} variant="contained" size="small" onClick=${() => openNodePalette()} sx=${{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
2638
3080
|
<span style="font-size: 18px;">+</span> Add Node /
|
|
2639
3081
|
<//>
|
|
@@ -2665,6 +3107,14 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
2665
3107
|
<span class="btn-icon">${resolveIcon("chart")}</span>
|
|
2666
3108
|
Runs
|
|
2667
3109
|
<//>
|
|
3110
|
+
<${Button} variant="outlined" size="small" onClick=${() => { viewMode.value = "code"; }}>
|
|
3111
|
+
<span class="btn-icon">${resolveIcon("settings")}</span>
|
|
3112
|
+
Code
|
|
3113
|
+
<//>
|
|
3114
|
+
<${Button} variant="outlined" size="small" onClick=${() => exportWorkflow(workflow)}>
|
|
3115
|
+
<span class="btn-icon">${resolveIcon("save")}</span>
|
|
3116
|
+
Export
|
|
3117
|
+
<//>
|
|
2668
3118
|
${workflow?.metadata?.installedFrom && html`<${Button}
|
|
2669
3119
|
variant="outlined"
|
|
2670
3120
|
size="small"
|
|
@@ -3165,6 +3615,21 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
3165
3615
|
<!-- Context Menu -->
|
|
3166
3616
|
${contextMenu && html`
|
|
3167
3617
|
<div class="wf-context-menu" style="position: fixed; left: ${contextMenu.x}px; top: ${contextMenu.y}px; z-index: 50;">
|
|
3618
|
+
<${MenuItem}
|
|
3619
|
+
onClick=${() => {
|
|
3620
|
+
const node = nodes.find((entry) => entry.id === contextMenu.nodeId) || null;
|
|
3621
|
+
setContextMenu(null);
|
|
3622
|
+
openWorkflowCopilotFromCanvas({
|
|
3623
|
+
intent: "node",
|
|
3624
|
+
nodeId: contextMenu.nodeId,
|
|
3625
|
+
title: `Ask Bosun about node ${node?.label || contextMenu.nodeId}`.trim(),
|
|
3626
|
+
successToast: "Opened node copilot chat",
|
|
3627
|
+
});
|
|
3628
|
+
}}
|
|
3629
|
+
>
|
|
3630
|
+
<span class="btn-icon">${resolveIcon("bot")}</span>
|
|
3631
|
+
Ask Bosun About Node
|
|
3632
|
+
<//>
|
|
3168
3633
|
<${MenuItem} onClick=${() => { setEditingNode(contextMenu.nodeId); setContextMenu(null); }}>
|
|
3169
3634
|
<span class="btn-icon">${resolveIcon("settings")}</span>
|
|
3170
3635
|
Edit Config
|
|
@@ -3193,6 +3658,12 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
3193
3658
|
inlineFieldKeys=${inlineDescriptors.map((field) => field.key)}
|
|
3194
3659
|
onUpdate=${(config) => updateNodeConfig(editingNode, config)}
|
|
3195
3660
|
onUpdateLabel=${(label) => updateNodeLabel(editingNode, label)}
|
|
3661
|
+
onAskBosun=${() => openWorkflowCopilotFromCanvas({
|
|
3662
|
+
intent: "node",
|
|
3663
|
+
nodeId: editingNode,
|
|
3664
|
+
title: `Ask Bosun about node ${editingNodeDef?.label || editingNode}`.trim(),
|
|
3665
|
+
successToast: "Opened node copilot chat",
|
|
3666
|
+
})}
|
|
3196
3667
|
onClose=${() => setEditingNode(null)}
|
|
3197
3668
|
onDelete=${() => deleteNode(editingNode)}
|
|
3198
3669
|
/>
|
|
@@ -3604,7 +4075,7 @@ function WorkflowAgentLibraryPicker({ config, onUpdate }) {
|
|
|
3604
4075
|
* Node Config Editor (right side panel)
|
|
3605
4076
|
* ═══════════════════════════════════════════════════════════════ */
|
|
3606
4077
|
|
|
3607
|
-
function NodeConfigEditor({ node, nodeTypes: types, inlineFieldKeys = [], onUpdate, onUpdateLabel, onClose, onDelete }) {
|
|
4078
|
+
function NodeConfigEditor({ node, nodeTypes: types, inlineFieldKeys = [], onUpdate, onUpdateLabel, onAskBosun, onClose, onDelete }) {
|
|
3608
4079
|
if (!node) return null;
|
|
3609
4080
|
|
|
3610
4081
|
const meta = getNodeMeta(node.type);
|
|
@@ -3656,6 +4127,17 @@ function NodeConfigEditor({ node, nodeTypes: types, inlineFieldKeys = [], onUpda
|
|
|
3656
4127
|
${typeInfo.description}
|
|
3657
4128
|
</div>
|
|
3658
4129
|
`}
|
|
4130
|
+
${typeof onAskBosun === "function" && html`
|
|
4131
|
+
<${Button}
|
|
4132
|
+
onClick=${onAskBosun}
|
|
4133
|
+
variant="outlined"
|
|
4134
|
+
size="small"
|
|
4135
|
+
sx=${{ width: "100%", marginBottom: "12px", textTransform: "none" }}
|
|
4136
|
+
>
|
|
4137
|
+
<span class="btn-icon">${resolveIcon("bot")}</span>
|
|
4138
|
+
Ask Bosun About This Node
|
|
4139
|
+
<//>
|
|
4140
|
+
`}
|
|
3659
4141
|
|
|
3660
4142
|
<!-- ═══ Smart Presets: action.run_command ═══ -->
|
|
3661
4143
|
${node.type === "action.run_command" && html`
|
|
@@ -4356,6 +4838,17 @@ function WorkflowListView() {
|
|
|
4356
4838
|
>
|
|
4357
4839
|
<span class="icon-inline">${resolveIcon("play")}</span>
|
|
4358
4840
|
<//>
|
|
4841
|
+
<${Button}
|
|
4842
|
+
variant="text"
|
|
4843
|
+
size="small"
|
|
4844
|
+
sx=${{ fontSize: '11px', textTransform: 'none' }}
|
|
4845
|
+
onClick=${(e) => {
|
|
4846
|
+
e.stopPropagation();
|
|
4847
|
+
exportWorkflow(wf);
|
|
4848
|
+
}}
|
|
4849
|
+
>
|
|
4850
|
+
<span class="icon-inline">${resolveIcon("save")}</span>
|
|
4851
|
+
<//>
|
|
4359
4852
|
${!isCore && html`<${Button} variant="text" size="small" sx=${{ fontSize: '11px', color: '#ef4444', textTransform: 'none' }} onClick=${(e) => { e.stopPropagation(); if (confirm("Delete " + wf.name + "?")) deleteWorkflow(wf.id); }}>
|
|
4360
4853
|
<span class="icon-inline">${resolveIcon("trash")}</span>
|
|
4361
4854
|
<//>`}
|
|
@@ -4700,11 +5193,36 @@ function RunHistoryView() {
|
|
|
4700
5193
|
});
|
|
4701
5194
|
return true;
|
|
4702
5195
|
}, [canLoadMoreRuns, loadingMoreRuns, runs.length, totalRuns]);
|
|
5196
|
+
const openRunCopilot = useCallback((run, intent = "ask") => {
|
|
5197
|
+
const safeRunId = String(run?.runId || "").trim();
|
|
5198
|
+
const safeIntent = String(intent || "ask").trim().toLowerCase();
|
|
5199
|
+
if (!safeRunId) return null;
|
|
5200
|
+
return startWorkflowCopilotSession({
|
|
5201
|
+
endpoint: `/api/workflows/runs/${encodeURIComponent(safeRunId)}/copilot-context?intent=${encodeURIComponent(safeIntent)}`,
|
|
5202
|
+
fallbackPrompt: buildRunCopilotPrompt(run, safeIntent),
|
|
5203
|
+
title: `${safeIntent === "fix" ? "Fix failed workflow run" : "Ask about workflow run"} ${safeRunId}`.trim(),
|
|
5204
|
+
successToast: safeIntent === "fix" ? "Opened failed-run fix chat" : "Opened workflow run analysis chat",
|
|
5205
|
+
});
|
|
5206
|
+
}, []);
|
|
5207
|
+
const openRunNodeCopilot = useCallback((run, nodeId, intent = "node", workflow = null) => {
|
|
5208
|
+
const safeRunId = String(run?.runId || "").trim();
|
|
5209
|
+
const safeNodeId = String(nodeId || "").trim();
|
|
5210
|
+
const safeIntent = String(intent || "node").trim().toLowerCase();
|
|
5211
|
+
if (!safeRunId || !safeNodeId) return null;
|
|
5212
|
+
return startWorkflowCopilotSession({
|
|
5213
|
+
endpoint: `/api/workflows/runs/${encodeURIComponent(safeRunId)}/copilot-context?intent=${encodeURIComponent(safeIntent)}&nodeId=${encodeURIComponent(safeNodeId)}`,
|
|
5214
|
+
fallbackPrompt: buildRunNodeCopilotPrompt(run, safeNodeId, { intent: safeIntent, workflow }),
|
|
5215
|
+
title: `${safeIntent === "fix" ? "Fix node" : "Ask Bosun about node"} ${safeNodeId}`.trim(),
|
|
5216
|
+
successToast: safeIntent === "fix" ? "Opened node fix chat" : "Opened node copilot chat",
|
|
5217
|
+
});
|
|
5218
|
+
}, []);
|
|
4703
5219
|
|
|
4704
5220
|
if (selectedRun) {
|
|
4705
5221
|
const statusStyles = getRunStatusBadgeStyles(selectedRun.status);
|
|
4706
5222
|
const logs = Array.isArray(selectedRun?.detail?.logs) ? selectedRun.detail.logs : [];
|
|
4707
5223
|
const errors = Array.isArray(selectedRun?.detail?.errors) ? selectedRun.detail.errors : [];
|
|
5224
|
+
const currentWorkflow =
|
|
5225
|
+
(workflows.value || []).find((workflow) => workflow?.id === selectedRun.workflowId) || null;
|
|
4708
5226
|
const nodeStatuses = buildNodeStatusesFromRunDetail(selectedRun);
|
|
4709
5227
|
const nodeOutputs = selectedRun?.detail?.nodeOutputs || {};
|
|
4710
5228
|
const nodeIds = Object.keys(nodeStatuses).sort((a, b) => {
|
|
@@ -4731,6 +5249,63 @@ function RunHistoryView() {
|
|
|
4731
5249
|
${selectedRun.workflowId && html`<${Button} variant="text" size="small" onClick=${() => openWorkflowCanvas(selectedRun.workflowId)}>Open Workflow<//>`}
|
|
4732
5250
|
<h2 style="margin: 0; font-size: 18px; font-weight: 700;">Run Details</h2>
|
|
4733
5251
|
<${Button} variant="text" size="small" onClick=${() => loadRunDetail(selectedRun.runId)}>Refresh<//>
|
|
5252
|
+
<${Button}
|
|
5253
|
+
variant="outlined"
|
|
5254
|
+
size="small"
|
|
5255
|
+
onClick=${() => openRunCopilot(selectedRun, "ask")}
|
|
5256
|
+
>
|
|
5257
|
+
<span class="btn-icon">${resolveIcon("bot")}</span>
|
|
5258
|
+
Ask Bosun
|
|
5259
|
+
<//>
|
|
5260
|
+
${selectedRun.status === "failed" && html`
|
|
5261
|
+
<${Button}
|
|
5262
|
+
variant="contained"
|
|
5263
|
+
size="small"
|
|
5264
|
+
color="error"
|
|
5265
|
+
onClick=${() => openRunCopilot(selectedRun, "fix")}
|
|
5266
|
+
>
|
|
5267
|
+
<span class="btn-icon">${resolveIcon("settings")}</span>
|
|
5268
|
+
Fix With Bosun
|
|
5269
|
+
<//>
|
|
5270
|
+
`}
|
|
5271
|
+
${selectedRun.status === "failed" && html`
|
|
5272
|
+
<${Button}
|
|
5273
|
+
variant="contained"
|
|
5274
|
+
size="small"
|
|
5275
|
+
color="warning"
|
|
5276
|
+
onClick=${async () => {
|
|
5277
|
+
try {
|
|
5278
|
+
await apiFetch("/api/workflows/runs/" + encodeURIComponent(selectedRun.runId) + "/retry", { method: "POST" });
|
|
5279
|
+
showToast("Run retry initiated", "success");
|
|
5280
|
+
setTimeout(() => loadRunDetail(selectedRun.runId), 1000);
|
|
5281
|
+
} catch (err) {
|
|
5282
|
+
showToast("Retry failed: " + (err.message || err), "error");
|
|
5283
|
+
}
|
|
5284
|
+
}}
|
|
5285
|
+
>
|
|
5286
|
+
<span class="btn-icon">${resolveIcon("refresh")}</span>
|
|
5287
|
+
Retry Run
|
|
5288
|
+
<//>
|
|
5289
|
+
`}
|
|
5290
|
+
${selectedRun.status === "running" && html`
|
|
5291
|
+
<${Button}
|
|
5292
|
+
variant="contained"
|
|
5293
|
+
size="small"
|
|
5294
|
+
color="error"
|
|
5295
|
+
onClick=${async () => {
|
|
5296
|
+
try {
|
|
5297
|
+
await apiFetch("/api/workflows/runs/" + encodeURIComponent(selectedRun.runId) + "/cancel", { method: "POST" });
|
|
5298
|
+
showToast("Run cancellation requested", "success");
|
|
5299
|
+
setTimeout(() => loadRunDetail(selectedRun.runId), 1000);
|
|
5300
|
+
} catch (err) {
|
|
5301
|
+
showToast("Cancel failed: " + (err.message || err), "error");
|
|
5302
|
+
}
|
|
5303
|
+
}}
|
|
5304
|
+
>
|
|
5305
|
+
<span class="btn-icon">${resolveIcon("close")}</span>
|
|
5306
|
+
Stop Run
|
|
5307
|
+
<//>
|
|
5308
|
+
`}
|
|
4734
5309
|
</div>
|
|
4735
5310
|
|
|
4736
5311
|
<div style="background: var(--color-bg-secondary, #1a1f2e); border-radius: 10px; border: 1px solid var(--color-border, #2a3040); padding: 14px; margin-bottom: 12px;">
|
|
@@ -4783,6 +5358,27 @@ function RunHistoryView() {
|
|
|
4783
5358
|
${nodeNarrative ? html`<div style="margin-top: ${nodeSummary ? "6px" : "0"};"><b>Narrative:</b> ${nodeNarrative}</div>` : ""}
|
|
4784
5359
|
</div>
|
|
4785
5360
|
`}
|
|
5361
|
+
<div style="display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px;">
|
|
5362
|
+
<${Button}
|
|
5363
|
+
variant="outlined"
|
|
5364
|
+
size="small"
|
|
5365
|
+
onClick=${() => openRunNodeCopilot(selectedRun, nodeId, "node", currentWorkflow)}
|
|
5366
|
+
>
|
|
5367
|
+
<span class="btn-icon">${resolveIcon("bot")}</span>
|
|
5368
|
+
Ask Bosun About This Node
|
|
5369
|
+
<//>
|
|
5370
|
+
${String(nodeStatus || "").trim().toLowerCase() === "failed" && html`
|
|
5371
|
+
<${Button}
|
|
5372
|
+
variant="contained"
|
|
5373
|
+
size="small"
|
|
5374
|
+
color="error"
|
|
5375
|
+
onClick=${() => openRunNodeCopilot(selectedRun, nodeId, "fix", currentWorkflow)}
|
|
5376
|
+
>
|
|
5377
|
+
<span class="btn-icon">${resolveIcon("settings")}</span>
|
|
5378
|
+
Fix This Node
|
|
5379
|
+
<//>
|
|
5380
|
+
`}
|
|
5381
|
+
</div>
|
|
4786
5382
|
<pre style="margin-top: 8px; white-space: pre-wrap; word-break: break-word; font-size: 11px; color: #c9d1d9; background: #111827; border-radius: 6px; padding: 8px;">${safePrettyJson(nodeOutput)}</pre>
|
|
4787
5383
|
</details>
|
|
4788
5384
|
`;
|
|
@@ -4977,6 +5573,171 @@ function RunHistoryView() {
|
|
|
4977
5573
|
`;
|
|
4978
5574
|
}
|
|
4979
5575
|
|
|
5576
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
5577
|
+
* Code View — JSON Editor for Workflows
|
|
5578
|
+
* ═══════════════════════════════════════════════════════════════ */
|
|
5579
|
+
|
|
5580
|
+
function WorkflowCodeView({ workflow, onSave }) {
|
|
5581
|
+
const [code, setCode] = useState("");
|
|
5582
|
+
const [originalCode, setOriginalCode] = useState("");
|
|
5583
|
+
const [errors, setErrors] = useState([]);
|
|
5584
|
+
const [isDirty, setIsDirty] = useState(false);
|
|
5585
|
+
const [saving, setSaving] = useState(false);
|
|
5586
|
+
const textareaRef = useRef(null);
|
|
5587
|
+
|
|
5588
|
+
useEffect(() => {
|
|
5589
|
+
if (!workflow?.id) return;
|
|
5590
|
+
apiFetch(`/api/workflows/${encodeURIComponent(workflow.id)}/code`)
|
|
5591
|
+
.then(data => {
|
|
5592
|
+
if (data?.code) {
|
|
5593
|
+
setCode(data.code);
|
|
5594
|
+
setOriginalCode(data.code);
|
|
5595
|
+
setErrors([]);
|
|
5596
|
+
setIsDirty(false);
|
|
5597
|
+
}
|
|
5598
|
+
})
|
|
5599
|
+
.catch(() => showToast("Failed to load workflow code", "error"));
|
|
5600
|
+
}, [workflow?.id]);
|
|
5601
|
+
|
|
5602
|
+
const handleCodeChange = useCallback((e) => {
|
|
5603
|
+
const newCode = e.target.value;
|
|
5604
|
+
setCode(newCode);
|
|
5605
|
+
setIsDirty(newCode !== originalCode);
|
|
5606
|
+
|
|
5607
|
+
try {
|
|
5608
|
+
JSON.parse(newCode);
|
|
5609
|
+
setErrors([]);
|
|
5610
|
+
} catch (err) {
|
|
5611
|
+
const lineMatch = String(err.message).match(/position\s+(\d+)/i);
|
|
5612
|
+
let line;
|
|
5613
|
+
if (lineMatch) {
|
|
5614
|
+
const pos = parseInt(lineMatch[1], 10);
|
|
5615
|
+
line = newCode.slice(0, pos).split("\n").length;
|
|
5616
|
+
}
|
|
5617
|
+
setErrors([{ message: err.message, line }]);
|
|
5618
|
+
}
|
|
5619
|
+
}, [originalCode]);
|
|
5620
|
+
|
|
5621
|
+
const handleSave = useCallback(async () => {
|
|
5622
|
+
if (!workflow?.id || !isDirty) return;
|
|
5623
|
+
setSaving(true);
|
|
5624
|
+
try {
|
|
5625
|
+
const resp = await apiFetch(`/api/workflows/${encodeURIComponent(workflow.id)}/code`, {
|
|
5626
|
+
method: "PUT",
|
|
5627
|
+
body: JSON.stringify({ code }),
|
|
5628
|
+
});
|
|
5629
|
+
if (resp?.ok) {
|
|
5630
|
+
showToast("Workflow updated from code", "success");
|
|
5631
|
+
setOriginalCode(code);
|
|
5632
|
+
setIsDirty(false);
|
|
5633
|
+
if (resp.workflow && onSave) onSave(resp.workflow);
|
|
5634
|
+
} else if (resp?.errors) {
|
|
5635
|
+
setErrors(resp.errors.map(e => typeof e === "string" ? { message: e } : e));
|
|
5636
|
+
showToast("Validation errors — check below", "error");
|
|
5637
|
+
}
|
|
5638
|
+
} catch (err) {
|
|
5639
|
+
showToast("Failed to save: " + (err.message || err), "error");
|
|
5640
|
+
} finally {
|
|
5641
|
+
setSaving(false);
|
|
5642
|
+
}
|
|
5643
|
+
}, [workflow?.id, code, isDirty, onSave]);
|
|
5644
|
+
|
|
5645
|
+
const handleRevert = useCallback(() => {
|
|
5646
|
+
setCode(originalCode);
|
|
5647
|
+
setIsDirty(false);
|
|
5648
|
+
setErrors([]);
|
|
5649
|
+
}, [originalCode]);
|
|
5650
|
+
|
|
5651
|
+
const handleFormat = useCallback(() => {
|
|
5652
|
+
try {
|
|
5653
|
+
const parsed = JSON.parse(code);
|
|
5654
|
+
const formatted = JSON.stringify(parsed, null, 2);
|
|
5655
|
+
setCode(formatted);
|
|
5656
|
+
setIsDirty(formatted !== originalCode);
|
|
5657
|
+
setErrors([]);
|
|
5658
|
+
} catch {
|
|
5659
|
+
showToast("Cannot format — fix JSON syntax errors first", "warning");
|
|
5660
|
+
}
|
|
5661
|
+
}, [code, originalCode]);
|
|
5662
|
+
|
|
5663
|
+
const lineCount = code.split("\n").length;
|
|
5664
|
+
|
|
5665
|
+
return html`
|
|
5666
|
+
<div style="padding: 0 4px; display: flex; flex-direction: column; height: calc(100vh - 120px);">
|
|
5667
|
+
<!-- Toolbar -->
|
|
5668
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap;">
|
|
5669
|
+
<${Button} variant="text" size="small" onClick=${() => { viewMode.value = "canvas"; }}>
|
|
5670
|
+
← Back to Canvas
|
|
5671
|
+
<//>
|
|
5672
|
+
<h2 style="margin: 0; font-size: 18px; font-weight: 700; flex: 1;">
|
|
5673
|
+
Code View: ${workflow?.name || "Workflow"}
|
|
5674
|
+
</h2>
|
|
5675
|
+
<${Button} variant="outlined" size="small" onClick=${handleFormat} disabled=${saving}>
|
|
5676
|
+
Format JSON
|
|
5677
|
+
<//>
|
|
5678
|
+
<${Button} variant="outlined" size="small" onClick=${handleRevert} disabled=${!isDirty || saving}>
|
|
5679
|
+
Revert
|
|
5680
|
+
<//>
|
|
5681
|
+
<${Button} variant="contained" size="small" onClick=${handleSave} disabled=${!isDirty || errors.length > 0 || saving}>
|
|
5682
|
+
${saving ? "Saving…" : "Save"}
|
|
5683
|
+
<//>
|
|
5684
|
+
${isDirty && html`
|
|
5685
|
+
<${Chip} label="Unsaved changes" size="small" color="warning" variant="outlined" />
|
|
5686
|
+
`}
|
|
5687
|
+
</div>
|
|
5688
|
+
|
|
5689
|
+
<!-- Error bar -->
|
|
5690
|
+
${errors.length > 0 && html`
|
|
5691
|
+
<${Alert} severity="error" style="margin-bottom: 8px; font-size: 12px;">
|
|
5692
|
+
${errors.map((e, i) => html`
|
|
5693
|
+
<div key=${i}>${e.line ? `Line ${e.line}: ` : ""}${e.message}</div>
|
|
5694
|
+
`)}
|
|
5695
|
+
<//>
|
|
5696
|
+
`}
|
|
5697
|
+
|
|
5698
|
+
<!-- Editor area -->
|
|
5699
|
+
<div style="flex: 1; display: flex; border: 1px solid ${errors.length > 0 ? '#ef4444' : 'var(--color-border, #2a3040)'}; border-radius: 8px; overflow: hidden; background: #0d1117;">
|
|
5700
|
+
<!-- Line numbers -->
|
|
5701
|
+
<div style="padding: 12px 8px; background: #161b22; color: #484f58; font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: 12px; line-height: 1.6; text-align: right; user-select: none; min-width: 48px; border-right: 1px solid #21262d;">
|
|
5702
|
+
${Array.from({ length: lineCount }, (_, i) => html`<div key=${i}>${i + 1}</div>`)}
|
|
5703
|
+
</div>
|
|
5704
|
+
|
|
5705
|
+
<!-- Code textarea -->
|
|
5706
|
+
<textarea
|
|
5707
|
+
ref=${textareaRef}
|
|
5708
|
+
value=${code}
|
|
5709
|
+
onInput=${handleCodeChange}
|
|
5710
|
+
spellcheck=${false}
|
|
5711
|
+
style="flex: 1; padding: 12px; background: transparent; color: #e6edf3; font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: 12px; line-height: 1.6; border: none; outline: none; resize: none; tab-size: 2; white-space: pre; overflow: auto;"
|
|
5712
|
+
onKeyDown=${(e) => {
|
|
5713
|
+
if (e.key === "Tab") {
|
|
5714
|
+
e.preventDefault();
|
|
5715
|
+
const ta = e.target;
|
|
5716
|
+
const start = ta.selectionStart;
|
|
5717
|
+
const end = ta.selectionEnd;
|
|
5718
|
+
const newVal = code.slice(0, start) + " " + code.slice(end);
|
|
5719
|
+
setCode(newVal);
|
|
5720
|
+
setIsDirty(newVal !== originalCode);
|
|
5721
|
+
requestAnimationFrame(() => { ta.selectionStart = ta.selectionEnd = start + 2; });
|
|
5722
|
+
}
|
|
5723
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
|
5724
|
+
e.preventDefault();
|
|
5725
|
+
handleSave();
|
|
5726
|
+
}
|
|
5727
|
+
}}
|
|
5728
|
+
/>
|
|
5729
|
+
</div>
|
|
5730
|
+
|
|
5731
|
+
<!-- Status bar -->
|
|
5732
|
+
<div style="display: flex; align-items: center; gap: 12px; padding: 6px 4px; font-size: 11px; color: var(--color-text-secondary, #8b95a5);">
|
|
5733
|
+
<span>${lineCount} lines</span>
|
|
5734
|
+
<span>${code.length} characters</span>
|
|
5735
|
+
<span>JSON${errors.length === 0 ? " ✓" : " ✗"}</span>
|
|
5736
|
+
</div>
|
|
5737
|
+
</div>
|
|
5738
|
+
`;
|
|
5739
|
+
}
|
|
5740
|
+
|
|
4980
5741
|
/* ═══════════════════════════════════════════════════════════════
|
|
4981
5742
|
* Main Tab Export
|
|
4982
5743
|
* ═══════════════════════════════════════════════════════════════ */
|
|
@@ -5246,7 +6007,9 @@ export function WorkflowsTab() {
|
|
|
5246
6007
|
class="wf-theme"
|
|
5247
6008
|
style="padding: 8px; --color-bg: var(--bg-card); --color-bg-secondary: var(--bg-secondary); --color-border: var(--border); --color-text: var(--text-primary); --color-text-secondary: var(--text-secondary);"
|
|
5248
6009
|
>
|
|
5249
|
-
${mode === "
|
|
6010
|
+
${mode === "code" && activeWorkflow.value
|
|
6011
|
+
? html`<${WorkflowCodeView} workflow=${activeWorkflow.value} onSave=${(wf) => { activeWorkflow.value = wf; viewMode.value = "canvas"; }} />`
|
|
6012
|
+
: mode === "canvas" && activeWorkflow.value
|
|
5250
6013
|
? html`<${WorkflowCanvas} workflow=${activeWorkflow.value} nodeTypes=${nodeTypes.value} />`
|
|
5251
6014
|
: mode === "runs"
|
|
5252
6015
|
? html`<${RunHistoryView} />`
|