agentweaver 0.1.18 → 0.1.20

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 (50) hide show
  1. package/README.md +54 -6
  2. package/dist/artifacts.js +9 -0
  3. package/dist/executors/git-commit-executor.js +24 -6
  4. package/dist/flow-state.js +3 -8
  5. package/dist/git/git-diff-parser.js +223 -0
  6. package/dist/git/git-service.js +562 -0
  7. package/dist/git/git-stage-selection.js +24 -0
  8. package/dist/git/git-status-parser.js +171 -0
  9. package/dist/git/git-types.js +1 -0
  10. package/dist/index.js +454 -108
  11. package/dist/interactive/auto-flow.js +644 -0
  12. package/dist/interactive/controller.js +489 -7
  13. package/dist/interactive/progress.js +194 -1
  14. package/dist/interactive/state.js +34 -0
  15. package/dist/interactive/web/index.js +237 -5
  16. package/dist/interactive/web/protocol.js +222 -1
  17. package/dist/interactive/web/server.js +497 -3
  18. package/dist/interactive/web/static/app.js +2462 -37
  19. package/dist/interactive/web/static/index.html +113 -11
  20. package/dist/interactive/web/static/styles.css +1 -1
  21. package/dist/interactive/web/static/styles.input.css +1383 -149
  22. package/dist/pipeline/auto-flow-blocks.js +307 -0
  23. package/dist/pipeline/auto-flow-config.js +273 -0
  24. package/dist/pipeline/auto-flow-identity.js +49 -0
  25. package/dist/pipeline/auto-flow-presets.js +52 -0
  26. package/dist/pipeline/auto-flow-resolver.js +830 -0
  27. package/dist/pipeline/auto-flow-types.js +17 -0
  28. package/dist/pipeline/context.js +1 -0
  29. package/dist/pipeline/declarative-flows.js +27 -1
  30. package/dist/pipeline/flow-specs/auto-common-guided.json +11 -0
  31. package/dist/pipeline/flow-specs/auto-golang.json +12 -1
  32. package/dist/pipeline/flow-specs/bugz/bug-analyze.json +54 -1
  33. package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +19 -1
  34. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +33 -1
  35. package/dist/pipeline/flow-specs/review/review-project.json +19 -1
  36. package/dist/pipeline/flow-specs/task-source/manual-jira-input.json +70 -0
  37. package/dist/pipeline/node-registry.js +9 -0
  38. package/dist/pipeline/nodes/codex-prompt-node.js +8 -1
  39. package/dist/pipeline/nodes/flow-run-node.js +5 -3
  40. package/dist/pipeline/nodes/git-status-node.js +2 -168
  41. package/dist/pipeline/nodes/manual-jira-task-input-node.js +146 -0
  42. package/dist/pipeline/nodes/opencode-prompt-node.js +8 -1
  43. package/dist/pipeline/nodes/plan-codex-node.js +8 -1
  44. package/dist/pipeline/spec-loader.js +14 -4
  45. package/dist/runtime/artifact-catalog.js +403 -0
  46. package/dist/runtime/settings.js +114 -0
  47. package/dist/scope.js +14 -4
  48. package/package.json +1 -1
  49. package/dist/pipeline/flow-specs/auto-common.json +0 -179
  50. package/dist/pipeline/flow-specs/auto-simple.json +0 -141
@@ -1,3 +1,4 @@
1
+ import { buildAutoFlowEditorViewModel } from "./auto-flow.js";
1
2
  export function displayPhaseId(phase) {
2
3
  let result = phase.id;
3
4
  const values = Object.entries(phase.repeatVars)
@@ -139,7 +140,7 @@ export function visiblePhaseItems(flow, flowState) {
139
140
  return true;
140
141
  });
141
142
  }
