agentweaver 0.1.17 → 0.1.18
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/README.md +104 -23
- package/dist/artifacts.js +41 -0
- package/dist/index.js +252 -27
- package/dist/interactive/controller.js +249 -13
- package/dist/interactive/ink/index.js +2 -2
- package/dist/interactive/state.js +1 -0
- package/dist/interactive/web/index.js +179 -0
- package/dist/interactive/web/protocol.js +154 -0
- package/dist/interactive/web/server.js +575 -0
- package/dist/interactive/web/static/app.js +709 -0
- package/dist/interactive/web/static/index.html +77 -0
- package/dist/interactive/web/static/styles.css +2 -0
- package/dist/interactive/web/static/styles.input.css +469 -0
- package/dist/pipeline/flow-catalog.js +4 -0
- package/dist/pipeline/flow-specs/auto-common-guided.json +313 -0
- package/dist/pipeline/flow-specs/auto-common.json +3 -1
- package/dist/pipeline/flow-specs/design-review/design-review-loop.json +2 -0
- package/dist/pipeline/flow-specs/design-review.json +2 -0
- package/dist/pipeline/flow-specs/implement.json +3 -1
- package/dist/pipeline/flow-specs/plan.json +4 -0
- package/dist/pipeline/flow-specs/playbook-init.json +199 -0
- package/dist/pipeline/flow-specs/review/review-fix.json +3 -1
- package/dist/pipeline/flow-specs/review/review-loop.json +4 -0
- package/dist/pipeline/flow-specs/review/review.json +2 -0
- package/dist/pipeline/node-registry.js +45 -0
- package/dist/pipeline/nodes/flow-run-node.js +13 -1
- package/dist/pipeline/nodes/playbook-ensure-node.js +115 -0
- package/dist/pipeline/nodes/playbook-inventory-node.js +51 -0
- package/dist/pipeline/nodes/playbook-questions-form-node.js +166 -0
- package/dist/pipeline/nodes/playbook-write-node.js +243 -0
- package/dist/pipeline/nodes/project-guidance-node.js +69 -0
- package/dist/pipeline/prompt-registry.js +4 -1
- package/dist/pipeline/prompt-runtime.js +6 -2
- package/dist/pipeline/spec-types.js +19 -0
- package/dist/pipeline/value-resolver.js +39 -1
- package/dist/playbook/practice-candidates.js +12 -0
- package/dist/playbook/repo-inventory.js +208 -0
- package/dist/prompts.js +31 -0
- package/dist/runtime/playbook.js +485 -0
- package/dist/runtime/project-guidance.js +339 -0
- package/dist/structured-artifact-schema-registry.js +8 -0
- package/dist/structured-artifact-schemas.json +235 -0
- package/dist/structured-artifacts.js +7 -1
- package/docs/declarative-workflows.md +565 -0
- package/docs/features.md +77 -0
- package/docs/playbook.md +327 -0
- package/package.json +8 -3
|
@@ -182,6 +182,7 @@ export class InteractiveSessionController {
|
|
|
182
182
|
currentOptionIndex: initialOptionIndex,
|
|
183
183
|
currentTextCursorIndex: initialCursorIndex,
|
|
184
184
|
previewScrollOffset: 0,
|
|
185
|
+
validationError: null,
|
|
185
186
|
resolve,
|
|
186
187
|
reject,
|
|
187
188
|
};
|
|
@@ -206,9 +207,12 @@ export class InteractiveSessionController {
|
|
|
206
207
|
}
|
|
207
208
|
this.emitChange();
|
|
208
209
|
}
|
|
209
|
-
setScope(scopeKey, jiraIssueKey) {
|
|
210
|
+
setScope(scopeKey, jiraIssueKey, gitBranchName) {
|
|
210
211
|
this.state.scopeKey = scopeKey;
|
|
211
212
|
this.state.jiraIssueKey = jiraIssueKey ?? null;
|
|
213
|
+
if (gitBranchName !== undefined) {
|
|
214
|
+
this.state.gitBranchName = gitBranchName;
|
|
215
|
+
}
|
|
212
216
|
this.emitChange();
|
|
213
217
|
}
|
|
214
218
|
appendLog(text) {
|
|
@@ -306,7 +310,7 @@ export class InteractiveSessionController {
|
|
|
306
310
|
selectFlowIndex(index) {
|
|
307
311
|
const selectedItem = this.visibleFlowItems[index];
|
|
308
312
|
if (!selectedItem) {
|
|
309
|
-
|
|
313
|
+
throw new Error(`Invalid flow index: ${index}`);
|
|
310
314
|
}
|
|
311
315
|
this.state.selectedFlowItemKey = selectedItem.key;
|
|
312
316
|
if (selectedItem.kind === "flow") {
|
|
@@ -314,6 +318,210 @@ export class InteractiveSessionController {
|
|
|
314
318
|
}
|
|
315
319
|
this.emitChange();
|
|
316
320
|
}
|
|
321
|
+
selectFlowKey(key) {
|
|
322
|
+
const selectedItem = this.visibleFlowItems.find((item) => item.key === key);
|
|
323
|
+
if (!selectedItem) {
|
|
324
|
+
throw new Error(`Unknown visible flow item key: ${key}`);
|
|
325
|
+
}
|
|
326
|
+
this.state.selectedFlowItemKey = selectedItem.key;
|
|
327
|
+
if (selectedItem.kind === "flow") {
|
|
328
|
+
this.state.selectedFlowId = selectedItem.flow.id;
|
|
329
|
+
}
|
|
330
|
+
this.emitChange();
|
|
331
|
+
}
|
|
332
|
+
selectFlowId(flowId) {
|
|
333
|
+
const selectedItem = this.visibleFlowItems.find((item) => item.kind === "flow" && item.flow.id === flowId);
|
|
334
|
+
if (!selectedItem) {
|
|
335
|
+
throw new Error(`Unknown visible flow: ${flowId}`);
|
|
336
|
+
}
|
|
337
|
+
this.state.selectedFlowItemKey = selectedItem.key;
|
|
338
|
+
this.state.selectedFlowId = selectedItem.flow.id;
|
|
339
|
+
this.emitChange();
|
|
340
|
+
}
|
|
341
|
+
toggleFolderKey(key) {
|
|
342
|
+
const item = this.visibleFlowItems.find((candidate) => candidate.key === key);
|
|
343
|
+
if (!item || item.kind !== "folder") {
|
|
344
|
+
throw new Error(`Unknown visible folder key: ${key}`);
|
|
345
|
+
}
|
|
346
|
+
this.toggleFlowFolder(key);
|
|
347
|
+
}
|
|
348
|
+
toggleFolder(key) {
|
|
349
|
+
this.toggleFolderKey(key);
|
|
350
|
+
}
|
|
351
|
+
async openRunConfirm(flowId, key) {
|
|
352
|
+
if (flowId) {
|
|
353
|
+
if (!this.visibleFlowItems.some((item) => item.kind === "flow" && item.flow.id === flowId)) {
|
|
354
|
+
throw new Error(`Unknown visible flow: ${flowId}`);
|
|
355
|
+
}
|
|
356
|
+
this.selectFlowId(flowId);
|
|
357
|
+
}
|
|
358
|
+
else if (key) {
|
|
359
|
+
const keyedItem = this.visibleFlowItems.find((item) => item.key === key);
|
|
360
|
+
if (!keyedItem || keyedItem.kind !== "flow") {
|
|
361
|
+
throw new Error(`Unknown visible flow item key: ${key}`);
|
|
362
|
+
}
|
|
363
|
+
this.selectFlowKey(key);
|
|
364
|
+
}
|
|
365
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
366
|
+
if (!selectedItem || selectedItem.kind !== "flow") {
|
|
367
|
+
throw new Error("A flow must be selected before opening run confirmation.");
|
|
368
|
+
}
|
|
369
|
+
await this.openConfirm();
|
|
370
|
+
}
|
|
371
|
+
selectConfirmAction(action) {
|
|
372
|
+
if (!this.confirmSession) {
|
|
373
|
+
throw new Error("No confirmation is active.");
|
|
374
|
+
}
|
|
375
|
+
const actions = this.confirmActions();
|
|
376
|
+
if (!actions.includes(action)) {
|
|
377
|
+
throw new Error(`Invalid confirmation action: ${action}`);
|
|
378
|
+
}
|
|
379
|
+
this.confirmSession.selectedAction = action;
|
|
380
|
+
this.emitChange();
|
|
381
|
+
}
|
|
382
|
+
async acceptConfirmation() {
|
|
383
|
+
await this.acceptConfirm();
|
|
384
|
+
}
|
|
385
|
+
async acceptConfirm() {
|
|
386
|
+
if (!this.confirmSession) {
|
|
387
|
+
throw new Error("No confirmation is active.");
|
|
388
|
+
}
|
|
389
|
+
await this.acceptActiveConfirm();
|
|
390
|
+
}
|
|
391
|
+
cancelConfirmation() {
|
|
392
|
+
this.cancelConfirm();
|
|
393
|
+
}
|
|
394
|
+
cancelConfirm() {
|
|
395
|
+
if (!this.confirmSession) {
|
|
396
|
+
throw new Error("No confirmation is active.");
|
|
397
|
+
}
|
|
398
|
+
this.confirmSession = null;
|
|
399
|
+
this.emitChange();
|
|
400
|
+
}
|
|
401
|
+
updateActiveFormValues(values) {
|
|
402
|
+
const session = this.activeFormSession;
|
|
403
|
+
if (!session) {
|
|
404
|
+
throw new Error("No form is active.");
|
|
405
|
+
}
|
|
406
|
+
const fieldIds = new Set(session.form.fields.map((field) => field.id));
|
|
407
|
+
const nextValues = { ...session.values };
|
|
408
|
+
let changed = false;
|
|
409
|
+
for (const [fieldId, value] of Object.entries(values)) {
|
|
410
|
+
if (!fieldIds.has(fieldId)) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
nextValues[fieldId] = value;
|
|
414
|
+
changed = true;
|
|
415
|
+
}
|
|
416
|
+
if (!changed) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
session.values = nextValues;
|
|
420
|
+
for (const field of session.form.fields) {
|
|
421
|
+
normalizeUserInputFieldValue(field, session.values);
|
|
422
|
+
}
|
|
423
|
+
session.validationError = null;
|
|
424
|
+
const field = this.currentFormField();
|
|
425
|
+
if (field?.type === "text") {
|
|
426
|
+
session.currentTextCursorIndex = String(session.values[field.id] ?? "").length;
|
|
427
|
+
}
|
|
428
|
+
else if (field?.type === "single-select" || field?.type === "multi-select") {
|
|
429
|
+
session.currentOptionIndex = this.selectedOptionIndexForField(field);
|
|
430
|
+
}
|
|
431
|
+
this.emitChange();
|
|
432
|
+
}
|
|
433
|
+
updateFormField(fieldId, value) {
|
|
434
|
+
const session = this.activeFormSession;
|
|
435
|
+
if (!session) {
|
|
436
|
+
throw new Error("No form is active.");
|
|
437
|
+
}
|
|
438
|
+
const fieldIndex = session.form.fields.findIndex((candidate) => candidate.id === fieldId);
|
|
439
|
+
const baseField = session.form.fields[fieldIndex];
|
|
440
|
+
if (!baseField) {
|
|
441
|
+
throw new Error(`Unknown form field: ${fieldId}`);
|
|
442
|
+
}
|
|
443
|
+
const field = resolveFieldDefinition(baseField, session.values);
|
|
444
|
+
if (field.type === "boolean" && typeof value !== "boolean") {
|
|
445
|
+
throw new Error(`Field '${field.label}' must be a boolean.`);
|
|
446
|
+
}
|
|
447
|
+
if (field.type === "text" && typeof value !== "string") {
|
|
448
|
+
throw new Error(`Field '${field.label}' must be a string.`);
|
|
449
|
+
}
|
|
450
|
+
if (field.type === "single-select" && typeof value !== "string") {
|
|
451
|
+
throw new Error(`Field '${field.label}' must be a string.`);
|
|
452
|
+
}
|
|
453
|
+
if (field.type === "multi-select"
|
|
454
|
+
&& (!Array.isArray(value) || value.some((item) => typeof item !== "string"))) {
|
|
455
|
+
throw new Error(`Field '${field.label}' must be a string array.`);
|
|
456
|
+
}
|
|
457
|
+
session.currentFieldIndex = fieldIndex;
|
|
458
|
+
this.updateActiveFormValues({ [fieldId]: value });
|
|
459
|
+
}
|
|
460
|
+
submitActiveFormValues(values) {
|
|
461
|
+
if (!this.activeFormSession) {
|
|
462
|
+
throw new Error("No form is active.");
|
|
463
|
+
}
|
|
464
|
+
if (values) {
|
|
465
|
+
this.updateActiveFormValues(values);
|
|
466
|
+
}
|
|
467
|
+
this.submitActiveForm();
|
|
468
|
+
if (this.activeFormSession) {
|
|
469
|
+
throw new Error("Form validation failed. See session log for details.");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
submitForm(values) {
|
|
473
|
+
this.submitActiveFormValues(values);
|
|
474
|
+
}
|
|
475
|
+
cancelForm() {
|
|
476
|
+
if (!this.activeFormSession) {
|
|
477
|
+
throw new Error("No form is active.");
|
|
478
|
+
}
|
|
479
|
+
this.cancelActiveForm();
|
|
480
|
+
}
|
|
481
|
+
async interruptFlow(flowId) {
|
|
482
|
+
const hadActiveForm = this.activeFormSession !== null;
|
|
483
|
+
if (this.activeFormSession) {
|
|
484
|
+
this.interruptActiveForm();
|
|
485
|
+
}
|
|
486
|
+
const targetFlowId = flowId ?? this.state.currentFlowId;
|
|
487
|
+
if (!targetFlowId && hadActiveForm) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (!targetFlowId) {
|
|
491
|
+
throw new Error("No running flow is available to interrupt.");
|
|
492
|
+
}
|
|
493
|
+
await this.options.onInterrupt(targetFlowId);
|
|
494
|
+
}
|
|
495
|
+
async interruptCurrentFlow(flowId) {
|
|
496
|
+
await this.interruptFlow(flowId);
|
|
497
|
+
}
|
|
498
|
+
toggleHelp(visible) {
|
|
499
|
+
this.helpVisible = visible ?? !this.helpVisible;
|
|
500
|
+
this.emitChange();
|
|
501
|
+
}
|
|
502
|
+
showHelp(visible) {
|
|
503
|
+
this.toggleHelp(visible);
|
|
504
|
+
}
|
|
505
|
+
scrollPane(panel, options) {
|
|
506
|
+
if (panel === "flows") {
|
|
507
|
+
if (options.delta !== undefined) {
|
|
508
|
+
this.moveSelectedFlow(options.delta);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (options.offset !== undefined) {
|
|
512
|
+
this.selectFlowIndex(options.offset);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
throw new Error("Flow scroll requires delta or offset.");
|
|
516
|
+
}
|
|
517
|
+
const scrollPanel = panel;
|
|
518
|
+
const maxOffset = this.panelMaxScroll(scrollPanel);
|
|
519
|
+
const current = this.scrollOffsetFor(scrollPanel);
|
|
520
|
+
this.applyScrollOffset(scrollPanel, options.offset ?? current + (options.delta ?? 0), maxOffset);
|
|
521
|
+
}
|
|
522
|
+
setScrollOffset(panel, offset) {
|
|
523
|
+
this.applyScrollOffset(panel, offset, this.panelMaxScroll(panel));
|
|
524
|
+
}
|
|
317
525
|
getViewModel(layout) {
|
|
318
526
|
const selectedItem = this.selectedFlowTreeItem();
|
|
319
527
|
const activeFlowId = this.activeFlowId();
|
|
@@ -337,6 +545,10 @@ export class InteractiveSessionController {
|
|
|
337
545
|
flowItems: this.visibleFlowItems.map((item) => ({
|
|
338
546
|
key: item.key,
|
|
339
547
|
label: this.renderFlowTreeLabel(item),
|
|
548
|
+
kind: item.kind,
|
|
549
|
+
name: item.name,
|
|
550
|
+
depth: item.depth,
|
|
551
|
+
...(item.kind === "folder" ? { expanded: this.expandedFlowFolders.has(item.key) } : {}),
|
|
340
552
|
})),
|
|
341
553
|
selectedFlowIndex: Math.max(0, this.visibleFlowItems.findIndex((item) => item.key === this.state.selectedFlowItemKey)),
|
|
342
554
|
progressTitle: this.panelTitle("Current Flow", "progress"),
|
|
@@ -352,6 +564,7 @@ export class InteractiveSessionController {
|
|
|
352
564
|
logText: this.logText,
|
|
353
565
|
logScrollOffset: this.state.logScrollOffset,
|
|
354
566
|
confirmText: this.renderConfirmText(),
|
|
567
|
+
confirmation: this.renderConfirmationView(),
|
|
355
568
|
form: this.renderFormView(layout),
|
|
356
569
|
};
|
|
357
570
|
}
|
|
@@ -395,7 +608,7 @@ export class InteractiveSessionController {
|
|
|
395
608
|
const current = this.state.currentFlowId ?? selectHeaderLabel(this.selectedFlowTreeItem(), this.state.selectedFlowId);
|
|
396
609
|
const pathParts = this.options.cwd.split(path.sep).filter(Boolean);
|
|
397
610
|
const folderName = pathParts.slice(-3).join("/") || this.options.cwd;
|
|
398
|
-
const branchLabel = this.
|
|
611
|
+
const branchLabel = this.state.gitBranchName ? this.state.gitBranchName : "detached-head";
|
|
399
612
|
const runningSuffix = this.state.busy ? " [running]" : "";
|
|
400
613
|
const versionLabel = this.state.version ? ` | Version ${this.state.version}` : "";
|
|
401
614
|
const jiraLabel = this.state.jiraIssueKey ? ` | Jira ${this.state.jiraIssueKey}` : "";
|
|
@@ -512,6 +725,20 @@ export class InteractiveSessionController {
|
|
|
512
725
|
lines.push("", actionLabels, "", "Left/Right or Tab: choose Enter: confirm Esc: cancel");
|
|
513
726
|
return lines.join("\n");
|
|
514
727
|
}
|
|
728
|
+
renderConfirmationView() {
|
|
729
|
+
const session = this.confirmSession;
|
|
730
|
+
const text = this.renderConfirmText();
|
|
731
|
+
if (!session || !text) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
kind: session.kind,
|
|
736
|
+
flowId: session.flowId,
|
|
737
|
+
text,
|
|
738
|
+
actions: this.confirmActions(),
|
|
739
|
+
selectedAction: session.selectedAction,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
515
742
|
renderFormView(layout) {
|
|
516
743
|
const session = this.activeFormSession;
|
|
517
744
|
const field = this.currentFormField();
|
|
@@ -573,6 +800,12 @@ export class InteractiveSessionController {
|
|
|
573
800
|
title: "User Input",
|
|
574
801
|
content: lines.join("\n"),
|
|
575
802
|
footer,
|
|
803
|
+
formId: session.form.formId,
|
|
804
|
+
definition: session.form,
|
|
805
|
+
values: { ...session.values },
|
|
806
|
+
fields: session.form.fields.map((candidate) => resolveFieldDefinition(candidate, session.values)),
|
|
807
|
+
currentFieldId: field.id,
|
|
808
|
+
error: session.validationError,
|
|
576
809
|
};
|
|
577
810
|
}
|
|
578
811
|
currentFormField() {
|
|
@@ -609,7 +842,7 @@ export class InteractiveSessionController {
|
|
|
609
842
|
return;
|
|
610
843
|
}
|
|
611
844
|
if (key.name === "enter") {
|
|
612
|
-
await this.
|
|
845
|
+
await this.acceptActiveConfirm();
|
|
613
846
|
}
|
|
614
847
|
}
|
|
615
848
|
async handleFlowKey(key) {
|
|
@@ -661,27 +894,27 @@ export class InteractiveSessionController {
|
|
|
661
894
|
const maxOffset = this.panelMaxScroll(panel);
|
|
662
895
|
const current = this.scrollOffsetFor(panel);
|
|
663
896
|
if (key.name === "up") {
|
|
664
|
-
this.
|
|
897
|
+
this.applyScrollOffset(panel, current - 1, maxOffset);
|
|
665
898
|
return;
|
|
666
899
|
}
|
|
667
900
|
if (key.name === "down") {
|
|
668
|
-
this.
|
|
901
|
+
this.applyScrollOffset(panel, current + 1, maxOffset);
|
|
669
902
|
return;
|
|
670
903
|
}
|
|
671
904
|
if (key.name === "pageup") {
|
|
672
|
-
this.
|
|
905
|
+
this.applyScrollOffset(panel, current - 10, maxOffset);
|
|
673
906
|
return;
|
|
674
907
|
}
|
|
675
908
|
if (key.name === "pagedown") {
|
|
676
|
-
this.
|
|
909
|
+
this.applyScrollOffset(panel, current + 10, maxOffset);
|
|
677
910
|
return;
|
|
678
911
|
}
|
|
679
912
|
if (key.name === "home") {
|
|
680
|
-
this.
|
|
913
|
+
this.applyScrollOffset(panel, 0, maxOffset);
|
|
681
914
|
return;
|
|
682
915
|
}
|
|
683
916
|
if (key.name === "end") {
|
|
684
|
-
this.
|
|
917
|
+
this.applyScrollOffset(panel, maxOffset, maxOffset);
|
|
685
918
|
}
|
|
686
919
|
}
|
|
687
920
|
moveSelectedFlow(delta) {
|
|
@@ -853,7 +1086,7 @@ export class InteractiveSessionController {
|
|
|
853
1086
|
this.confirmSession.selectedAction = (actions[nextIndex] ?? "cancel");
|
|
854
1087
|
this.emitChange();
|
|
855
1088
|
}
|
|
856
|
-
async
|
|
1089
|
+
async acceptActiveConfirm() {
|
|
857
1090
|
const session = this.confirmSession;
|
|
858
1091
|
if (!session) {
|
|
859
1092
|
return;
|
|
@@ -978,7 +1211,7 @@ export class InteractiveSessionController {
|
|
|
978
1211
|
}
|
|
979
1212
|
return this.state.logScrollOffset;
|
|
980
1213
|
}
|
|
981
|
-
|
|
1214
|
+
applyScrollOffset(panel, value, maxOffset) {
|
|
982
1215
|
const next = clamp(value, 0, maxOffset);
|
|
983
1216
|
if (panel === "progress") {
|
|
984
1217
|
this.state.progressScrollOffset = next;
|
|
@@ -1100,6 +1333,7 @@ export class InteractiveSessionController {
|
|
|
1100
1333
|
}
|
|
1101
1334
|
this.syncActiveSelectFieldValue();
|
|
1102
1335
|
try {
|
|
1336
|
+
session.validationError = null;
|
|
1103
1337
|
validateUserInputValues(session.form, session.values);
|
|
1104
1338
|
const result = {
|
|
1105
1339
|
formId: session.form.formId,
|
|
@@ -1112,7 +1346,9 @@ export class InteractiveSessionController {
|
|
|
1112
1346
|
this.emitChange();
|
|
1113
1347
|
}
|
|
1114
1348
|
catch (error) {
|
|
1115
|
-
|
|
1349
|
+
session.validationError = error.message;
|
|
1350
|
+
this.appendLog(session.validationError);
|
|
1351
|
+
this.emitChange();
|
|
1116
1352
|
}
|
|
1117
1353
|
}
|
|
1118
1354
|
cancelActiveForm() {
|
|
@@ -562,8 +562,8 @@ class InkInteractiveSession {
|
|
|
562
562
|
clearSummary() {
|
|
563
563
|
this.controller.clearSummary();
|
|
564
564
|
}
|
|
565
|
-
setScope(scopeKey, jiraIssueKey) {
|
|
566
|
-
this.controller.setScope(scopeKey, jiraIssueKey);
|
|
565
|
+
setScope(scopeKey, jiraIssueKey, gitBranchName) {
|
|
566
|
+
this.controller.setScope(scopeKey, jiraIssueKey, gitBranchName);
|
|
567
567
|
}
|
|
568
568
|
appendLog(text) {
|
|
569
569
|
this.controller.appendLog(text);
|
|
@@ -8,6 +8,7 @@ export function createInitialInteractiveState(options) {
|
|
|
8
8
|
return {
|
|
9
9
|
scopeKey: options.scopeKey,
|
|
10
10
|
jiraIssueKey: options.jiraIssueKey ?? null,
|
|
11
|
+
gitBranchName: options.gitBranchName,
|
|
11
12
|
summaryText: options.summaryText.trim(),
|
|
12
13
|
version: options.version ?? "",
|
|
13
14
|
flowTreeKeys: flowTree.map((node) => node.key),
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { writeSync } from "node:fs";
|
|
3
|
+
import { FlowInterruptedError } from "../../errors.js";
|
|
4
|
+
import { InteractiveSessionController } from "../controller.js";
|
|
5
|
+
import { startWebServer } from "./server.js";
|
|
6
|
+
function actionId(action) {
|
|
7
|
+
return "actionId" in action ? action.actionId : undefined;
|
|
8
|
+
}
|
|
9
|
+
export function createWebInteractiveSession(options, webOptions = {}) {
|
|
10
|
+
const controller = new InteractiveSessionController(options);
|
|
11
|
+
let server = null;
|
|
12
|
+
let unsubscribe = null;
|
|
13
|
+
let mounted = false;
|
|
14
|
+
let shuttingDown = false;
|
|
15
|
+
function snapshot() {
|
|
16
|
+
return { type: "snapshot", viewModel: controller.getViewModel() };
|
|
17
|
+
}
|
|
18
|
+
function sendError(client, message, id) {
|
|
19
|
+
const event = { type: "error", message, ...(id ? { actionId: id } : {}) };
|
|
20
|
+
if (client) {
|
|
21
|
+
client.send(event);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
server?.broadcast(event);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function dispatch(action, client) {
|
|
28
|
+
try {
|
|
29
|
+
if (action.type === "flow.select") {
|
|
30
|
+
if (action.key) {
|
|
31
|
+
controller.selectFlowKey(action.key);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
controller.selectFlowIndex(action.index ?? 0);
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (action.type === "folder.toggle") {
|
|
39
|
+
controller.toggleFolderKey(action.key);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (action.type === "run.openConfirm") {
|
|
43
|
+
await controller.openRunConfirm(action.flowId, action.key);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (action.type === "confirm.select") {
|
|
47
|
+
controller.selectConfirmAction(action.action);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (action.type === "confirm.accept") {
|
|
51
|
+
if (action.action) {
|
|
52
|
+
controller.selectConfirmAction(action.action);
|
|
53
|
+
}
|
|
54
|
+
await controller.acceptConfirmation();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (action.type === "confirm.cancel") {
|
|
58
|
+
controller.cancelConfirmation();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (action.type === "form.update") {
|
|
62
|
+
controller.updateActiveFormValues(action.values);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (action.type === "form.fieldUpdate") {
|
|
66
|
+
controller.updateFormField(action.fieldId, action.value);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (action.type === "form.submit") {
|
|
70
|
+
controller.submitForm(action.values);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (action.type === "form.cancel") {
|
|
74
|
+
controller.cancelForm();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (action.type === "flow.interrupt") {
|
|
78
|
+
await controller.interruptCurrentFlow(action.flowId);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (action.type === "interrupt.openConfirm") {
|
|
82
|
+
controller.openInterruptConfirm();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (action.type === "log.clear") {
|
|
86
|
+
controller.clearLog();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (action.type === "help.toggle") {
|
|
90
|
+
controller.showHelp(action.visible ?? !controller.getViewModel().helpVisible);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
controller.scrollPane(action.pane, { ...(action.delta !== undefined ? { delta: action.delta } : {}), ...(action.offset !== undefined ? { offset: action.offset } : {}) });
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const message = error.message;
|
|
97
|
+
controller.appendLog(`Web action failed: ${message}`);
|
|
98
|
+
sendError(client, message, actionId(action));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
mount() {
|
|
103
|
+
if (mounted) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
mounted = true;
|
|
107
|
+
controller.mount();
|
|
108
|
+
unsubscribe = controller.subscribe((event) => {
|
|
109
|
+
if (event.type === "log") {
|
|
110
|
+
server?.broadcast({ type: "log.append", appendedLines: event.appendedLines });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
server?.broadcast(snapshot());
|
|
114
|
+
});
|
|
115
|
+
void startWebServer({
|
|
116
|
+
...(webOptions.noOpen !== undefined ? { noOpen: webOptions.noOpen } : {}),
|
|
117
|
+
...(webOptions.host !== undefined ? { host: webOptions.host } : {}),
|
|
118
|
+
...(webOptions.auth !== undefined ? { auth: webOptions.auth } : {}),
|
|
119
|
+
printInfo: (message) => {
|
|
120
|
+
webOptions.printInfo?.(message);
|
|
121
|
+
controller.appendLog(message);
|
|
122
|
+
},
|
|
123
|
+
...(webOptions.openBrowser ? { openBrowser: webOptions.openBrowser } : {}),
|
|
124
|
+
onClientAction: (action, client) => {
|
|
125
|
+
void dispatch(action, client);
|
|
126
|
+
},
|
|
127
|
+
onClientConnected: (client) => {
|
|
128
|
+
client.send(snapshot());
|
|
129
|
+
},
|
|
130
|
+
onExitRequested: () => {
|
|
131
|
+
options.onExit();
|
|
132
|
+
},
|
|
133
|
+
}).then((started) => {
|
|
134
|
+
if (shuttingDown) {
|
|
135
|
+
void started.close().catch((error) => {
|
|
136
|
+
process.stderr.write(`Failed to close Web UI server: ${error.message}\n`);
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
server = started;
|
|
141
|
+
webOptions.onServerReady?.(started);
|
|
142
|
+
}).catch((error) => {
|
|
143
|
+
const message = `Web UI startup failed: ${error.message}`;
|
|
144
|
+
controller.appendLog(message);
|
|
145
|
+
writeSync(process.stderr.fd, `${message}\n`);
|
|
146
|
+
options.onExit();
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
destroy() {
|
|
150
|
+
if (shuttingDown) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
shuttingDown = true;
|
|
154
|
+
controller.interruptActiveForm("Web UI session closed.");
|
|
155
|
+
unsubscribe?.();
|
|
156
|
+
unsubscribe = null;
|
|
157
|
+
const closePromise = server?.close();
|
|
158
|
+
server = null;
|
|
159
|
+
if (closePromise) {
|
|
160
|
+
void closePromise.catch((error) => {
|
|
161
|
+
process.stderr.write(`Failed to close Web UI server: ${error.message}\n`);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
controller.destroy();
|
|
165
|
+
},
|
|
166
|
+
requestUserInput: (form) => controller.requestUserInput(form),
|
|
167
|
+
setSummary: (markdown) => controller.setSummary(markdown),
|
|
168
|
+
clearSummary: () => controller.clearSummary(),
|
|
169
|
+
setScope: (scopeKey, jiraIssueKey, gitBranchName) => controller.setScope(scopeKey, jiraIssueKey, gitBranchName),
|
|
170
|
+
appendLog: (text) => controller.appendLog(text),
|
|
171
|
+
setFlowFailed: (flowId) => controller.setFlowFailed(flowId),
|
|
172
|
+
interruptActiveForm: (message = "Flow interrupted by user.") => {
|
|
173
|
+
controller.interruptActiveForm(message);
|
|
174
|
+
if (message) {
|
|
175
|
+
controller.appendLog(new FlowInterruptedError(message).message);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|