agentweaver 0.1.14 → 0.1.16
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 +29 -7
- package/dist/artifact-manifest.js +219 -0
- package/dist/artifacts.js +21 -1
- package/dist/doctor/checks/cwd-context.js +4 -3
- package/dist/doctor/checks/env-diagnostics.js +193 -71
- package/dist/doctor/checks/flow-readiness.js +212 -203
- package/dist/doctor/index.js +1 -1
- package/dist/doctor/orchestrator.js +18 -7
- package/dist/doctor/runner.js +9 -8
- package/dist/doctor/types.js +12 -0
- package/dist/flow-state.js +75 -15
- package/dist/index.js +499 -199
- package/dist/interactive/blessed-session.js +361 -0
- package/dist/interactive/controller.js +1293 -0
- package/dist/interactive/create-interactive-session.js +5 -0
- package/dist/interactive/ink/index.js +576 -0
- package/dist/interactive/progress.js +245 -0
- package/dist/interactive/selectors.js +14 -0
- package/dist/interactive/session.js +1 -0
- package/dist/interactive/state.js +34 -0
- package/dist/interactive/tree.js +155 -0
- package/dist/interactive/types.js +1 -0
- package/dist/interactive/view-model.js +1 -0
- package/dist/interactive-ui.js +159 -194
- package/dist/pipeline/context.js +1 -0
- package/dist/pipeline/declarative-flow-runner.js +212 -6
- package/dist/pipeline/declarative-flows.js +27 -0
- package/dist/pipeline/execution-routing-config.js +15 -0
- package/dist/pipeline/flow-catalog.js +23 -3
- package/dist/pipeline/flow-run-resume.js +29 -0
- package/dist/pipeline/flow-specs/auto-common.json +89 -360
- package/dist/pipeline/flow-specs/auto-golang.json +58 -363
- package/dist/pipeline/flow-specs/auto-simple.json +141 -0
- package/dist/pipeline/flow-specs/bugz/bug-analyze.json +2 -0
- package/dist/pipeline/flow-specs/bugz/bug-fix.json +1 -0
- package/dist/pipeline/flow-specs/design-review/design-review-loop.json +304 -0
- package/dist/pipeline/flow-specs/design-review.json +249 -0
- package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +11 -0
- package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +2 -0
- package/dist/pipeline/flow-specs/gitlab/mr-description.json +1 -0
- package/dist/pipeline/flow-specs/go/run-go-linter-loop.json +2 -0
- package/dist/pipeline/flow-specs/go/run-go-tests-loop.json +2 -0
- package/dist/pipeline/flow-specs/implement.json +24 -5
- package/dist/pipeline/flow-specs/instant-task.json +177 -0
- package/dist/pipeline/flow-specs/normalize-task-source.json +311 -0
- package/dist/pipeline/flow-specs/plan-revise.json +267 -0
- package/dist/pipeline/flow-specs/plan.json +48 -70
- package/dist/pipeline/flow-specs/review/review-fix.json +24 -4
- package/dist/pipeline/flow-specs/review/review-loop.json +351 -45
- package/dist/pipeline/flow-specs/review/review-project-loop.json +590 -0
- package/dist/pipeline/flow-specs/review/review-project.json +12 -0
- package/dist/pipeline/flow-specs/review/review.json +37 -31
- package/dist/pipeline/flow-specs/task-describe.json +62 -2
- package/dist/pipeline/flow-specs/task-source/jira-fetch.json +70 -0
- package/dist/pipeline/flow-specs/task-source/manual-input.json +216 -0
- package/dist/pipeline/node-registry.js +49 -1
- package/dist/pipeline/node-runner.js +3 -2
- package/dist/pipeline/nodes/build-review-fix-prompt-node.js +5 -1
- package/dist/pipeline/nodes/clear-ready-to-merge-node.js +11 -0
- package/dist/pipeline/nodes/commit-message-form-node.js +8 -0
- package/dist/pipeline/nodes/design-review-verdict-node.js +36 -0
- package/dist/pipeline/nodes/ensure-summary-json-node.js +70 -0
- package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +19 -2
- package/dist/pipeline/nodes/fetch-gitlab-review-node.js +19 -2
- package/dist/pipeline/nodes/flow-run-node.js +226 -7
- package/dist/pipeline/nodes/git-commit-form-node.js +8 -0
- package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +19 -2
- package/dist/pipeline/nodes/jira-fetch-node.js +50 -4
- package/dist/pipeline/nodes/llm-prompt-node.js +32 -12
- package/dist/pipeline/nodes/planning-bundle-node.js +10 -0
- package/dist/pipeline/nodes/review-verdict-node.js +86 -0
- package/dist/pipeline/nodes/select-files-form-node.js +8 -0
- package/dist/pipeline/nodes/structured-summary-node.js +24 -0
- package/dist/pipeline/nodes/user-input-node.js +38 -3
- package/dist/pipeline/nodes/write-selection-file-node.js +20 -4
- package/dist/pipeline/prompt-registry.js +5 -1
- package/dist/pipeline/prompt-runtime.js +4 -1
- package/dist/pipeline/review-iteration.js +26 -0
- package/dist/pipeline/spec-compiler.js +2 -0
- package/dist/pipeline/spec-types.js +5 -0
- package/dist/pipeline/spec-validator.js +14 -0
- package/dist/pipeline/value-resolver.js +84 -1
- package/dist/prompts.js +82 -13
- package/dist/review-severity.js +45 -0
- package/dist/runtime/artifact-registry.js +402 -0
- package/dist/runtime/design-review-input-contract.js +113 -0
- package/dist/runtime/env-loader.js +3 -0
- package/dist/runtime/execution-routing-store.js +134 -0
- package/dist/runtime/execution-routing.js +227 -0
- package/dist/runtime/interactive-execution-routing.js +462 -0
- package/dist/runtime/plan-revise-input-contract.js +147 -0
- package/dist/runtime/planning-bundle.js +123 -0
- package/dist/runtime/ready-to-merge.js +31 -0
- package/dist/runtime/review-input-contract.js +100 -0
- package/dist/scope.js +11 -2
- package/dist/structured-artifact-schema-registry.js +10 -0
- package/dist/structured-artifact-schemas.json +257 -1
- package/dist/structured-artifacts.js +83 -6
- package/dist/user-input.js +70 -3
- package/package.json +6 -3
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { TaskRunnerError } from "../../errors.js";
|
|
4
|
+
import { InteractiveSessionController } from "../controller.js";
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
function hasRuntimeModule(moduleName) {
|
|
7
|
+
try {
|
|
8
|
+
require.resolve(moduleName);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function clampScrollOffset(value, maxOffset) {
|
|
16
|
+
return Math.min(Math.max(0, value), maxOffset);
|
|
17
|
+
}
|
|
18
|
+
function buildSolidFill(width, height) {
|
|
19
|
+
const safeWidth = Math.max(1, width);
|
|
20
|
+
const safeHeight = Math.max(1, height);
|
|
21
|
+
const line = " ".repeat(safeWidth);
|
|
22
|
+
return Array.from({ length: safeHeight }, () => line).join("\n");
|
|
23
|
+
}
|
|
24
|
+
function parseActionSegments(line) {
|
|
25
|
+
const matches = line.match(/\[[^\]]+\]/g);
|
|
26
|
+
if (!matches) {
|
|
27
|
+
return [{ text: line }];
|
|
28
|
+
}
|
|
29
|
+
const segments = [];
|
|
30
|
+
let cursor = 0;
|
|
31
|
+
for (const match of matches) {
|
|
32
|
+
const index = line.indexOf(match, cursor);
|
|
33
|
+
if (index > cursor) {
|
|
34
|
+
segments.push({ color: "gray", text: line.slice(cursor, index) });
|
|
35
|
+
}
|
|
36
|
+
segments.push({
|
|
37
|
+
backgroundColor: "yellow",
|
|
38
|
+
bold: true,
|
|
39
|
+
color: "black",
|
|
40
|
+
text: match,
|
|
41
|
+
});
|
|
42
|
+
cursor = index + match.length;
|
|
43
|
+
}
|
|
44
|
+
if (cursor < line.length) {
|
|
45
|
+
segments.push({ color: "gray", text: line.slice(cursor) });
|
|
46
|
+
}
|
|
47
|
+
return segments;
|
|
48
|
+
}
|
|
49
|
+
function stylePanelLine(panelTitle, line) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (panelTitle.includes("Flows")) {
|
|
52
|
+
const folderMatch = line.match(/^([> ]\s+)(\s*)([▾▸]) (.+)$/);
|
|
53
|
+
if (folderMatch) {
|
|
54
|
+
const prefix = folderMatch[1] ?? "";
|
|
55
|
+
const indent = folderMatch[2] ?? "";
|
|
56
|
+
const icon = folderMatch[3] ?? "▸";
|
|
57
|
+
const name = folderMatch[4] ?? "";
|
|
58
|
+
const selected = prefix.startsWith(">");
|
|
59
|
+
const lineBackgroundColor = selected ? "cyan" : undefined;
|
|
60
|
+
return {
|
|
61
|
+
...(lineBackgroundColor ? { backgroundColor: lineBackgroundColor } : {}),
|
|
62
|
+
segments: [
|
|
63
|
+
{
|
|
64
|
+
...(lineBackgroundColor ? { backgroundColor: lineBackgroundColor } : {}),
|
|
65
|
+
color: selected ? "black" : "gray",
|
|
66
|
+
text: `${prefix}${indent}`,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
...(lineBackgroundColor ? { backgroundColor: lineBackgroundColor } : {}),
|
|
70
|
+
bold: true,
|
|
71
|
+
color: "magenta",
|
|
72
|
+
text: `${icon} ${name}`,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
text: line,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (panelTitle.includes("Flows") && line.startsWith("> ")) {
|
|
80
|
+
return {
|
|
81
|
+
backgroundColor: "cyan",
|
|
82
|
+
bold: true,
|
|
83
|
+
color: "black",
|
|
84
|
+
text: line,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if ((panelTitle === "User Input" || panelTitle === "Confirm") && line.startsWith("> ")) {
|
|
88
|
+
return {
|
|
89
|
+
backgroundColor: "cyan",
|
|
90
|
+
bold: true,
|
|
91
|
+
color: "black",
|
|
92
|
+
text: line,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (panelTitle === "Confirm") {
|
|
96
|
+
if (/^(Run|Interrupt) flow /.test(line)) {
|
|
97
|
+
return { bold: true, color: "cyan", text: line };
|
|
98
|
+
}
|
|
99
|
+
if (line.includes("[") && line.includes("]")) {
|
|
100
|
+
return {
|
|
101
|
+
segments: parseActionSegments(line),
|
|
102
|
+
text: line,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (trimmed.startsWith("Left/Right or Tab:")) {
|
|
106
|
+
return { color: "gray", text: line };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (panelTitle === "User Input") {
|
|
110
|
+
const checkboxMatch = line.match(/^(\s*[> ]\s*)(\[[x ]\])(.*)$/);
|
|
111
|
+
if (checkboxMatch) {
|
|
112
|
+
const prefix = checkboxMatch[1] ?? "";
|
|
113
|
+
const marker = checkboxMatch[2] ?? "[ ]";
|
|
114
|
+
const suffix = checkboxMatch[3] ?? "";
|
|
115
|
+
const selected = prefix.includes(">");
|
|
116
|
+
const lineBackgroundColor = selected ? "cyan" : undefined;
|
|
117
|
+
const lineColor = selected ? "black" : "white";
|
|
118
|
+
return {
|
|
119
|
+
...(lineBackgroundColor ? { backgroundColor: lineBackgroundColor } : {}),
|
|
120
|
+
color: lineColor,
|
|
121
|
+
segments: [
|
|
122
|
+
{ ...(lineBackgroundColor ? { backgroundColor: lineBackgroundColor } : {}), color: lineColor, text: prefix },
|
|
123
|
+
{
|
|
124
|
+
...(lineBackgroundColor ? { backgroundColor: lineBackgroundColor } : {}),
|
|
125
|
+
bold: marker === "[x]",
|
|
126
|
+
color: selected ? "black" : marker === "[x]" ? "green" : "gray",
|
|
127
|
+
text: marker,
|
|
128
|
+
},
|
|
129
|
+
{ ...(lineBackgroundColor ? { backgroundColor: lineBackgroundColor } : {}), color: lineColor, text: suffix },
|
|
130
|
+
],
|
|
131
|
+
text: line,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (/^Field \d+\/\d+/.test(trimmed)) {
|
|
135
|
+
return { bold: true, color: "cyan", text: line };
|
|
136
|
+
}
|
|
137
|
+
if (trimmed === "Preview:" || trimmed === "Options:" || trimmed === "Text input:") {
|
|
138
|
+
return { bold: true, color: "magenta", text: line };
|
|
139
|
+
}
|
|
140
|
+
if (/^[┌└][─]+[┐┘]$/.test(trimmed) || /^│ .* │$/.test(trimmed)) {
|
|
141
|
+
return { color: "cyan", text: line };
|
|
142
|
+
}
|
|
143
|
+
if (/^Preview \d+-\d+ of \d+/.test(trimmed)) {
|
|
144
|
+
return { color: "gray", text: line };
|
|
145
|
+
}
|
|
146
|
+
if (trimmed.startsWith("Hint:")) {
|
|
147
|
+
return { color: "gray", text: line };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (panelTitle === "Help") {
|
|
151
|
+
if (trimmed === "Keys:" || trimmed === "Available flows:") {
|
|
152
|
+
return { bold: true, color: "magenta", text: line };
|
|
153
|
+
}
|
|
154
|
+
if (/^[A-Z][^:]+$/.test(trimmed) && !trimmed.includes(" ")) {
|
|
155
|
+
return { bold: true, color: "cyan", text: line };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { text: line };
|
|
159
|
+
}
|
|
160
|
+
function renderStyledContent(react, ink, panelTitle, content, backgroundColor) {
|
|
161
|
+
const { Fragment, createElement } = react;
|
|
162
|
+
const { Box, Text } = ink;
|
|
163
|
+
const lines = content.length > 0 ? content.split("\n") : [" "];
|
|
164
|
+
const wrap = panelTitle.includes("Flows") ? "truncate-end" : undefined;
|
|
165
|
+
return createElement(Box, {
|
|
166
|
+
flexDirection: "column",
|
|
167
|
+
flexGrow: 1,
|
|
168
|
+
width: "100%",
|
|
169
|
+
backgroundColor,
|
|
170
|
+
}, lines.map((line, index) => {
|
|
171
|
+
const styledLine = stylePanelLine(panelTitle, line);
|
|
172
|
+
const lineBackgroundColor = styledLine.backgroundColor ?? backgroundColor;
|
|
173
|
+
const contentNode = styledLine.segments
|
|
174
|
+
? createElement(Text, {
|
|
175
|
+
backgroundColor: lineBackgroundColor,
|
|
176
|
+
bold: styledLine.bold,
|
|
177
|
+
color: styledLine.color,
|
|
178
|
+
...(wrap ? { wrap } : {}),
|
|
179
|
+
}, styledLine.segments.map((segment, segmentIndex) => createElement(Text, {
|
|
180
|
+
key: `segment-${index}-${segmentIndex}`,
|
|
181
|
+
backgroundColor: segment.backgroundColor ?? lineBackgroundColor,
|
|
182
|
+
bold: segment.bold,
|
|
183
|
+
color: segment.color,
|
|
184
|
+
...(wrap ? { wrap } : {}),
|
|
185
|
+
}, segment.text)))
|
|
186
|
+
: createElement(Text, {
|
|
187
|
+
backgroundColor: lineBackgroundColor,
|
|
188
|
+
bold: styledLine.bold,
|
|
189
|
+
color: styledLine.color,
|
|
190
|
+
...(wrap ? { wrap } : {}),
|
|
191
|
+
}, styledLine.text.length > 0 ? styledLine.text : " ");
|
|
192
|
+
return createElement(Box, {
|
|
193
|
+
key: `line-${index}`,
|
|
194
|
+
width: "100%",
|
|
195
|
+
backgroundColor: lineBackgroundColor,
|
|
196
|
+
}, createElement(Fragment, null, contentNode));
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
export function sliceFromScroll(text, offset, maxLines = 12) {
|
|
200
|
+
return resolveScrollWindow(text, offset, maxLines).text;
|
|
201
|
+
}
|
|
202
|
+
function resolveScrollWindow(text, offset, maxLines = 12) {
|
|
203
|
+
const lines = text.split("\n");
|
|
204
|
+
const viewportLines = Math.max(1, maxLines);
|
|
205
|
+
const boundedOffset = clampScrollOffset(offset, Math.max(0, lines.length - viewportLines));
|
|
206
|
+
const start = boundedOffset;
|
|
207
|
+
const visible = lines.slice(start, start + maxLines);
|
|
208
|
+
return {
|
|
209
|
+
text: visible.join("\n"),
|
|
210
|
+
startLine: start,
|
|
211
|
+
totalLines: lines.length,
|
|
212
|
+
viewportLines,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function buildScrollbarLines(scrollbar) {
|
|
216
|
+
const viewport = Math.max(1, scrollbar.viewportLines);
|
|
217
|
+
const total = Math.max(0, scrollbar.totalLines);
|
|
218
|
+
if (total <= viewport) {
|
|
219
|
+
return Array.from({ length: viewport }, () => ({ char: " ", color: "gray" }));
|
|
220
|
+
}
|
|
221
|
+
const thumbSize = Math.max(1, Math.round((viewport * viewport) / total));
|
|
222
|
+
const maxStart = Math.max(1, total - viewport);
|
|
223
|
+
const maxThumbOffset = Math.max(0, viewport - thumbSize);
|
|
224
|
+
const thumbStart = maxThumbOffset === 0
|
|
225
|
+
? 0
|
|
226
|
+
: Math.round((scrollbar.startLine / maxStart) * maxThumbOffset);
|
|
227
|
+
return Array.from({ length: viewport }, (_, index) => {
|
|
228
|
+
const inThumb = index >= thumbStart && index < thumbStart + thumbSize;
|
|
229
|
+
return {
|
|
230
|
+
char: inThumb ? "█" : "│",
|
|
231
|
+
color: inThumb ? "cyan" : "gray",
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
export function normalizeInkKeypress(input, key) {
|
|
236
|
+
let name;
|
|
237
|
+
if (key.upArrow) {
|
|
238
|
+
name = "up";
|
|
239
|
+
}
|
|
240
|
+
else if (key.downArrow) {
|
|
241
|
+
name = "down";
|
|
242
|
+
}
|
|
243
|
+
else if (key.leftArrow) {
|
|
244
|
+
name = "left";
|
|
245
|
+
}
|
|
246
|
+
else if (key.rightArrow) {
|
|
247
|
+
name = "right";
|
|
248
|
+
}
|
|
249
|
+
else if (key.pageUp) {
|
|
250
|
+
name = "pageup";
|
|
251
|
+
}
|
|
252
|
+
else if (key.pageDown) {
|
|
253
|
+
name = "pagedown";
|
|
254
|
+
}
|
|
255
|
+
else if (key.return) {
|
|
256
|
+
name = "enter";
|
|
257
|
+
}
|
|
258
|
+
else if (key.escape) {
|
|
259
|
+
name = "escape";
|
|
260
|
+
}
|
|
261
|
+
else if (key.tab) {
|
|
262
|
+
name = "tab";
|
|
263
|
+
}
|
|
264
|
+
else if (key.backspace || key.delete || input === "\b" || input === "\x7f") {
|
|
265
|
+
name = "backspace";
|
|
266
|
+
}
|
|
267
|
+
else if (input === " ") {
|
|
268
|
+
name = "space";
|
|
269
|
+
}
|
|
270
|
+
else if (input.length === 1) {
|
|
271
|
+
name = input.toLowerCase();
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
...(name ? { name } : {}),
|
|
275
|
+
ctrl: key.ctrl,
|
|
276
|
+
shift: key.shift,
|
|
277
|
+
meta: key.meta,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function buildFlowListText(viewModel, maxLines) {
|
|
281
|
+
const totalItems = viewModel.flowItems.length;
|
|
282
|
+
const safeMaxLines = Math.max(1, maxLines);
|
|
283
|
+
const unclampedStart = viewModel.selectedFlowIndex - Math.floor(safeMaxLines / 2);
|
|
284
|
+
const maxStart = Math.max(0, totalItems - safeMaxLines);
|
|
285
|
+
const start = clampScrollOffset(unclampedStart, maxStart);
|
|
286
|
+
const visibleItems = viewModel.flowItems.slice(start, start + safeMaxLines);
|
|
287
|
+
return visibleItems
|
|
288
|
+
.map((item, index) => {
|
|
289
|
+
const absoluteIndex = start + index;
|
|
290
|
+
return `${absoluteIndex === viewModel.selectedFlowIndex ? ">" : " "} ${item.label}`;
|
|
291
|
+
})
|
|
292
|
+
.join("\n");
|
|
293
|
+
}
|
|
294
|
+
function createPanelComponent(react, ink) {
|
|
295
|
+
const { createElement } = react;
|
|
296
|
+
const { Box, Text } = ink;
|
|
297
|
+
return function InkPanel({ backgroundColor, borderColor = "green", content, flexGrow, height, scrollbar, title, width, }) {
|
|
298
|
+
const scrollbarLines = scrollbar ? buildScrollbarLines(scrollbar) : null;
|
|
299
|
+
return createElement(Box, {
|
|
300
|
+
borderStyle: "round",
|
|
301
|
+
backgroundColor,
|
|
302
|
+
borderColor,
|
|
303
|
+
flexDirection: "column",
|
|
304
|
+
flexGrow,
|
|
305
|
+
height,
|
|
306
|
+
width,
|
|
307
|
+
paddingX: 1,
|
|
308
|
+
paddingY: 0,
|
|
309
|
+
}, createElement(Box, {
|
|
310
|
+
height: 1,
|
|
311
|
+
width: "100%",
|
|
312
|
+
backgroundColor,
|
|
313
|
+
}, createElement(Text, { bold: true, backgroundColor }, title)), createElement(Box, {
|
|
314
|
+
flexDirection: "row",
|
|
315
|
+
flexGrow: 1,
|
|
316
|
+
width: "100%",
|
|
317
|
+
backgroundColor,
|
|
318
|
+
}, createElement(Box, {
|
|
319
|
+
flexGrow: 1,
|
|
320
|
+
width: scrollbarLines ? undefined : "100%",
|
|
321
|
+
backgroundColor,
|
|
322
|
+
}, renderStyledContent(react, ink, title, content, backgroundColor)), scrollbarLines
|
|
323
|
+
? createElement(Box, {
|
|
324
|
+
flexDirection: "column",
|
|
325
|
+
width: 1,
|
|
326
|
+
marginLeft: 1,
|
|
327
|
+
backgroundColor,
|
|
328
|
+
}, scrollbarLines.map((line, index) => createElement(Text, {
|
|
329
|
+
key: `scrollbar-${title}-${index}`,
|
|
330
|
+
backgroundColor,
|
|
331
|
+
color: line.color,
|
|
332
|
+
}, line.char)))
|
|
333
|
+
: null));
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function createOverlayBox(react, ink, panel, options) {
|
|
337
|
+
const { createElement } = react;
|
|
338
|
+
const { Box, Text } = ink;
|
|
339
|
+
return createElement(Box, {
|
|
340
|
+
key: options.key,
|
|
341
|
+
position: "absolute",
|
|
342
|
+
top: 0,
|
|
343
|
+
left: 0,
|
|
344
|
+
width: "100%",
|
|
345
|
+
height: "100%",
|
|
346
|
+
alignItems: "center",
|
|
347
|
+
justifyContent: "center",
|
|
348
|
+
}, createElement(Box, {
|
|
349
|
+
position: "relative",
|
|
350
|
+
width: options.width,
|
|
351
|
+
height: options.height,
|
|
352
|
+
backgroundColor: "black",
|
|
353
|
+
}, createElement(Box, {
|
|
354
|
+
position: "absolute",
|
|
355
|
+
top: 0,
|
|
356
|
+
left: 0,
|
|
357
|
+
width: options.width,
|
|
358
|
+
height: options.height,
|
|
359
|
+
backgroundColor: "black",
|
|
360
|
+
}, createElement(Text, { backgroundColor: "black" }, buildSolidFill(options.width, options.height))), createElement(panel, {
|
|
361
|
+
backgroundColor: "black",
|
|
362
|
+
borderColor: options.borderColor,
|
|
363
|
+
content: options.content,
|
|
364
|
+
height: options.height,
|
|
365
|
+
title: options.title,
|
|
366
|
+
width: options.width,
|
|
367
|
+
})));
|
|
368
|
+
}
|
|
369
|
+
function createInkApp(react, ink, controller) {
|
|
370
|
+
const { Fragment, createElement, useEffect, useState } = react;
|
|
371
|
+
const { Box, Text, useInput, useStdout } = ink;
|
|
372
|
+
const Panel = createPanelComponent(react, ink);
|
|
373
|
+
const App = () => {
|
|
374
|
+
const [, setVersion] = useState(0);
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
const unsubscribe = controller.subscribe(() => {
|
|
377
|
+
setVersion((previous) => previous + 1);
|
|
378
|
+
});
|
|
379
|
+
controller.mount();
|
|
380
|
+
return () => {
|
|
381
|
+
unsubscribe();
|
|
382
|
+
controller.destroy();
|
|
383
|
+
};
|
|
384
|
+
}, []);
|
|
385
|
+
useInput((input, key) => {
|
|
386
|
+
void controller.handleKeypress(input, normalizeInkKeypress(input, key));
|
|
387
|
+
});
|
|
388
|
+
const { stdout } = useStdout();
|
|
389
|
+
const terminalRows = Math.max(stdout.rows ?? 24, 20);
|
|
390
|
+
const terminalColumns = Math.max(stdout.columns ?? 80, 80);
|
|
391
|
+
const bodyHeight = Math.max(terminalRows - 8, 12);
|
|
392
|
+
const minRightLogHeight = 4;
|
|
393
|
+
const minRightSummaryHeight = 4;
|
|
394
|
+
const sideStatusHeight = 6;
|
|
395
|
+
const sideDescriptionHeight = Math.max(8, Math.floor(bodyHeight * 0.22));
|
|
396
|
+
const sideFlowListHeight = Math.max(8, bodyHeight - sideDescriptionHeight - sideStatusHeight);
|
|
397
|
+
const formModalWidth = Math.max(56, Math.floor(terminalColumns * 0.72));
|
|
398
|
+
const formContentWidth = Math.max(8, formModalWidth - 8);
|
|
399
|
+
const viewModel = controller.getViewModel({ formContentWidth });
|
|
400
|
+
const rightProgressHeight = sideFlowListHeight;
|
|
401
|
+
const rightLowerAreaHeight = Math.max(0, bodyHeight - rightProgressHeight);
|
|
402
|
+
const desiredRightSummaryHeight = viewModel.summaryVisible ? Math.max(8, Math.floor(bodyHeight * 0.24)) : 0;
|
|
403
|
+
const maxRightSummaryHeight = Math.max(0, rightLowerAreaHeight - minRightLogHeight);
|
|
404
|
+
const rightSummaryHeight = viewModel.summaryVisible
|
|
405
|
+
? Math.min(desiredRightSummaryHeight, maxRightSummaryHeight)
|
|
406
|
+
: 0;
|
|
407
|
+
const summaryPanelVisible = viewModel.summaryVisible && rightSummaryHeight >= minRightSummaryHeight;
|
|
408
|
+
const rightLogHeight = Math.max(minRightLogHeight, rightLowerAreaHeight - (summaryPanelVisible ? rightSummaryHeight : 0));
|
|
409
|
+
const progressViewportLines = Math.max(4, rightProgressHeight - 4);
|
|
410
|
+
const progressWindow = resolveScrollWindow(viewModel.progressText, viewModel.progressScrollOffset, progressViewportLines);
|
|
411
|
+
const overlayPanels = [];
|
|
412
|
+
if (viewModel.helpVisible) {
|
|
413
|
+
const modalHeight = Math.max(10, Math.floor(terminalRows * 0.6));
|
|
414
|
+
const modalWidth = Math.max(48, Math.floor(terminalColumns * 0.64));
|
|
415
|
+
overlayPanels.push(createOverlayBox(react, ink, Panel, {
|
|
416
|
+
key: "help",
|
|
417
|
+
borderColor: "magenta",
|
|
418
|
+
height: modalHeight,
|
|
419
|
+
width: modalWidth,
|
|
420
|
+
title: "Help",
|
|
421
|
+
content: sliceFromScroll(viewModel.helpText, viewModel.helpScrollOffset, Math.max(8, modalHeight - 4)),
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
if (viewModel.confirmText) {
|
|
425
|
+
overlayPanels.push(createOverlayBox(react, ink, Panel, {
|
|
426
|
+
key: "confirm",
|
|
427
|
+
borderColor: "yellow",
|
|
428
|
+
height: 9,
|
|
429
|
+
width: Math.max(48, Math.floor(terminalColumns * 0.44)),
|
|
430
|
+
title: "Confirm",
|
|
431
|
+
content: viewModel.confirmText,
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
if (viewModel.form) {
|
|
435
|
+
const modalHeight = Math.max(10, Math.floor(terminalRows * 0.68));
|
|
436
|
+
overlayPanels.push(createOverlayBox(react, ink, Panel, {
|
|
437
|
+
key: "form",
|
|
438
|
+
borderColor: "magenta",
|
|
439
|
+
height: modalHeight,
|
|
440
|
+
width: formModalWidth,
|
|
441
|
+
title: viewModel.form.title,
|
|
442
|
+
content: viewModel.form.content,
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
return createElement(Fragment, null, createElement(Box, { flexDirection: "column", width: terminalColumns, height: terminalRows }, createElement(Panel, {
|
|
446
|
+
title: viewModel.title,
|
|
447
|
+
borderColor: "cyan",
|
|
448
|
+
height: 4,
|
|
449
|
+
content: viewModel.header,
|
|
450
|
+
}), createElement(Box, { flexDirection: "row", alignItems: "flex-start", flexGrow: 1 }, createElement(Box, { flexDirection: "column", width: "34%", marginRight: 1, height: bodyHeight }, createElement(Panel, {
|
|
451
|
+
title: viewModel.flowListTitle,
|
|
452
|
+
borderColor: "cyan",
|
|
453
|
+
height: sideFlowListHeight,
|
|
454
|
+
content: buildFlowListText(viewModel, Math.max(4, sideFlowListHeight - 4)),
|
|
455
|
+
}), createElement(Panel, {
|
|
456
|
+
title: "Flow Description",
|
|
457
|
+
borderColor: "magenta",
|
|
458
|
+
height: sideDescriptionHeight,
|
|
459
|
+
content: viewModel.descriptionText,
|
|
460
|
+
}), createElement(Panel, {
|
|
461
|
+
title: "Status",
|
|
462
|
+
borderColor: "green",
|
|
463
|
+
height: sideStatusHeight,
|
|
464
|
+
content: viewModel.statusText,
|
|
465
|
+
})), createElement(Box, { flexDirection: "column", width: "66%", height: bodyHeight }, createElement(Panel, {
|
|
466
|
+
title: viewModel.progressTitle,
|
|
467
|
+
borderColor: "green",
|
|
468
|
+
height: rightProgressHeight,
|
|
469
|
+
content: progressWindow.text,
|
|
470
|
+
scrollbar: {
|
|
471
|
+
startLine: progressWindow.startLine,
|
|
472
|
+
totalLines: progressWindow.totalLines,
|
|
473
|
+
viewportLines: progressWindow.viewportLines,
|
|
474
|
+
},
|
|
475
|
+
}), summaryPanelVisible
|
|
476
|
+
? createElement(Panel, {
|
|
477
|
+
title: viewModel.summaryTitle,
|
|
478
|
+
borderColor: "green",
|
|
479
|
+
height: rightSummaryHeight,
|
|
480
|
+
content: sliceFromScroll(viewModel.summaryText, viewModel.summaryScrollOffset, Math.max(4, rightSummaryHeight - 4)),
|
|
481
|
+
})
|
|
482
|
+
: null, createElement(Panel, {
|
|
483
|
+
title: viewModel.logTitle,
|
|
484
|
+
borderColor: "yellow",
|
|
485
|
+
height: rightLogHeight,
|
|
486
|
+
content: sliceFromScroll(viewModel.logText, viewModel.logScrollOffset, Math.max(4, rightLogHeight - 4)),
|
|
487
|
+
}))), createElement(Box, {
|
|
488
|
+
borderStyle: "round",
|
|
489
|
+
borderColor: "gray",
|
|
490
|
+
height: 3,
|
|
491
|
+
paddingX: 1,
|
|
492
|
+
}, createElement(Text, null, viewModel.footer)), ...overlayPanels));
|
|
493
|
+
};
|
|
494
|
+
return createElement(App);
|
|
495
|
+
}
|
|
496
|
+
export function isInkRuntimeDependencyAvailable() {
|
|
497
|
+
return hasRuntimeModule("ink") && hasRuntimeModule("react");
|
|
498
|
+
}
|
|
499
|
+
export function describeInkInteractiveSessionAvailability() {
|
|
500
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
501
|
+
return "Interactive mode requires a real TTY on both stdin and stdout.";
|
|
502
|
+
}
|
|
503
|
+
if (!isInkRuntimeDependencyAvailable()) {
|
|
504
|
+
return "Interactive mode requires installed runtime dependencies: run `npm install` in the AgentWeaver checkout or reinstall the published package.";
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
async function loadInkModules() {
|
|
509
|
+
const [inkModule, reactModule] = await Promise.all([import("ink"), import("react")]);
|
|
510
|
+
const react = (reactModule.default ?? reactModule);
|
|
511
|
+
return {
|
|
512
|
+
ink: inkModule,
|
|
513
|
+
react,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
class InkInteractiveSession {
|
|
517
|
+
controller;
|
|
518
|
+
inkInstance = null;
|
|
519
|
+
mountingPromise = null;
|
|
520
|
+
destroyed = false;
|
|
521
|
+
constructor(options) {
|
|
522
|
+
this.controller = new InteractiveSessionController(options);
|
|
523
|
+
}
|
|
524
|
+
mount() {
|
|
525
|
+
if (this.mountingPromise) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
this.mountingPromise = this.mountInk();
|
|
529
|
+
}
|
|
530
|
+
destroy() {
|
|
531
|
+
this.destroyed = true;
|
|
532
|
+
this.inkInstance?.unmount();
|
|
533
|
+
this.inkInstance = null;
|
|
534
|
+
}
|
|
535
|
+
requestUserInput(form) {
|
|
536
|
+
return this.controller.requestUserInput(form);
|
|
537
|
+
}
|
|
538
|
+
setSummary(markdown) {
|
|
539
|
+
this.controller.setSummary(markdown);
|
|
540
|
+
}
|
|
541
|
+
clearSummary() {
|
|
542
|
+
this.controller.clearSummary();
|
|
543
|
+
}
|
|
544
|
+
setScope(scopeKey, jiraIssueKey) {
|
|
545
|
+
this.controller.setScope(scopeKey, jiraIssueKey);
|
|
546
|
+
}
|
|
547
|
+
appendLog(text) {
|
|
548
|
+
this.controller.appendLog(text);
|
|
549
|
+
}
|
|
550
|
+
setFlowFailed(flowId) {
|
|
551
|
+
this.controller.setFlowFailed(flowId);
|
|
552
|
+
}
|
|
553
|
+
interruptActiveForm(message) {
|
|
554
|
+
this.controller.interruptActiveForm(message);
|
|
555
|
+
}
|
|
556
|
+
async mountInk() {
|
|
557
|
+
const { ink, react } = await loadInkModules();
|
|
558
|
+
if (this.destroyed) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
this.inkInstance = ink.render(createInkApp(react, ink, this.controller), {
|
|
562
|
+
exitOnCtrlC: false,
|
|
563
|
+
patchConsole: false,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
export function isInkInteractiveSessionAvailable() {
|
|
568
|
+
return describeInkInteractiveSessionAvailability() === null;
|
|
569
|
+
}
|
|
570
|
+
export function createInkInteractiveSession(options) {
|
|
571
|
+
const unavailableReason = describeInkInteractiveSessionAvailability();
|
|
572
|
+
if (unavailableReason) {
|
|
573
|
+
throw new TaskRunnerError(unavailableReason);
|
|
574
|
+
}
|
|
575
|
+
return new InkInteractiveSession(options);
|
|
576
|
+
}
|