agentweaver 0.1.16 → 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 +148 -27
- package/dist/artifacts.js +114 -3
- package/dist/doctor/checks/executors.js +2 -2
- package/dist/flow-state.js +138 -1
- package/dist/index.js +421 -82
- package/dist/interactive/controller.js +305 -36
- package/dist/interactive/ink/index.js +24 -3
- package/dist/interactive/state.js +1 -0
- package/dist/interactive/tree.js +2 -2
- 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/auto-flow.js +9 -6
- package/dist/pipeline/context.js +6 -5
- package/dist/pipeline/declarative-flows.js +39 -20
- package/dist/pipeline/flow-catalog.js +40 -14
- package/dist/pipeline/flow-specs/auto-common-guided.json +313 -0
- package/dist/pipeline/flow-specs/auto-common.json +4 -1
- package/dist/pipeline/flow-specs/auto-golang.json +27 -1
- package/dist/pipeline/flow-specs/design-review/design-review-loop.json +15 -1
- 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 +8 -2
- 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/launch-profile-config.js +30 -18
- package/dist/pipeline/node-contract.js +1 -0
- package/dist/pipeline/node-registry.js +119 -5
- package/dist/pipeline/nodes/flow-run-node.js +200 -173
- package/dist/pipeline/nodes/llm-prompt-node.js +15 -33
- 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/plugin-loader.js +389 -0
- package/dist/pipeline/plugin-types.js +1 -0
- package/dist/pipeline/prompt-registry.js +4 -1
- package/dist/pipeline/prompt-runtime.js +6 -2
- package/dist/pipeline/registry.js +71 -4
- package/dist/pipeline/spec-compiler.js +1 -0
- package/dist/pipeline/spec-loader.js +14 -0
- package/dist/pipeline/spec-types.js +19 -0
- package/dist/pipeline/spec-validator.js +6 -0
- package/dist/pipeline/value-resolver.js +41 -2
- package/dist/playbook/practice-candidates.js +12 -0
- package/dist/playbook/repo-inventory.js +208 -0
- package/dist/plugin-sdk.js +1 -0
- package/dist/prompts.js +31 -0
- package/dist/runtime/artifact-registry.js +3 -0
- package/dist/runtime/execution-routing.js +25 -19
- package/dist/runtime/interactive-execution-routing.js +66 -57
- 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/example/.flows/examples/claude-example.json +50 -0
- package/docs/example/.plugins/claude-example-plugin/index.js +149 -0
- package/docs/example/.plugins/claude-example-plugin/plugin.json +8 -0
- package/docs/examples/.flows/claude-example.json +50 -0
- package/docs/examples/.plugins/claude-example-plugin/index.js +149 -0
- package/docs/examples/.plugins/claude-example-plugin/plugin.json +8 -0
- package/docs/features.md +77 -0
- package/docs/playbook.md +327 -0
- package/docs/plugin-sdk.md +731 -0
- package/package.json +13 -4
|
@@ -23,6 +23,8 @@ const HELP_TEXT = renderMarkdownToTerminal([
|
|
|
23
23
|
"q / Ctrl+C exit",
|
|
24
24
|
].join("\n"));
|
|
25
25
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
26
|
+
const SPINNER_INTERVAL_MS = 200;
|
|
27
|
+
const LOG_FLUSH_INTERVAL_MS = 120;
|
|
26
28
|
function clamp(value, min, max) {
|
|
27
29
|
return Math.min(max, Math.max(min, value));
|
|
28
30
|
}
|
|
@@ -180,6 +182,7 @@ export class InteractiveSessionController {
|
|
|
180
182
|
currentOptionIndex: initialOptionIndex,
|
|
181
183
|
currentTextCursorIndex: initialCursorIndex,
|
|
182
184
|
previewScrollOffset: 0,
|
|
185
|
+
validationError: null,
|
|
183
186
|
resolve,
|
|
184
187
|
reject,
|
|
185
188
|
};
|
|
@@ -204,9 +207,12 @@ export class InteractiveSessionController {
|
|
|
204
207
|
}
|
|
205
208
|
this.emitChange();
|
|
206
209
|
}
|
|
207
|
-
setScope(scopeKey, jiraIssueKey) {
|
|
210
|
+
setScope(scopeKey, jiraIssueKey, gitBranchName) {
|
|
208
211
|
this.state.scopeKey = scopeKey;
|
|
209
212
|
this.state.jiraIssueKey = jiraIssueKey ?? null;
|
|
213
|
+
if (gitBranchName !== undefined) {
|
|
214
|
+
this.state.gitBranchName = gitBranchName;
|
|
215
|
+
}
|
|
210
216
|
this.emitChange();
|
|
211
217
|
}
|
|
212
218
|
appendLog(text) {
|
|
@@ -304,7 +310,18 @@ export class InteractiveSessionController {
|
|
|
304
310
|
selectFlowIndex(index) {
|
|
305
311
|
const selectedItem = this.visibleFlowItems[index];
|
|
306
312
|
if (!selectedItem) {
|
|
307
|
-
|
|
313
|
+
throw new Error(`Invalid flow index: ${index}`);
|
|
314
|
+
}
|
|
315
|
+
this.state.selectedFlowItemKey = selectedItem.key;
|
|
316
|
+
if (selectedItem.kind === "flow") {
|
|
317
|
+
this.state.selectedFlowId = selectedItem.flow.id;
|
|
318
|
+
}
|
|
319
|
+
this.emitChange();
|
|
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}`);
|
|
308
325
|
}
|
|
309
326
|
this.state.selectedFlowItemKey = selectedItem.key;
|
|
310
327
|
if (selectedItem.kind === "flow") {
|
|
@@ -312,6 +329,199 @@ export class InteractiveSessionController {
|
|
|
312
329
|
}
|
|
313
330
|
this.emitChange();
|
|
314
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
|
+
}
|
|
315
525
|
getViewModel(layout) {
|
|
316
526
|
const selectedItem = this.selectedFlowTreeItem();
|
|
317
527
|
const activeFlowId = this.activeFlowId();
|
|
@@ -335,6 +545,10 @@ export class InteractiveSessionController {
|
|
|
335
545
|
flowItems: this.visibleFlowItems.map((item) => ({
|
|
336
546
|
key: item.key,
|
|
337
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) } : {}),
|
|
338
552
|
})),
|
|
339
553
|
selectedFlowIndex: Math.max(0, this.visibleFlowItems.findIndex((item) => item.key === this.state.selectedFlowItemKey)),
|
|
340
554
|
progressTitle: this.panelTitle("Current Flow", "progress"),
|
|
@@ -350,6 +564,7 @@ export class InteractiveSessionController {
|
|
|
350
564
|
logText: this.logText,
|
|
351
565
|
logScrollOffset: this.state.logScrollOffset,
|
|
352
566
|
confirmText: this.renderConfirmText(),
|
|
567
|
+
confirmation: this.renderConfirmationView(),
|
|
353
568
|
form: this.renderFormView(layout),
|
|
354
569
|
};
|
|
355
570
|
}
|
|
@@ -393,7 +608,7 @@ export class InteractiveSessionController {
|
|
|
393
608
|
const current = this.state.currentFlowId ?? selectHeaderLabel(this.selectedFlowTreeItem(), this.state.selectedFlowId);
|
|
394
609
|
const pathParts = this.options.cwd.split(path.sep).filter(Boolean);
|
|
395
610
|
const folderName = pathParts.slice(-3).join("/") || this.options.cwd;
|
|
396
|
-
const branchLabel = this.
|
|
611
|
+
const branchLabel = this.state.gitBranchName ? this.state.gitBranchName : "detached-head";
|
|
397
612
|
const runningSuffix = this.state.busy ? " [running]" : "";
|
|
398
613
|
const versionLabel = this.state.version ? ` | Version ${this.state.version}` : "";
|
|
399
614
|
const jiraLabel = this.state.jiraIssueKey ? ` | Jira ${this.state.jiraIssueKey}` : "";
|
|
@@ -428,7 +643,8 @@ export class InteractiveSessionController {
|
|
|
428
643
|
return "Flow structure is not available.";
|
|
429
644
|
}
|
|
430
645
|
if (selectedItem.kind === "folder") {
|
|
431
|
-
const
|
|
646
|
+
const rootName = selectedItem.pathSegments[0];
|
|
647
|
+
const kindLabel = rootName === "custom" ? "project-local" : rootName === "global" ? "global" : "built-in";
|
|
432
648
|
return [
|
|
433
649
|
`Flow folder '${selectedItem.pathSegments.join("/")}'.`,
|
|
434
650
|
"",
|
|
@@ -440,8 +656,8 @@ export class InteractiveSessionController {
|
|
|
440
656
|
const description = flow.description?.trim() || "No description available for this flow.";
|
|
441
657
|
const details = [
|
|
442
658
|
`Path: ${flow.treePath.join("/")}`,
|
|
443
|
-
`Source: ${flow.source === "project-local" ? "project-local" : "built-in"}`,
|
|
444
|
-
flow.source
|
|
659
|
+
`Source: ${flow.source === "project-local" ? "project-local" : flow.source === "global" ? "global" : "built-in"}`,
|
|
660
|
+
flow.source !== "built-in" && flow.sourcePath ? `File: ${flow.sourcePath}` : "",
|
|
445
661
|
]
|
|
446
662
|
.filter((line) => line.length > 0)
|
|
447
663
|
.join("\n");
|
|
@@ -488,11 +704,13 @@ export class InteractiveSessionController {
|
|
|
488
704
|
? "Stop"
|
|
489
705
|
: action === "resume"
|
|
490
706
|
? "Resume"
|
|
491
|
-
: action === "
|
|
492
|
-
? "
|
|
493
|
-
: action === "
|
|
494
|
-
? "
|
|
495
|
-
: "
|
|
707
|
+
: action === "continue"
|
|
708
|
+
? "Continue"
|
|
709
|
+
: action === "restart"
|
|
710
|
+
? "Restart"
|
|
711
|
+
: action === "ok"
|
|
712
|
+
? "OK"
|
|
713
|
+
: "Cancel";
|
|
496
714
|
return session.selectedAction === action ? `[ ${label} ]` : ` ${label} `;
|
|
497
715
|
})
|
|
498
716
|
.join(" ");
|
|
@@ -507,6 +725,20 @@ export class InteractiveSessionController {
|
|
|
507
725
|
lines.push("", actionLabels, "", "Left/Right or Tab: choose Enter: confirm Esc: cancel");
|
|
508
726
|
return lines.join("\n");
|
|
509
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
|
+
}
|
|
510
742
|
renderFormView(layout) {
|
|
511
743
|
const session = this.activeFormSession;
|
|
512
744
|
const field = this.currentFormField();
|
|
@@ -568,6 +800,12 @@ export class InteractiveSessionController {
|
|
|
568
800
|
title: "User Input",
|
|
569
801
|
content: lines.join("\n"),
|
|
570
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,
|
|
571
809
|
};
|
|
572
810
|
}
|
|
573
811
|
currentFormField() {
|
|
@@ -604,7 +842,7 @@ export class InteractiveSessionController {
|
|
|
604
842
|
return;
|
|
605
843
|
}
|
|
606
844
|
if (key.name === "enter") {
|
|
607
|
-
await this.
|
|
845
|
+
await this.acceptActiveConfirm();
|
|
608
846
|
}
|
|
609
847
|
}
|
|
610
848
|
async handleFlowKey(key) {
|
|
@@ -656,27 +894,27 @@ export class InteractiveSessionController {
|
|
|
656
894
|
const maxOffset = this.panelMaxScroll(panel);
|
|
657
895
|
const current = this.scrollOffsetFor(panel);
|
|
658
896
|
if (key.name === "up") {
|
|
659
|
-
this.
|
|
897
|
+
this.applyScrollOffset(panel, current - 1, maxOffset);
|
|
660
898
|
return;
|
|
661
899
|
}
|
|
662
900
|
if (key.name === "down") {
|
|
663
|
-
this.
|
|
901
|
+
this.applyScrollOffset(panel, current + 1, maxOffset);
|
|
664
902
|
return;
|
|
665
903
|
}
|
|
666
904
|
if (key.name === "pageup") {
|
|
667
|
-
this.
|
|
905
|
+
this.applyScrollOffset(panel, current - 10, maxOffset);
|
|
668
906
|
return;
|
|
669
907
|
}
|
|
670
908
|
if (key.name === "pagedown") {
|
|
671
|
-
this.
|
|
909
|
+
this.applyScrollOffset(panel, current + 10, maxOffset);
|
|
672
910
|
return;
|
|
673
911
|
}
|
|
674
912
|
if (key.name === "home") {
|
|
675
|
-
this.
|
|
913
|
+
this.applyScrollOffset(panel, 0, maxOffset);
|
|
676
914
|
return;
|
|
677
915
|
}
|
|
678
916
|
if (key.name === "end") {
|
|
679
|
-
this.
|
|
917
|
+
this.applyScrollOffset(panel, maxOffset, maxOffset);
|
|
680
918
|
}
|
|
681
919
|
}
|
|
682
920
|
moveSelectedFlow(delta) {
|
|
@@ -759,10 +997,20 @@ export class InteractiveSessionController {
|
|
|
759
997
|
this.confirmSession = {
|
|
760
998
|
kind: "run",
|
|
761
999
|
flowId: selectedItem.flow.id,
|
|
762
|
-
|
|
763
|
-
|
|
1000
|
+
availability: {
|
|
1001
|
+
hasExistingState: confirmation.hasExistingState,
|
|
1002
|
+
resume: confirmation.resume.available,
|
|
1003
|
+
continue: confirmation.continue.available,
|
|
1004
|
+
restart: confirmation.restart.available,
|
|
1005
|
+
},
|
|
764
1006
|
details: confirmation.details ?? null,
|
|
765
|
-
selectedAction: confirmation.
|
|
1007
|
+
selectedAction: confirmation.resume.available
|
|
1008
|
+
? "resume"
|
|
1009
|
+
: confirmation.continue.available
|
|
1010
|
+
? "continue"
|
|
1011
|
+
: confirmation.restart.available
|
|
1012
|
+
? "restart"
|
|
1013
|
+
: "ok",
|
|
766
1014
|
};
|
|
767
1015
|
this.emitChange();
|
|
768
1016
|
}
|
|
@@ -774,8 +1022,12 @@ export class InteractiveSessionController {
|
|
|
774
1022
|
this.confirmSession = {
|
|
775
1023
|
kind: "interrupt",
|
|
776
1024
|
flowId,
|
|
777
|
-
|
|
778
|
-
|
|
1025
|
+
availability: {
|
|
1026
|
+
hasExistingState: true,
|
|
1027
|
+
resume: true,
|
|
1028
|
+
continue: false,
|
|
1029
|
+
restart: false,
|
|
1030
|
+
},
|
|
779
1031
|
details: "The current flow will be stopped. State will be saved and can be continued via Resume.",
|
|
780
1032
|
selectedAction: "stop",
|
|
781
1033
|
};
|
|
@@ -791,8 +1043,12 @@ export class InteractiveSessionController {
|
|
|
791
1043
|
this.confirmSession = {
|
|
792
1044
|
kind: "exit",
|
|
793
1045
|
flowId: null,
|
|
794
|
-
|
|
795
|
-
|
|
1046
|
+
availability: {
|
|
1047
|
+
hasExistingState: false,
|
|
1048
|
+
resume: false,
|
|
1049
|
+
continue: false,
|
|
1050
|
+
restart: false,
|
|
1051
|
+
},
|
|
796
1052
|
details,
|
|
797
1053
|
selectedAction: "ok",
|
|
798
1054
|
};
|
|
@@ -808,11 +1064,17 @@ export class InteractiveSessionController {
|
|
|
808
1064
|
if (this.confirmSession.kind === "exit") {
|
|
809
1065
|
return ["ok", "cancel"];
|
|
810
1066
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1067
|
+
const actions = [];
|
|
1068
|
+
if (this.confirmSession.availability.resume) {
|
|
1069
|
+
actions.push("resume");
|
|
1070
|
+
}
|
|
1071
|
+
if (this.confirmSession.availability.continue) {
|
|
1072
|
+
actions.push("continue");
|
|
1073
|
+
}
|
|
1074
|
+
if (this.confirmSession.availability.restart) {
|
|
1075
|
+
actions.push("restart");
|
|
1076
|
+
}
|
|
1077
|
+
return actions.length > 0 ? [...actions, "cancel"] : ["ok", "cancel"];
|
|
816
1078
|
}
|
|
817
1079
|
moveConfirmSelection(delta) {
|
|
818
1080
|
if (!this.confirmSession) {
|
|
@@ -824,7 +1086,7 @@ export class InteractiveSessionController {
|
|
|
824
1086
|
this.confirmSession.selectedAction = (actions[nextIndex] ?? "cancel");
|
|
825
1087
|
this.emitChange();
|
|
826
1088
|
}
|
|
827
|
-
async
|
|
1089
|
+
async acceptActiveConfirm() {
|
|
828
1090
|
const session = this.confirmSession;
|
|
829
1091
|
if (!session) {
|
|
830
1092
|
return;
|
|
@@ -850,7 +1112,11 @@ export class InteractiveSessionController {
|
|
|
850
1112
|
return;
|
|
851
1113
|
}
|
|
852
1114
|
const flowId = session.flowId ?? this.state.selectedFlowId;
|
|
853
|
-
const launchMode = session.selectedAction === "resume"
|
|
1115
|
+
const launchMode = session.selectedAction === "resume"
|
|
1116
|
+
? "resume"
|
|
1117
|
+
: session.selectedAction === "continue"
|
|
1118
|
+
? "continue"
|
|
1119
|
+
: "restart";
|
|
854
1120
|
this.confirmSession = null;
|
|
855
1121
|
this.setBusy(true, flowId);
|
|
856
1122
|
this.clearFlowFailure(flowId);
|
|
@@ -895,7 +1161,7 @@ export class InteractiveSessionController {
|
|
|
895
1161
|
this.spinnerTimer = setInterval(() => {
|
|
896
1162
|
this.state.spinnerFrame = (this.state.spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
897
1163
|
this.emitChange();
|
|
898
|
-
},
|
|
1164
|
+
}, SPINNER_INTERVAL_MS);
|
|
899
1165
|
return;
|
|
900
1166
|
}
|
|
901
1167
|
if (!running && this.spinnerTimer) {
|
|
@@ -945,7 +1211,7 @@ export class InteractiveSessionController {
|
|
|
945
1211
|
}
|
|
946
1212
|
return this.state.logScrollOffset;
|
|
947
1213
|
}
|
|
948
|
-
|
|
1214
|
+
applyScrollOffset(panel, value, maxOffset) {
|
|
949
1215
|
const next = clamp(value, 0, maxOffset);
|
|
950
1216
|
if (panel === "progress") {
|
|
951
1217
|
this.state.progressScrollOffset = next;
|
|
@@ -1067,6 +1333,7 @@ export class InteractiveSessionController {
|
|
|
1067
1333
|
}
|
|
1068
1334
|
this.syncActiveSelectFieldValue();
|
|
1069
1335
|
try {
|
|
1336
|
+
session.validationError = null;
|
|
1070
1337
|
validateUserInputValues(session.form, session.values);
|
|
1071
1338
|
const result = {
|
|
1072
1339
|
formId: session.form.formId,
|
|
@@ -1079,7 +1346,9 @@ export class InteractiveSessionController {
|
|
|
1079
1346
|
this.emitChange();
|
|
1080
1347
|
}
|
|
1081
1348
|
catch (error) {
|
|
1082
|
-
|
|
1349
|
+
session.validationError = error.message;
|
|
1350
|
+
this.appendLog(session.validationError);
|
|
1351
|
+
this.emitChange();
|
|
1083
1352
|
}
|
|
1084
1353
|
}
|
|
1085
1354
|
cancelActiveForm() {
|
|
@@ -1278,7 +1547,7 @@ export class InteractiveSessionController {
|
|
|
1278
1547
|
this.logFlushTimer = setTimeout(() => {
|
|
1279
1548
|
this.logFlushTimer = null;
|
|
1280
1549
|
this.flushPendingLogLines();
|
|
1281
|
-
},
|
|
1550
|
+
}, LOG_FLUSH_INTERVAL_MS);
|
|
1282
1551
|
}
|
|
1283
1552
|
flushPendingLogLines() {
|
|
1284
1553
|
if (this.pendingLogLines.length === 0) {
|
|
@@ -370,14 +370,35 @@ function createInkApp(react, ink, controller) {
|
|
|
370
370
|
const { Fragment, createElement, useEffect, useState } = react;
|
|
371
371
|
const { Box, Text, useInput, useStdout } = ink;
|
|
372
372
|
const Panel = createPanelComponent(react, ink);
|
|
373
|
+
const LOG_REPAINT_DEBOUNCE_MS = 100;
|
|
373
374
|
const App = () => {
|
|
374
375
|
const [, setVersion] = useState(0);
|
|
375
376
|
useEffect(() => {
|
|
376
|
-
|
|
377
|
+
let logRepaintTimer = null;
|
|
378
|
+
const flushRepaint = () => {
|
|
379
|
+
if (logRepaintTimer) {
|
|
380
|
+
clearTimeout(logRepaintTimer);
|
|
381
|
+
logRepaintTimer = null;
|
|
382
|
+
}
|
|
377
383
|
setVersion((previous) => previous + 1);
|
|
384
|
+
};
|
|
385
|
+
const unsubscribe = controller.subscribe((event) => {
|
|
386
|
+
if (event.type === "log") {
|
|
387
|
+
if (logRepaintTimer) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
logRepaintTimer = setTimeout(() => {
|
|
391
|
+
flushRepaint();
|
|
392
|
+
}, LOG_REPAINT_DEBOUNCE_MS);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
flushRepaint();
|
|
378
396
|
});
|
|
379
397
|
controller.mount();
|
|
380
398
|
return () => {
|
|
399
|
+
if (logRepaintTimer) {
|
|
400
|
+
clearTimeout(logRepaintTimer);
|
|
401
|
+
}
|
|
381
402
|
unsubscribe();
|
|
382
403
|
controller.destroy();
|
|
383
404
|
};
|
|
@@ -541,8 +562,8 @@ class InkInteractiveSession {
|
|
|
541
562
|
clearSummary() {
|
|
542
563
|
this.controller.clearSummary();
|
|
543
564
|
}
|
|
544
|
-
setScope(scopeKey, jiraIssueKey) {
|
|
545
|
-
this.controller.setScope(scopeKey, jiraIssueKey);
|
|
565
|
+
setScope(scopeKey, jiraIssueKey, gitBranchName) {
|
|
566
|
+
this.controller.setScope(scopeKey, jiraIssueKey, gitBranchName);
|
|
546
567
|
}
|
|
547
568
|
appendLog(text) {
|
|
548
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),
|
package/dist/interactive/tree.js
CHANGED
|
@@ -75,7 +75,7 @@ export function buildFlowTree(flows) {
|
|
|
75
75
|
children: sortNodes(node.children),
|
|
76
76
|
}
|
|
77
77
|
: node);
|
|
78
|
-
const orderedRootNames = ["custom", "default"];
|
|
78
|
+
const orderedRootNames = ["global", "custom", "default"];
|
|
79
79
|
const sortedRoots = [...roots.values()].sort((left, right) => {
|
|
80
80
|
const leftIndex = orderedRootNames.indexOf(left.name);
|
|
81
81
|
const rightIndex = orderedRootNames.indexOf(right.name);
|
|
@@ -143,7 +143,7 @@ export function collectInitiallyExpandedFolderKeys(flowTree) {
|
|
|
143
143
|
if (node.kind !== "folder") {
|
|
144
144
|
continue;
|
|
145
145
|
}
|
|
146
|
-
const expandedByDefault = node.pathSegments.length === 1 && node.name === "default";
|
|
146
|
+
const expandedByDefault = node.pathSegments.length === 1 && (node.name === "default" || node.name === "global");
|
|
147
147
|
if (expandedByDefault) {
|
|
148
148
|
keys.push(node.key);
|
|
149
149
|
}
|