142
- export function buildProgressViewModel(flow, flowState) {
143
+ export function buildProgressViewModel(flow, flowState, options = {}) {
143
144
  if (!flow) {
144
145
  return {
145
146
  flow: null,
@@ -147,6 +148,9 @@ export function buildProgressViewModel(flow, flowState) {
147
148
  anchorIndex: null,
148
149
  };
149
150
  }
151
+ if (flow.autoFlow) {
152
+ return buildAutoFlowProgressViewModel(flow, flowState, options);
153
+ }
150
154
  const items = [];
151
155
  let anchorIndex = null;
152
156
  let sawExecutedItem = false;
@@ -243,3 +247,192 @@ export function buildProgressViewModel(flow, flowState) {
243
247
  anchorIndex,
244
248
  };
245
249
  }
250
+ function phaseOrder(flow) {
251
+ return new Map(flow.phases.map((phase, index) => [phase.id, index]));
252
+ }
253
+ function stoppedPhaseId(flowState) {
254
+ const terminationReason = flowState?.terminationReason ?? "";
255
+ const match = /^Stopped by ([^:]+):/.exec(terminationReason);
256
+ return match?.[1] ?? null;
257
+ }
258
+ function mapRuntimeStatus(status) {
259
+ if (status === "done") {
260
+ return "success";
261
+ }
262
+ return status;
263
+ }
264
+ function lastRuntimeBlockId(slots, flow, flowState) {
265
+ const order = phaseOrder(flow);
266
+ let result = null;
267
+ for (const slot of slots) {
268
+ for (const block of slot.blocks) {
269
+ if (!block.phaseId) {
270
+ continue;
271
+ }
272
+ const phaseState = flowState?.phases.find((phase) => phase.id === block.phaseId);
273
+ if (!phaseState || phaseState.status === "pending" || phaseState.status === "skipped") {
274
+ continue;
275
+ }
276
+ const phaseIndex = order.get(block.phaseId) ?? -1;
277
+ if (!result || phaseIndex >= result.phaseIndex) {
278
+ result = { blockId: block.blockId, phaseIndex };
279
+ }
280
+ }
281
+ }
282
+ return result?.blockId ?? null;
283
+ }
284
+ function runtimeStatusForBlock(block, input) {
285
+ if (block.status === "invalid" || block.status === "disabled" || block.status === "blocked" || block.status === "empty") {
286
+ return block.status;
287
+ }
288
+ if (!block.phaseId) {
289
+ return block.status;
290
+ }
291
+ const phaseState = input.flowState?.phases.find((phase) => phase.id === block.phaseId) ?? null;
292
+ const failed = input.failedFlowId === input.flow.id;
293
+ if (failed && (phaseState?.status === "running" || input.fallbackFailedBlockId === block.blockId)) {
294
+ return "failed";
295
+ }
296
+ if (input.waitingForUserInput && phaseState?.status === "running") {
297
+ return "waiting-user";
298
+ }
299
+ const stoppedId = input.flowState?.terminationOutcome === "stopped" ? stoppedPhaseId(input.flowState) : null;
300
+ if (stoppedId && stoppedId === block.phaseId) {
301
+ return "stopped";
302
+ }
303
+ if (phaseState) {
304
+ return mapRuntimeStatus(phaseState.status);
305
+ }
306
+ if (input.flowState?.terminated && stoppedId) {
307
+ const order = phaseOrder(input.flow);
308
+ const stoppedIndex = order.get(stoppedId) ?? -1;
309
+ const currentIndex = order.get(block.phaseId) ?? -1;
310
+ if (stoppedIndex >= 0 && currentIndex > stoppedIndex) {
311
+ return "skipped";
312
+ }
313
+ }
314
+ return block.status;
315
+ }
316
+ function aggregateSlotStatus(slot, blockStatuses) {
317
+ if (slot.status === "invalid" || blockStatuses.some((status) => status === "invalid")) {
318
+ return "invalid";
319
+ }
320
+ if (slot.blocks.length === 0) {
321
+ return "empty";
322
+ }
323
+ for (const status of ["failed", "stopped", "waiting-user", "running"]) {
324
+ if (blockStatuses.includes(status)) {
325
+ return status;
326
+ }
327
+ }
328
+ if (blockStatuses.every((status) => status === "disabled")) {
329
+ return "disabled";
330
+ }
331
+ const executableStatuses = blockStatuses.filter((status) => status !== "disabled");
332
+ if (executableStatuses.length > 0 && executableStatuses.every((status) => status === "success" || status === "skipped")) {
333
+ return executableStatuses.every((status) => status === "skipped") ? "skipped" : "success";
334
+ }
335
+ if (blockStatuses.some((status) => status === "blocked")) {
336
+ return "blocked";
337
+ }
338
+ return "pending";
339
+ }
340
+ function rememberProgressAnchor(items, status, state) {
341
+ if (status === "running" || status === "waiting-user" || status === "failed" || status === "stopped" || status === "invalid") {
342
+ state.anchorIndex = items.length;
343
+ state.sawExecutedItem = true;
344
+ return;
345
+ }
346
+ if (status === "done" || status === "success" || status === "skipped" || status === "disabled") {
347
+ state.sawExecutedItem = true;
348
+ return;
349
+ }
350
+ if (status === "pending" && state.sawExecutedItem && state.anchorIndex === null) {
351
+ state.anchorIndex = items.length;
352
+ }
353
+ }
354
+ function buildAutoFlowProgressViewModel(flow, flowState, options) {
355
+ if (!flow.autoFlow) {
356
+ return {
357
+ flow,
358
+ items: [],
359
+ anchorIndex: null,
360
+ };
361
+ }
362
+ const editor = buildAutoFlowEditorViewModel(flow.autoFlow, {
363
+ ...(flow.autoFlow.diagnostics && flow.autoFlow.diagnostics.length > 0 ? { diagnostics: flow.autoFlow.diagnostics } : {}),
364
+ ...(flow.autoFlow.lastMessage ? { lastMessage: flow.autoFlow.lastMessage } : {}),
365
+ });
366
+ const items = [];
367
+ const anchorState = { anchorIndex: null, sawExecutedItem: false };
368
+ const fallbackFailedBlockId = options.failedFlowId === flow.id
369
+ ? lastRuntimeBlockId(editor.slots, flow, flowState)
370
+ : null;
371
+ for (const slot of editor.slots) {
372
+ const blockRows = slot.blocks.map((block) => ({
373
+ block,
374
+ status: runtimeStatusForBlock(block, {
375
+ flow,
376
+ flowState,
377
+ ...(options.failedFlowId !== undefined ? { failedFlowId: options.failedFlowId } : {}),
378
+ ...(options.waitingForUserInput !== undefined ? { waitingForUserInput: options.waitingForUserInput } : {}),
379
+ fallbackFailedBlockId,
380
+ }),
381
+ }));
382
+ const status = aggregateSlotStatus(slot, blockRows.map((row) => row.status));
383
+ rememberProgressAnchor(items, status, anchorState);
384
+ items.push({
385
+ kind: "slot",
386
+ label: slot.title,
387
+ depth: 0,
388
+ status,
389
+ detail: slot.reason,
390
+ slotId: slot.slotId,
391
+ });
392
+ for (const { block, status: blockRuntimeStatus } of blockRows) {
393
+ rememberProgressAnchor(items, blockRuntimeStatus, anchorState);
394
+ items.push({
395
+ kind: "block",
396
+ label: block.title,
397
+ depth: 1,
398
+ status: blockRuntimeStatus,
399
+ detail: block.reason,
400
+ slotId: block.slotId,
401
+ blockId: block.blockId,
402
+ locked: block.locked,
403
+ enabled: block.enabled,
404
+ });
405
+ for (const diagnostic of block.diagnostics) {
406
+ rememberProgressAnchor(items, "invalid", anchorState);
407
+ const detail = diagnostic.paramName ? `${diagnostic.blockId ?? "flow"}.${diagnostic.paramName}` : diagnostic.blockId;
408
+ items.push({
409
+ kind: "block",
410
+ label: diagnostic.message,
411
+ depth: 2,
412
+ status: "invalid",
413
+ ...(detail ? { detail } : {}),
414
+ slotId: block.slotId,
415
+ blockId: block.blockId,
416
+ locked: block.locked,
417
+ enabled: block.enabled,
418
+ });
419
+ }
420
+ }
421
+ }
422
+ if (flowState?.terminated) {
423
+ const terminationOutcome = flowState.terminationOutcome ?? "success";
424
+ const status = terminationOutcome === "stopped" ? "stopped" : "success";
425
+ items.push({
426
+ kind: "termination",
427
+ label: terminationOutcome === "stopped" ? "Flow stopped before completion" : "Flow completed successfully",
428
+ detail: `Reason: ${flowState.terminationReason ?? "flow terminated"}`,
429
+ depth: 0,
430
+ status,
431
+ });
432
+ }
433
+ return {
434
+ flow,
435
+ items,
436
+ anchorIndex: anchorState.anchorIndex,
437
+ };
438
+ }
@@ -1,4 +1,28 @@
1
1
  import { buildFlowTree, collectInitiallyExpandedFolderKeys, computeVisibleFlowItems, makeFlowKey } from "./tree.js";
2
+ export function createUnavailableGitWorkspace(message = "Git workspace has not been refreshed yet.") {
3
+ return {
4
+ available: false,
5
+ repositoryRoot: null,
6
+ branch: null,
7
+ detachedHead: false,
8
+ clean: true,
9
+ upstream: null,
10
+ ahead: 0,
11
+ behind: 0,
12
+ lastCommit: null,
13
+ changedFiles: [],
14
+ branches: [],
15
+ remotes: [],
16
+ canPush: false,
17
+ pushDisabledReason: "Git repository is not available.",
18
+ warnings: [],
19
+ error: message,
20
+ refreshedAt: null,
21
+ selectedPaths: [],
22
+ commitMessage: "",
23
+ operation: { status: "idle" },
24
+ };
25
+ }
2
26
  export function createInitialInteractiveState(options) {
3
27
  const flowTree = buildFlowTree(options.flows);
4
28
  const expandedFlowFolders = new Set(collectInitiallyExpandedFolderKeys(flowTree));
@@ -31,5 +55,15 @@ export function createInitialInteractiveState(options) {
31
55
  summaryScrollOffset: 0,
32
56
  logScrollOffset: 0,
33
57
  helpScrollOffset: 0,
58
+ artifactExplorer: {
59
+ available: false,
60
+ open: false,
61
+ scopeKey: null,
62
+ runId: null,
63
+ status: "unavailable",
64
+ label: "Artifact Explorer",
65
+ message: "Artifacts are available after a Web UI workflow run completes.",
66
+ },
67
+ gitWorkspace: createUnavailableGitWorkspace(),
34
68
  };
35
69
  }
@@ -1,8 +1,11 @@
1
1
  import process from "node:process";
2
2
  import { writeSync } from "node:fs";
3
3
  import { FlowInterruptedError } from "../../errors.js";
4
+ import { listArtifactCatalog } from "../../runtime/artifact-catalog.js";
5
+ import { createArtifactRegistry } from "../../runtime/artifact-registry.js";
6
+ import { loadAgentWeaverSettings, updateWebUiSettings } from "../../runtime/settings.js";
4
7
  import { InteractiveSessionController } from "../controller.js";
5
- import { startWebServer } from "./server.js";
8
+ import { startWebServer, } from "./server.js";
6
9
  function actionId(action) {
7
10
  return "actionId" in action ? action.actionId : undefined;
8
11
  }
@@ -12,8 +15,20 @@ export function createWebInteractiveSession(options, webOptions = {}) {
12
15
  let unsubscribe = null;
13
16
  let mounted = false;
14
17
  let shuttingDown = false;
18
+ let activeScopeKey = options.scopeKey;
19
+ let artifactRestoreGeneration = 0;
20
+ let webUiSettings = loadAgentWeaverSettings().webUi;
21
+ const artifactCatalogProvider = webOptions.getArtifactCatalog ?? ((input) => {
22
+ const explorerScopeKey = controller.getViewModel().artifactExplorer.scopeKey;
23
+ const requestedScopeKey = input?.scopeKey;
24
+ const scopeKey = requestedScopeKey && requestedScopeKey === explorerScopeKey ? requestedScopeKey : activeScopeKey;
25
+ return listArtifactCatalog({
26
+ scopeKey,
27
+ artifactRegistry: createArtifactRegistry(),
28
+ });
29
+ });
15
30
  function snapshot() {
16
- return { type: "snapshot", viewModel: controller.getViewModel() };
31
+ return { type: "snapshot", viewModel: controller.getViewModel(), settings: webUiSettings };
17
32
  }
18
33
  function sendError(client, message, id) {
19
34
  const event = { type: "error", message, ...(id ? { actionId: id } : {}) };
@@ -24,7 +39,119 @@ export function createWebInteractiveSession(options, webOptions = {}) {
24
39
  server?.broadcast(event);
25
40
  }
26
41
  }
42
+ function uniqueStrings(values) {
43
+ return values.filter((value, index, allValues) => (typeof value === "string" && value.length > 0 && allValues.indexOf(value) === index));
44
+ }
45
+ function collectPublishedArtifactRunIds(executionState) {
46
+ const runIds = [];
47
+ for (const phase of executionState?.phases ?? []) {
48
+ for (const step of phase.steps) {
49
+ for (const artifact of step.publishedArtifacts ?? []) {
50
+ const runId = artifact.manifest?.run_id;
51
+ if (runId) {
52
+ runIds.push(runId);
53
+ }
54
+ }
55
+ }
56
+ }
57
+ return uniqueStrings(runIds);
58
+ }
59
+ function candidateRunIds() {
60
+ const executionState = controller.getCurrentFlowExecutionState();
61
+ return uniqueStrings([
62
+ executionState?.runId ?? null,
63
+ executionState?.publicationRunId ?? null,
64
+ ...collectPublishedArtifactRunIds(executionState),
65
+ ]);
66
+ }
67
+ async function resolveArtifactExplorerRunMetadata(scopeKey) {
68
+ const candidates = candidateRunIds();
69
+ const preferredRunId = candidates[0] ?? null;
70
+ try {
71
+ const catalog = await artifactCatalogProvider({
72
+ scopeKey,
73
+ });
74
+ if (!catalog || catalog.scopeKey !== scopeKey) {
75
+ return { runId: preferredRunId };
76
+ }
77
+ const markdownArtifactCount = catalog.items.filter((item) => item.scopeKey === scopeKey && item.kind === "markdown").length;
78
+ if (candidates.length === 0) {
79
+ return {
80
+ runId: null,
81
+ artifactCount: markdownArtifactCount,
82
+ };
83
+ }
84
+ const matchingRunIds = candidates.filter((candidate) => (catalog.items.some((item) => item.scopeKey === scopeKey && item.runId === candidate && item.kind === "markdown")));
85
+ if (matchingRunIds.length > 0) {
86
+ return {
87
+ runId: matchingRunIds[0] ?? preferredRunId,
88
+ ...(matchingRunIds.length > 1 ? { runIds: matchingRunIds } : {}),
89
+ artifactCount: markdownArtifactCount,
90
+ };
91
+ }
92
+ if (markdownArtifactCount > 0) {
93
+ return {
94
+ runId: null,
95
+ artifactCount: markdownArtifactCount,
96
+ };
97
+ }
98
+ return {
99
+ runId: preferredRunId,
100
+ artifactCount: markdownArtifactCount,
101
+ };
102
+ }
103
+ catch {
104
+ return { runId: preferredRunId };
105
+ }
106
+ }
107
+ async function markArtifactExplorerForCompletedRun(status) {
108
+ artifactRestoreGeneration += 1;
109
+ const scopeKey = activeScopeKey;
110
+ const { runId, runIds, artifactCount } = await resolveArtifactExplorerRunMetadata(scopeKey);
111
+ controller.setArtifactExplorerAvailability({
112
+ scopeKey,
113
+ runId,
114
+ ...(runIds ? { runIds } : {}),
115
+ status,
116
+ ...(artifactCount !== undefined ? { artifactCount } : {}),
117
+ open: !controller.hasActiveInput(),
118
+ });
119
+ await controller.refreshGitWorkspace();
120
+ }
121
+ async function restoreArtifactExplorerFromScope(scopeKey) {
122
+ const generation = ++artifactRestoreGeneration;
123
+ try {
124
+ const catalog = await artifactCatalogProvider({ scopeKey });
125
+ if (generation !== artifactRestoreGeneration || shuttingDown || activeScopeKey !== scopeKey) {
126
+ return;
127
+ }
128
+ if (!catalog || catalog.scopeKey !== scopeKey) {
129
+ controller.setArtifactExplorerUnavailable();
130
+ return;
131
+ }
132
+ const artifactCount = catalog.items.filter((item) => item.scopeKey === scopeKey && item.kind === "markdown").length;
133
+ if (artifactCount === 0) {
134
+ controller.setArtifactExplorerUnavailable("No markdown artifacts were found for the current scope.");
135
+ return;
136
+ }
137
+ controller.setArtifactExplorerAvailability({
138
+ scopeKey,
139
+ runId: null,
140
+ status: "completed",
141
+ artifactCount,
142
+ open: false,
143
+ label: "Artifacts available",
144
+ message: "Markdown artifacts from this scope are available for review.",
145
+ });
146
+ }
147
+ catch {
148
+ if (generation === artifactRestoreGeneration && !shuttingDown && activeScopeKey === scopeKey) {
149
+ controller.setArtifactExplorerUnavailable("Artifact Explorer could not inspect the current scope.");
150
+ }
151
+ }
152
+ }
27
153
  async function dispatch(action, client) {
154
+ let acceptedRunConfirmation = false;
28
155
  try {
29
156
  if (action.type === "flow.select") {
30
157
  if (action.key) {
@@ -48,10 +175,16 @@ export function createWebInteractiveSession(options, webOptions = {}) {
48
175
  return;
49
176
  }
50
177
  if (action.type === "confirm.accept") {
178
+ const confirmation = controller.getViewModel().confirmation;
179
+ const acceptedAction = action.action ?? confirmation?.selectedAction;
180
+ acceptedRunConfirmation = confirmation?.kind === "run" && acceptedAction !== "cancel";
51
181
  if (action.action) {
52
182
  controller.selectConfirmAction(action.action);
53
183
  }
54
184
  await controller.acceptConfirmation();
185
+ if (acceptedRunConfirmation) {
186
+ await markArtifactExplorerForCompletedRun("completed");
187
+ }
55
188
  return;
56
189
  }
57
190
  if (action.type === "confirm.cancel") {
@@ -86,6 +219,91 @@ export function createWebInteractiveSession(options, webOptions = {}) {
86
219
  controller.clearLog();
87
220
  return;
88
221
  }
222
+ if (action.type === "artifactExplorer.open") {
223
+ controller.openArtifactExplorer();
224
+ return;
225
+ }
226
+ if (action.type === "artifactExplorer.close") {
227
+ controller.closeArtifactExplorer();
228
+ return;
229
+ }
230
+ if (action.type === "autoFlow.selectPreset") {
231
+ controller.selectAutoFlowPreset(action.preset);
232
+ return;
233
+ }
234
+ if (action.type === "autoFlow.loadConfig") {
235
+ controller.loadAutoFlowConfig(action.name, action.flowId);
236
+ return;
237
+ }
238
+ if (action.type === "autoFlow.save") {
239
+ controller.saveAutoFlowConfig(action.flowId, action.name, action.location);
240
+ return;
241
+ }
242
+ if (action.type === "autoFlow.reset") {
243
+ controller.resetAutoFlowConfig(action.flowId);
244
+ return;
245
+ }
246
+ if (action.type === "autoFlow.toggleBlock") {
247
+ controller.toggleAutoFlowBlock(action.flowId, action.blockId, action.enabled, action.slotId);
248
+ return;
249
+ }
250
+ if (action.type === "autoFlow.updateParam") {
251
+ controller.updateAutoFlowParameter(action.flowId, action.blockId, action.paramName, action.value, action.slotId);
252
+ return;
253
+ }
254
+ if (action.type === "autoFlow.insertBlock") {
255
+ controller.insertAutoFlowBlock(action.flowId, action.slotId, action.blockId);
256
+ return;
257
+ }
258
+ if (action.type === "autoFlow.removeBlock") {
259
+ controller.removeAutoFlowBlock(action.flowId, action.slotId, action.blockId);
260
+ return;
261
+ }
262
+ if (action.type === "git.refresh") {
263
+ await controller.refreshGitWorkspace();
264
+ return;
265
+ }
266
+ if (action.type === "git.createBranch") {
267
+ await controller.createGitBranch(action.branchName);
268
+ return;
269
+ }
270
+ if (action.type === "git.checkout") {
271
+ await controller.checkoutGitBranch(action.branchName);
272
+ return;
273
+ }
274
+ if (action.type === "git.fetch") {
275
+ await controller.fetchGitWorkspace();
276
+ return;
277
+ }
278
+ if (action.type === "git.pullFfOnly") {
279
+ await controller.pullGitWorkspaceFfOnly();
280
+ return;
281
+ }
282
+ if (action.type === "git.stage") {
283
+ await controller.stageGitPaths(action.paths);
284
+ return;
285
+ }
286
+ if (action.type === "git.unstage") {
287
+ await controller.unstageGitPaths(action.paths);
288
+ return;
289
+ }
290
+ if (action.type === "git.updateCommitMessage") {
291
+ controller.updateGitCommitMessage(action.message);
292
+ return;
293
+ }
294
+ if (action.type === "git.commit") {
295
+ await controller.commitGitChanges(action.paths, action.message);
296
+ return;
297
+ }
298
+ if (action.type === "git.push") {
299
+ await controller.pushGitWorkspace();
300
+ return;
301
+ }
302
+ if (action.type === "settings.update") {
303
+ webUiSettings = updateWebUiSettings(action.settings);
304
+ server?.broadcast(snapshot());
305
+ return;
306
+ }
89
307
  if (action.type === "help.toggle") {
90
308
  controller.showHelp(action.visible ?? !controller.getViewModel().helpVisible);
91
309
  return;
@@ -93,6 +311,9 @@ export function createWebInteractiveSession(options, webOptions = {}) {
93
311
  controller.scrollPane(action.pane, { ...(action.delta !== undefined ? { delta: action.delta } : {}), ...(action.offset !== undefined ? { offset: action.offset } : {}) });
94
312
  }
95
313
  catch (error) {
314
+ if (acceptedRunConfirmation) {
315
+ await markArtifactExplorerForCompletedRun("failed");
316
+ }
96
317
  const message = error.message;
97
318
  controller.appendLog(`Web action failed: ${message}`);
98
319
  sendError(client, message, actionId(action));
@@ -105,6 +326,7 @@ export function createWebInteractiveSession(options, webOptions = {}) {
105
326
  }
106
327
  mounted = true;
107
328
  controller.mount();
329
+ void restoreArtifactExplorerFromScope(activeScopeKey);
108
330
  unsubscribe = controller.subscribe((event) => {
109
331
  if (event.type === "log") {
110
332
  server?.broadcast({ type: "log.append", appendedLines: event.appendedLines });
@@ -112,7 +334,9 @@ export function createWebInteractiveSession(options, webOptions = {}) {
112
334
  }
113
335
  server?.broadcast(snapshot());
114
336
  });
115
- void startWebServer({
337
+ void controller.refreshGitWorkspace().catch((error) => {
338
+ controller.appendLog(`Git workspace refresh failed: ${error.message}`);
339
+ }).then(() => startWebServer({
116
340
  ...(webOptions.noOpen !== undefined ? { noOpen: webOptions.noOpen } : {}),
117
341
  ...(webOptions.host !== undefined ? { host: webOptions.host } : {}),
118
342
  ...(webOptions.auth !== undefined ? { auth: webOptions.auth } : {}),
@@ -121,6 +345,9 @@ export function createWebInteractiveSession(options, webOptions = {}) {
121
345
  controller.appendLog(message);
122
346
  },
123
347
  ...(webOptions.openBrowser ? { openBrowser: webOptions.openBrowser } : {}),
348
+ getArtifactCatalog: (input) => artifactCatalogProvider(input),
349
+ gitService: controller.getGitService(),
350
+ getGitWorkspaceSnapshot: () => controller.getGitWorkspaceSnapshot(),
124
351
  onClientAction: (action, client) => {
125
352
  void dispatch(action, client);
126
353
  },
@@ -130,7 +357,7 @@ export function createWebInteractiveSession(options, webOptions = {}) {
130
357
  onExitRequested: () => {
131
358
  options.onExit();
132
359
  },
133
- }).then((started) => {
360
+ })).then((started) => {
134
361
  if (shuttingDown) {
135
362
  void started.close().catch((error) => {
136
363
  process.stderr.write(`Failed to close Web UI server: ${error.message}\n`);
@@ -166,7 +393,12 @@ export function createWebInteractiveSession(options, webOptions = {}) {
166
393
  requestUserInput: (form) => controller.requestUserInput(form),
167
394
  setSummary: (markdown) => controller.setSummary(markdown),
168
395
  clearSummary: () => controller.clearSummary(),
169
- setScope: (scopeKey, jiraIssueKey, gitBranchName) => controller.setScope(scopeKey, jiraIssueKey, gitBranchName),
396
+ setScope: (scopeKey, jiraIssueKey, gitBranchName) => {
397
+ activeScopeKey = scopeKey;
398
+ controller.setScope(scopeKey, jiraIssueKey, gitBranchName);
399
+ controller.setArtifactExplorerUnavailable();
400
+ void restoreArtifactExplorerFromScope(scopeKey);
401
+ },
170
402
  appendLog: (text) => controller.appendLog(text),
171
403
  setFlowFailed: (flowId) => controller.setFlowFailed(flowId),
172
404
  interruptActiveForm: (message = "Flow interrupted by user.") => {