singleton-pipeline 0.4.0-beta.13 → 0.4.0-beta.14
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/dist/packages/cli/src/assets/singleton-logo.txt +10 -0
- package/dist/packages/cli/src/commands/new.js +763 -0
- package/dist/packages/cli/src/commands/repl.js +557 -0
- package/dist/packages/cli/src/commands/usage.js +49 -0
- package/dist/packages/cli/src/executor/debug-loop.js +525 -0
- package/dist/packages/cli/src/executor/inputs.js +226 -0
- package/dist/packages/cli/src/executor/outputs.js +134 -0
- package/dist/packages/cli/src/executor/preflight.js +605 -0
- package/dist/packages/cli/src/executor/replay-loop.js +120 -0
- package/dist/packages/cli/src/executor/run-report.js +209 -0
- package/dist/packages/cli/src/executor/run-setup.js +114 -0
- package/dist/packages/cli/src/executor/security-review.js +97 -0
- package/dist/packages/cli/src/executor/snapshot-manager.js +349 -0
- package/dist/packages/cli/src/executor/step-runner.js +241 -0
- package/dist/packages/cli/src/executor.js +584 -0
- package/dist/packages/cli/src/index.js +107 -0
- package/dist/packages/cli/src/parser.js +89 -0
- package/dist/packages/cli/src/runners/_shared.js +96 -0
- package/dist/packages/cli/src/runners/claude.js +103 -0
- package/dist/packages/cli/src/runners/codex-instructions.js +69 -0
- package/dist/packages/cli/src/runners/codex.js +141 -0
- package/dist/packages/cli/src/runners/copilot.js +209 -0
- package/dist/packages/cli/src/runners/index.js +18 -0
- package/dist/packages/cli/src/runners/opencode.js +240 -0
- package/dist/packages/cli/src/scanner.js +43 -0
- package/dist/packages/cli/src/security/policy.js +115 -0
- package/dist/packages/cli/src/sentinels.js +1 -0
- package/dist/packages/cli/src/shell.js +753 -0
- package/dist/packages/cli/src/theme.js +39 -0
- package/dist/packages/cli/src/timeline.js +238 -0
- package/dist/packages/cli/src/types.js +1 -0
- package/dist/packages/cli/src/usage/aggregator.js +44 -0
- package/dist/packages/cli/src/usage/reader.js +30 -0
- package/dist/packages/cli/src/usage/types.js +1 -0
- package/dist/packages/server/src/index.js +36 -0
- package/dist/packages/server/src/routes/agents.js +31 -0
- package/dist/packages/server/src/routes/files.js +45 -0
- package/dist/packages/server/src/routes/pipelines.js +74 -0
- package/docs/reference.md +28 -0
- package/package.json +15 -14
- package/packages/web/dist/assets/{index-CnKytBly.js → index-9S0goZlQ.js} +1 -1
- package/packages/web/dist/assets/{index-CCFWfCA2.css → index-iV4UtXoN.css} +1 -1
- package/packages/web/dist/assets/logo-COSyZmgk.png +0 -0
- package/packages/web/dist/index.html +2 -2
- package/packages/cli/package.json +0 -18
- package/packages/cli/src/commands/new.js +0 -786
- package/packages/cli/src/commands/repl.js +0 -548
- package/packages/cli/src/executor/debug-loop.js +0 -587
- package/packages/cli/src/executor/inputs.js +0 -202
- package/packages/cli/src/executor/outputs.js +0 -140
- package/packages/cli/src/executor/preflight.js +0 -459
- package/packages/cli/src/executor/replay-loop.js +0 -172
- package/packages/cli/src/executor/run-report.js +0 -189
- package/packages/cli/src/executor/run-setup.js +0 -93
- package/packages/cli/src/executor/security-review.js +0 -108
- package/packages/cli/src/executor/snapshot-manager.js +0 -335
- package/packages/cli/src/executor/step-runner.js +0 -266
- package/packages/cli/src/executor.js +0 -652
- package/packages/cli/src/index.js +0 -107
- package/packages/cli/src/parser.js +0 -78
- package/packages/cli/src/runners/_shared.js +0 -83
- package/packages/cli/src/runners/claude.js +0 -122
- package/packages/cli/src/runners/codex-instructions.js +0 -75
- package/packages/cli/src/runners/codex.js +0 -165
- package/packages/cli/src/runners/copilot.js +0 -224
- package/packages/cli/src/runners/index.js +0 -20
- package/packages/cli/src/runners/opencode.js +0 -265
- package/packages/cli/src/scanner.js +0 -47
- package/packages/cli/src/security/policy.js +0 -126
- package/packages/cli/src/shell.js +0 -732
- package/packages/cli/src/theme.js +0 -46
- package/packages/cli/src/timeline.js +0 -180
- package/packages/server/package.json +0 -11
- package/packages/server/src/index.js +0 -43
- package/packages/server/src/routes/agents.js +0 -32
- package/packages/server/src/routes/files.js +0 -42
- package/packages/server/src/routes/pipelines.js +0 -74
|
@@ -1,732 +0,0 @@
|
|
|
1
|
-
import blessed from 'blessed';
|
|
2
|
-
|
|
3
|
-
// ── Pastel theme ────────────────────────────────────────────────
|
|
4
|
-
export const C = {
|
|
5
|
-
violet: '#C084FC', // accent principal
|
|
6
|
-
pink: '#F9A8D4', // secondaire
|
|
7
|
-
blue: '#93C5FD', // tertiaire
|
|
8
|
-
mint: '#6EE7B7', // success
|
|
9
|
-
peach: '#FDBA74', // warning
|
|
10
|
-
salmon: '#FCA5A5', // erreur
|
|
11
|
-
dimV: '#b58eb8', // violet clair (texte muted)
|
|
12
|
-
line: '#4A4060', // separators
|
|
13
|
-
ghost: '#797C81', // gris discret lisible sur fond sombre
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// ── Semantic tokens — one color, one role ────────────────────────
|
|
17
|
-
// Use these everywhere. Each token carries meaning, not decoration.
|
|
18
|
-
// text — primary readable body text
|
|
19
|
-
// muted — secondary text, metadata (dates, versions, descriptions)
|
|
20
|
-
// subtle — decorative separators (·, ─)
|
|
21
|
-
// accent — brand, interactive elements (slash commands, agent IDs)
|
|
22
|
-
// keyword — technical labels and feature/provider names
|
|
23
|
-
// string — user data (pipeline names, paths, URLs)
|
|
24
|
-
// success — positive markers (✓), confirmations
|
|
25
|
-
// warning — attention markers (!), announcements (New)
|
|
26
|
-
// error — failure markers (✕), blocking errors
|
|
27
|
-
export const S = {
|
|
28
|
-
text: '#FFFFFF',
|
|
29
|
-
muted: '#8E8B9E', // soft cool gray, very subtly violet-tinted — reads as "quiet"
|
|
30
|
-
subtle: '#797C81',
|
|
31
|
-
border: '#4A4060', // structural separators (blessed.line widgets, frames)
|
|
32
|
-
accent: '#C084FC',
|
|
33
|
-
keyword: '#93C5FD',
|
|
34
|
-
string: '#F9A8D4',
|
|
35
|
-
success: '#6EE7B7',
|
|
36
|
-
warning: '#FDBA74',
|
|
37
|
-
error: '#FCA5A5',
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export function createShell() {
|
|
41
|
-
const screen = blessed.screen({ smartCSR: true, title: 'Singleton' });
|
|
42
|
-
const inputHints = [
|
|
43
|
-
'/help to start',
|
|
44
|
-
'/scan to scan agents',
|
|
45
|
-
'/new to create an agent',
|
|
46
|
-
'/run to execute a pipeline',
|
|
47
|
-
'/commit-last to commit the last run',
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
// ── Normal mode ────────────────────────────────────────────────
|
|
51
|
-
const content = blessed.log({
|
|
52
|
-
top: 0, left: 0,
|
|
53
|
-
width: '100%', height: '100%-4',
|
|
54
|
-
scrollable: true, alwaysScroll: true,
|
|
55
|
-
tags: true,
|
|
56
|
-
border: { type: 'line' },
|
|
57
|
-
style: { border: { fg: S.border } },
|
|
58
|
-
padding: { left: 1, top: 0, right: 1 },
|
|
59
|
-
scrollbar: {
|
|
60
|
-
ch: '│',
|
|
61
|
-
style: { fg: S.border }
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// ── Pipeline mode (single column) ──────────────────────────────
|
|
66
|
-
const pipelineLog = blessed.log({
|
|
67
|
-
top: 0, left: 0,
|
|
68
|
-
width: '100%', height: '100%-10',
|
|
69
|
-
tags: true,
|
|
70
|
-
hidden: true,
|
|
71
|
-
scrollable: true,
|
|
72
|
-
alwaysScroll: true,
|
|
73
|
-
border: { type: 'line' },
|
|
74
|
-
style: { border: { fg: S.border } },
|
|
75
|
-
padding: { left: 1, top: 0, right: 1 },
|
|
76
|
-
scrollbar: {
|
|
77
|
-
ch: '│',
|
|
78
|
-
style: { fg: S.border }
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const pipelineStatus = blessed.box({
|
|
83
|
-
bottom: 5, left: 0,
|
|
84
|
-
width: '100%', height: 4,
|
|
85
|
-
tags: true,
|
|
86
|
-
hidden: true,
|
|
87
|
-
padding: { left: 2, right: 2 }
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Label overlay sitting on the top border of pipelineLog (e.g. "Step 2/4" or "input waiting")
|
|
91
|
-
const pipelineLabel = blessed.box({
|
|
92
|
-
top: 0, left: 4,
|
|
93
|
-
width: 'shrink', height: 1,
|
|
94
|
-
tags: true,
|
|
95
|
-
hidden: true
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// ── Shell bar (toujours visible) ───────────────────────────────
|
|
99
|
-
const sep1 = blessed.line({
|
|
100
|
-
orientation: 'horizontal',
|
|
101
|
-
bottom: 3, left: 0, width: '100%',
|
|
102
|
-
style: { fg: S.border }
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const suggestBox = blessed.box({
|
|
106
|
-
bottom: 4, left: 0,
|
|
107
|
-
width: '100%', height: 5,
|
|
108
|
-
tags: true,
|
|
109
|
-
hidden: true,
|
|
110
|
-
padding: { left: 2, right: 2 },
|
|
111
|
-
style: { bg: 'default' }
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const promptBox = blessed.box({
|
|
115
|
-
bottom: 2, left: 0,
|
|
116
|
-
width: '100%', height: 1,
|
|
117
|
-
padding: { left: 2 }, tags: true
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const sep2 = blessed.line({
|
|
121
|
-
orientation: 'horizontal',
|
|
122
|
-
bottom: 1, left: 0, width: '100%',
|
|
123
|
-
style: { fg: S.border }
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const footerLeftBox = blessed.box({
|
|
127
|
-
bottom: 0, left: 0,
|
|
128
|
-
width: '70%', height: 1,
|
|
129
|
-
padding: { left: 2 },
|
|
130
|
-
tags: true
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const footerRightBox = blessed.box({
|
|
134
|
-
bottom: 0, right: 2,
|
|
135
|
-
width: '30%', height: 1,
|
|
136
|
-
align: 'right',
|
|
137
|
-
tags: true
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const footerCenterBox = blessed.box({
|
|
141
|
-
bottom: 0, left: '30%',
|
|
142
|
-
width: '40%', height: 1,
|
|
143
|
-
align: 'center',
|
|
144
|
-
tags: true
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
screen.append(content);
|
|
148
|
-
screen.append(pipelineLog);
|
|
149
|
-
screen.append(pipelineStatus);
|
|
150
|
-
screen.append(pipelineLabel);
|
|
151
|
-
screen.append(suggestBox);
|
|
152
|
-
screen.append(sep1);
|
|
153
|
-
screen.append(promptBox);
|
|
154
|
-
screen.append(sep2);
|
|
155
|
-
screen.append(footerLeftBox);
|
|
156
|
-
screen.append(footerRightBox);
|
|
157
|
-
screen.append(footerCenterBox);
|
|
158
|
-
|
|
159
|
-
pipelineStatus.hide();
|
|
160
|
-
|
|
161
|
-
// ── Input state ─────────────────────────────────────────────────
|
|
162
|
-
let buffer = '';
|
|
163
|
-
let inputEnabled = true;
|
|
164
|
-
let onSubmit = null;
|
|
165
|
-
let promptMode = null; // { resolve, message }
|
|
166
|
-
let completer = null;
|
|
167
|
-
let suggestions = [];
|
|
168
|
-
let suggestIndex = 0;
|
|
169
|
-
let completeSeq = 0;
|
|
170
|
-
let pipelineMode = false;
|
|
171
|
-
let history = [];
|
|
172
|
-
let historyIndex = -1;
|
|
173
|
-
let draftBuffer = '';
|
|
174
|
-
let hintIndex = 0;
|
|
175
|
-
let footerLeft = '';
|
|
176
|
-
let footerRight = '';
|
|
177
|
-
let footerCenter = '';
|
|
178
|
-
// Tracks the /run two-step submit: first Enter on `/run <pipeline>` opens flag suggestions
|
|
179
|
-
// passively, second Enter submits. Cleared by any keystroke that breaks the dance.
|
|
180
|
-
let runAwaitingSecondEnter = false;
|
|
181
|
-
function stripTags(s) {
|
|
182
|
-
return String(s || '').replace(/\{[^}]+\}/g, '');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function hideSuggestions() {
|
|
186
|
-
suggestions = [];
|
|
187
|
-
suggestIndex = 0;
|
|
188
|
-
suggestBox.hide();
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function renderSuggestions() {
|
|
192
|
-
if (!suggestions.length) {
|
|
193
|
-
suggestBox.hide();
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const maxItems = Math.min(5, suggestions.length);
|
|
198
|
-
const start = Math.min(
|
|
199
|
-
Math.max(0, suggestIndex - maxItems + 1),
|
|
200
|
-
Math.max(0, suggestions.length - maxItems)
|
|
201
|
-
);
|
|
202
|
-
const width = Math.max(40, (screen.width ?? 100) - 6);
|
|
203
|
-
// Semantic styling:
|
|
204
|
-
// active row → accent ›, white bold label, muted description (clearly the one in focus)
|
|
205
|
-
// inactive row → blank marker, muted label, subtle description (recedes)
|
|
206
|
-
// suggestIndex === -1 means "no active selection" (passive listing after a soft Enter on /run).
|
|
207
|
-
const lines = suggestions.slice(start, start + maxItems).map((item, idx) => {
|
|
208
|
-
const active = suggestIndex >= 0 && start + idx === suggestIndex;
|
|
209
|
-
const marker = active ? `{${S.accent}-fg}{bold}›{/}` : ' ';
|
|
210
|
-
const label = active
|
|
211
|
-
? `{${S.text}-fg}{bold}${item.label}{/}`
|
|
212
|
-
: `{${S.muted}-fg}${item.label}{/}`;
|
|
213
|
-
const descColor = active ? S.muted : S.subtle;
|
|
214
|
-
const desc = item.description ? ` {${descColor}-fg}${item.description}{/}` : '';
|
|
215
|
-
const visible = stripTags(`${marker} ${item.label}${item.description ? ` ${item.description}` : ''}`);
|
|
216
|
-
const clippedDesc = visible.length > width ? '' : desc;
|
|
217
|
-
return `${marker} ${label}${clippedDesc}`;
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
suggestBox.setContent(lines.join('\n'));
|
|
221
|
-
suggestBox.show();
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async function refreshSuggestions({ applySingle = false, passive = false } = {}) {
|
|
225
|
-
// Prompt-scoped completer (set via shell.prompt({ completer })) takes precedence,
|
|
226
|
-
// so per-field autocompletes don't leak into the global slash-command completer.
|
|
227
|
-
const activeCompleter = promptMode?.completer || completer;
|
|
228
|
-
if (!activeCompleter) return false;
|
|
229
|
-
|
|
230
|
-
const seq = ++completeSeq;
|
|
231
|
-
const result = await activeCompleter({ buffer, cursor: buffer.length });
|
|
232
|
-
if (seq !== completeSeq) return false;
|
|
233
|
-
|
|
234
|
-
suggestions = Array.isArray(result)
|
|
235
|
-
? result.filter((s) => s && typeof s.value === 'string' && typeof s.label === 'string')
|
|
236
|
-
: [];
|
|
237
|
-
// passive=true: shown as a list with no active row; Enter will submit, not apply.
|
|
238
|
-
suggestIndex = passive ? -1 : 0;
|
|
239
|
-
|
|
240
|
-
if (applySingle && suggestions.length === 1) {
|
|
241
|
-
applySuggestion(suggestions[0]);
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
renderSuggestions();
|
|
246
|
-
screen.render();
|
|
247
|
-
return suggestions.length > 0;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function applySuggestion(item = suggestions[suggestIndex]) {
|
|
251
|
-
if (!item) return false;
|
|
252
|
-
buffer = item.value;
|
|
253
|
-
hideSuggestions();
|
|
254
|
-
runAwaitingSecondEnter = false;
|
|
255
|
-
updatePrompt();
|
|
256
|
-
return true;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function updatePrompt() {
|
|
260
|
-
if (promptMode) {
|
|
261
|
-
const message = String(promptMode.message || '');
|
|
262
|
-
// If the caller passed pre-tagged content, respect it. Otherwise the message
|
|
263
|
-
// belongs to the "awaiting input" state → bold + warning to match the ambient frame.
|
|
264
|
-
const renderedMessage = message.includes('{')
|
|
265
|
-
? message
|
|
266
|
-
: `{${S.warning}-fg}{bold}${message}{/}`;
|
|
267
|
-
const marker = message.includes('Debug action')
|
|
268
|
-
? ''
|
|
269
|
-
: `{${S.warning}-fg}{bold}?{/} `;
|
|
270
|
-
// Ghost-text default: when the buffer is empty and the caller supplied a
|
|
271
|
-
// `default` value, render it in subtle after the cursor so it reads as a
|
|
272
|
-
// suggestion. Pressing Enter on an empty buffer accepts the default.
|
|
273
|
-
const ghost = (!buffer && promptMode.default)
|
|
274
|
-
? `{${S.subtle}-fg}${promptMode.default}{/}`
|
|
275
|
-
: '';
|
|
276
|
-
promptBox.setContent(
|
|
277
|
-
`${marker}${renderedMessage} {${S.muted}-fg}›{/} ${buffer}{${S.accent}-fg}▌{/}${ghost}`
|
|
278
|
-
);
|
|
279
|
-
} else {
|
|
280
|
-
if (buffer) {
|
|
281
|
-
promptBox.setContent(`{${S.muted}-fg}›{/} ${buffer}{${S.accent}-fg}▌{/}`);
|
|
282
|
-
} else {
|
|
283
|
-
const hint = history.length === 0 ? inputHints[0] : inputHints[hintIndex];
|
|
284
|
-
promptBox.setContent(`{${S.muted}-fg}›{/} {${S.accent}-fg}▌{/}{${S.subtle}-fg}${hint}{/}`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
screen.render();
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function renderFooter() {
|
|
291
|
-
footerLeftBox.setContent(footerLeft ? `{${C.dimV}-fg}${String(footerLeft)}{/}` : '');
|
|
292
|
-
footerRightBox.setContent(footerRight ? `{${C.dimV}-fg}${String(footerRight)}{/}` : '');
|
|
293
|
-
footerCenterBox.setContent(footerCenter || '');
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function setFooter(left = '', right = '') {
|
|
297
|
-
footerLeft = left;
|
|
298
|
-
footerRight = right;
|
|
299
|
-
renderFooter();
|
|
300
|
-
screen.render();
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function setFooterCenter(text = '') {
|
|
304
|
-
footerCenter = text;
|
|
305
|
-
renderFooter();
|
|
306
|
-
screen.render();
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
screen.on('resize', () => {
|
|
310
|
-
renderSuggestions();
|
|
311
|
-
screen.render();
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
function resetHistoryNav() {
|
|
315
|
-
historyIndex = -1;
|
|
316
|
-
draftBuffer = '';
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const hintTicker = setInterval(() => {
|
|
320
|
-
if (promptMode || pipelineMode || buffer || history.length === 0) return;
|
|
321
|
-
hintIndex = (hintIndex + 1) % inputHints.length;
|
|
322
|
-
updatePrompt();
|
|
323
|
-
}, 3000);
|
|
324
|
-
|
|
325
|
-
screen.on('keypress', async (ch, key) => {
|
|
326
|
-
if (key.full === 'C-c') {
|
|
327
|
-
log(`{${C.dimV}-fg}See you soon.{/}`);
|
|
328
|
-
screen.render();
|
|
329
|
-
setTimeout(() => { screen.destroy(); process.exit(0); }, 200);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (pipelineMode) {
|
|
333
|
-
if (key.name === 'up') {
|
|
334
|
-
pipelineLog.scroll(-1);
|
|
335
|
-
screen.render();
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
if (key.name === 'down') {
|
|
339
|
-
pipelineLog.scroll(1);
|
|
340
|
-
screen.render();
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
if (key.name === 'pageup') {
|
|
344
|
-
pipelineLog.scroll(-(pipelineLog.height - 2));
|
|
345
|
-
screen.render();
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
if (key.name === 'pagedown') {
|
|
349
|
-
pipelineLog.scroll(pipelineLog.height - 2);
|
|
350
|
-
screen.render();
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
if (key.name === 'home') {
|
|
354
|
-
pipelineLog.setScroll(0);
|
|
355
|
-
screen.render();
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
if (key.name === 'end') {
|
|
359
|
-
pipelineLog.setScrollPerc(100);
|
|
360
|
-
screen.render();
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (!pipelineMode && !promptMode && !suggestions.length) {
|
|
366
|
-
if (buffer === '' && key.name === 'up') {
|
|
367
|
-
content.scroll(-1);
|
|
368
|
-
screen.render();
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
if (buffer === '' && key.name === 'down') {
|
|
372
|
-
content.scroll(1);
|
|
373
|
-
screen.render();
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
if (key.name === 'pageup') {
|
|
377
|
-
content.scroll(-(content.height - 2));
|
|
378
|
-
screen.render();
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
if (key.name === 'pagedown') {
|
|
382
|
-
content.scroll(content.height - 2);
|
|
383
|
-
screen.render();
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
if (key.name === 'home') {
|
|
387
|
-
content.setScroll(0);
|
|
388
|
-
screen.render();
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (key.name === 'end') {
|
|
392
|
-
content.setScrollPerc(100);
|
|
393
|
-
screen.render();
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (!inputEnabled && !promptMode) return;
|
|
399
|
-
|
|
400
|
-
if (promptMode && key.name === 'escape') {
|
|
401
|
-
// In a prompt with autocomplete open, Esc first closes the suggestions instead
|
|
402
|
-
// of cancelling the prompt itself (matches the global-mode behavior).
|
|
403
|
-
if (suggestions.length) {
|
|
404
|
-
hideSuggestions();
|
|
405
|
-
updatePrompt();
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
const { resolve, message, silent } = promptMode;
|
|
409
|
-
promptMode = null;
|
|
410
|
-
buffer = '';
|
|
411
|
-
if (!silent) log(`{${S.subtle}-fg}↩ cancelled{/} {${S.muted}-fg}${message}{/}`);
|
|
412
|
-
updatePrompt();
|
|
413
|
-
resolve('__SINGLETON_ESC__');
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (key.name === 'tab' && (promptMode?.completer || (!promptMode && completer))) {
|
|
418
|
-
if (suggestions.length > 1) {
|
|
419
|
-
// From the passive -1 state, Tab focuses the first item rather than skipping it.
|
|
420
|
-
suggestIndex = suggestIndex < 0 ? 0 : (suggestIndex + 1) % suggestions.length;
|
|
421
|
-
renderSuggestions();
|
|
422
|
-
screen.render();
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
await refreshSuggestions({ applySingle: true });
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (suggestions.length && (key.name === 'down' || key.name === 'up')) {
|
|
430
|
-
const dir = key.name === 'down' ? 1 : -1;
|
|
431
|
-
// From the passive -1 state, the first arrow lands on item 0 (down) or last (up).
|
|
432
|
-
if (suggestIndex < 0) suggestIndex = dir === 1 ? 0 : suggestions.length - 1;
|
|
433
|
-
else suggestIndex = (suggestIndex + dir + suggestions.length) % suggestions.length;
|
|
434
|
-
renderSuggestions();
|
|
435
|
-
screen.render();
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (!promptMode && suggestions.length && key.name === 'escape') {
|
|
440
|
-
hideSuggestions();
|
|
441
|
-
updatePrompt();
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (suggestions.length && (key.name === 'right' || key.name === 'enter' || key.name === 'return')) {
|
|
446
|
-
// Passive listing (no active row) → Enter falls through to the submit handler below.
|
|
447
|
-
if (suggestIndex < 0 && (key.name === 'enter' || key.name === 'return')) {
|
|
448
|
-
// fall through
|
|
449
|
-
} else {
|
|
450
|
-
applySuggestion();
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (!promptMode && !suggestions.length && (key.full === 'C-p' || key.full === 'C-n')) {
|
|
456
|
-
if (history.length === 0) return;
|
|
457
|
-
|
|
458
|
-
if (key.full === 'C-p') {
|
|
459
|
-
if (historyIndex === -1) {
|
|
460
|
-
draftBuffer = buffer;
|
|
461
|
-
historyIndex = history.length - 1;
|
|
462
|
-
} else if (historyIndex > 0) {
|
|
463
|
-
historyIndex -= 1;
|
|
464
|
-
}
|
|
465
|
-
buffer = history[historyIndex] || '';
|
|
466
|
-
} else {
|
|
467
|
-
if (historyIndex === -1) return;
|
|
468
|
-
if (historyIndex < history.length - 1) {
|
|
469
|
-
historyIndex += 1;
|
|
470
|
-
buffer = history[historyIndex] || '';
|
|
471
|
-
} else {
|
|
472
|
-
historyIndex = -1;
|
|
473
|
-
buffer = draftBuffer;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
updatePrompt();
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (key.name === 'enter' || key.name === 'return') {
|
|
482
|
-
const value = buffer.trim();
|
|
483
|
-
|
|
484
|
-
// Two-step submit for /run <pipeline>: first Enter opens flag suggestions passively,
|
|
485
|
-
// second Enter submits. Guarded by runAwaitingSecondEnter so dismissing the suggestions
|
|
486
|
-
// (Esc) and pressing Enter again doesn't re-loop.
|
|
487
|
-
if (
|
|
488
|
-
!promptMode &&
|
|
489
|
-
!runAwaitingSecondEnter &&
|
|
490
|
-
/^\/run\s+\S+\s*$/.test(value) &&
|
|
491
|
-
!value.includes(' --')
|
|
492
|
-
) {
|
|
493
|
-
if (!buffer.endsWith(' ')) buffer += ' ';
|
|
494
|
-
runAwaitingSecondEnter = true;
|
|
495
|
-
updatePrompt();
|
|
496
|
-
await refreshSuggestions({ passive: true });
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
runAwaitingSecondEnter = false;
|
|
500
|
-
|
|
501
|
-
buffer = '';
|
|
502
|
-
hideSuggestions();
|
|
503
|
-
if (promptMode) {
|
|
504
|
-
const { resolve, message, silent, default: promptDefault } = promptMode;
|
|
505
|
-
// Empty submission with a ghost-text default → resolve with the default.
|
|
506
|
-
const finalValue = (value === '' && promptDefault) ? promptDefault : value;
|
|
507
|
-
promptMode = null;
|
|
508
|
-
if (!silent) log(`{${S.warning}-fg}{bold}?{/} {${S.muted}-fg}${message}{/} ${finalValue}`);
|
|
509
|
-
updatePrompt();
|
|
510
|
-
resolve(finalValue);
|
|
511
|
-
} else {
|
|
512
|
-
updatePrompt();
|
|
513
|
-
if (value) {
|
|
514
|
-
if (history[history.length - 1] !== value) history.push(value);
|
|
515
|
-
if (history.length > 200) history = history.slice(-200);
|
|
516
|
-
resetHistoryNav();
|
|
517
|
-
if (onSubmit) onSubmit(value);
|
|
518
|
-
} else {
|
|
519
|
-
resetHistoryNav();
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
} else if (key.name === 'backspace') {
|
|
523
|
-
buffer = buffer.slice(0, -1);
|
|
524
|
-
resetHistoryNav();
|
|
525
|
-
runAwaitingSecondEnter = false;
|
|
526
|
-
// In a prompt with a completer, keystrokes re-filter the suggestions instead
|
|
527
|
-
// of dismissing them. In all other modes, typing hides the suggest panel.
|
|
528
|
-
if (promptMode?.completer) {
|
|
529
|
-
updatePrompt();
|
|
530
|
-
await refreshSuggestions({ passive: true });
|
|
531
|
-
} else {
|
|
532
|
-
hideSuggestions();
|
|
533
|
-
updatePrompt();
|
|
534
|
-
}
|
|
535
|
-
} else if (ch && !key.ctrl && !key.meta) {
|
|
536
|
-
buffer += ch;
|
|
537
|
-
resetHistoryNav();
|
|
538
|
-
runAwaitingSecondEnter = false;
|
|
539
|
-
if (promptMode?.completer) {
|
|
540
|
-
updatePrompt();
|
|
541
|
-
await refreshSuggestions({ passive: true });
|
|
542
|
-
} else {
|
|
543
|
-
hideSuggestions();
|
|
544
|
-
updatePrompt();
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
function log(text) {
|
|
550
|
-
content.log(text);
|
|
551
|
-
screen.render();
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
updatePrompt();
|
|
555
|
-
|
|
556
|
-
// Mode → border color mapping. Drives the "ambient state" frame around the log panel.
|
|
557
|
-
// null/'idle' → S.border (faint, structural)
|
|
558
|
-
// 'running' → S.keyword (blue, run in progress)
|
|
559
|
-
// 'awaiting' → S.warning (orange, waiting for human input)
|
|
560
|
-
// 'error' → S.error (red, last run failed)
|
|
561
|
-
// 'debug' → S.warning (orange, debug mode active)
|
|
562
|
-
// Label overlay shown on the top border of the pipeline log frame.
|
|
563
|
-
// Timeline writes the step indicator (step X/N). The executor can override
|
|
564
|
-
// with "input waiting" while a prompt is pending — overrides are sticky
|
|
565
|
-
// until cleared, so the timeline's spinner-tick re-renders don't clobber them.
|
|
566
|
-
let pipelineLabelOverride = null;
|
|
567
|
-
function writePipelineLabel(text) {
|
|
568
|
-
if (!text) {
|
|
569
|
-
pipelineLabel.setContent('');
|
|
570
|
-
pipelineLabel.hide();
|
|
571
|
-
} else {
|
|
572
|
-
pipelineLabel.setContent(` ${text} `);
|
|
573
|
-
pipelineLabel.show();
|
|
574
|
-
}
|
|
575
|
-
screen.render();
|
|
576
|
-
}
|
|
577
|
-
function applyPipelineLabel(text) {
|
|
578
|
-
if (pipelineLabelOverride !== null) return;
|
|
579
|
-
writePipelineLabel(text);
|
|
580
|
-
}
|
|
581
|
-
function setPipelineLabel(text) {
|
|
582
|
-
pipelineLabelOverride = text;
|
|
583
|
-
writePipelineLabel(`{${S.warning}-fg}{bold}${text}{/}`);
|
|
584
|
-
}
|
|
585
|
-
function clearPipelineLabel() {
|
|
586
|
-
pipelineLabelOverride = null;
|
|
587
|
-
writePipelineLabel('');
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Mode → border color. Two-layer state:
|
|
591
|
-
// baseMode — ambient mode set by the executor ('running' during a step, etc.)
|
|
592
|
-
// currentMode — what is actually painted; prompts override to 'awaiting' and restore baseMode on resolve.
|
|
593
|
-
// Removed 'debug' as its own mode: a debug pause IS an awaiting state, a running debug step IS running.
|
|
594
|
-
let baseMode = null;
|
|
595
|
-
function applyMode(mode) {
|
|
596
|
-
const map = {
|
|
597
|
-
running: S.keyword,
|
|
598
|
-
awaiting: S.warning,
|
|
599
|
-
error: S.error,
|
|
600
|
-
};
|
|
601
|
-
const color = map[mode] || S.border;
|
|
602
|
-
content.style.border.fg = color;
|
|
603
|
-
pipelineLog.style.border.fg = color;
|
|
604
|
-
sep1.style.fg = color;
|
|
605
|
-
sep2.style.fg = color;
|
|
606
|
-
screen.render();
|
|
607
|
-
}
|
|
608
|
-
function setMode(mode) {
|
|
609
|
-
baseMode = mode;
|
|
610
|
-
applyMode(mode);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
return {
|
|
614
|
-
log,
|
|
615
|
-
logMuted(text) { log(`{${S.muted}-fg}${text}{/}`); },
|
|
616
|
-
logAccent(text) { log(`{${S.accent}-fg}${text}{/}`); },
|
|
617
|
-
setFooter,
|
|
618
|
-
setFooterCenter,
|
|
619
|
-
setMode,
|
|
620
|
-
setPipelineLabel,
|
|
621
|
-
clearPipelineLabel,
|
|
622
|
-
|
|
623
|
-
clear() { content.setContent(''); screen.render(); },
|
|
624
|
-
setContent(text) { content.setContent(text); screen.render(); },
|
|
625
|
-
onCommand(fn) { onSubmit = fn; },
|
|
626
|
-
setCompleter(fn) { completer = fn; },
|
|
627
|
-
|
|
628
|
-
// Shimmer animation overlay (e.g. for "Welcome back")
|
|
629
|
-
createShimmer(text, top, left) {
|
|
630
|
-
const box = blessed.box({
|
|
631
|
-
top, left,
|
|
632
|
-
width: text.length,
|
|
633
|
-
height: 1,
|
|
634
|
-
tags: true
|
|
635
|
-
});
|
|
636
|
-
screen.append(box);
|
|
637
|
-
|
|
638
|
-
let peak = -4;
|
|
639
|
-
function render() {
|
|
640
|
-
const content = text.split('').map((ch, i) => {
|
|
641
|
-
const dist = Math.abs(i - peak);
|
|
642
|
-
let color;
|
|
643
|
-
if (dist === 0) color = '#FFFFFF';
|
|
644
|
-
else if (dist === 1) color = '#EDD9FF';
|
|
645
|
-
else if (dist === 2) color = '#D4B0FE';
|
|
646
|
-
else color = C.violet;
|
|
647
|
-
return `{${color}-fg}{bold}${ch}{/}`;
|
|
648
|
-
}).join('');
|
|
649
|
-
box.setContent(content);
|
|
650
|
-
screen.render();
|
|
651
|
-
peak++;
|
|
652
|
-
if (peak > text.length + 4) peak = -4;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
render();
|
|
656
|
-
const iv = setInterval(render, 80);
|
|
657
|
-
return () => { clearInterval(iv); screen.remove(box); screen.render(); };
|
|
658
|
-
},
|
|
659
|
-
disableInput() { inputEnabled = false; hideSuggestions(); resetHistoryNav(); screen.render(); },
|
|
660
|
-
enableInput() { inputEnabled = true; buffer = ''; hideSuggestions(); resetHistoryNav(); updatePrompt(); },
|
|
661
|
-
|
|
662
|
-
prompt(message, { silent = false, completer: promptCompleter = null, default: promptDefault = '' } = {}) {
|
|
663
|
-
return new Promise((resolve) => {
|
|
664
|
-
// Override ambient mode to 'awaiting' (orange) for the duration of the prompt,
|
|
665
|
-
// and restore the baseMode (e.g. 'running') once the user has answered.
|
|
666
|
-
const shouldOverride = baseMode === 'running';
|
|
667
|
-
if (shouldOverride) applyMode('awaiting');
|
|
668
|
-
promptMode = {
|
|
669
|
-
message,
|
|
670
|
-
silent,
|
|
671
|
-
completer: promptCompleter,
|
|
672
|
-
default: promptDefault,
|
|
673
|
-
resolve: (value) => {
|
|
674
|
-
if (shouldOverride) applyMode(baseMode);
|
|
675
|
-
resolve(value);
|
|
676
|
-
},
|
|
677
|
-
};
|
|
678
|
-
buffer = '';
|
|
679
|
-
hideSuggestions();
|
|
680
|
-
resetHistoryNav();
|
|
681
|
-
updatePrompt();
|
|
682
|
-
// With a prompt-scoped completer, show the full suggestion list immediately
|
|
683
|
-
// so the user sees what's available without having to press Tab first.
|
|
684
|
-
// Use passive mode so Enter submits the typed value rather than applying
|
|
685
|
-
// the first row — Tab/arrows are the explicit "pick" path.
|
|
686
|
-
if (promptCompleter) {
|
|
687
|
-
refreshSuggestions({ passive: true }).catch(() => {});
|
|
688
|
-
}
|
|
689
|
-
});
|
|
690
|
-
},
|
|
691
|
-
|
|
692
|
-
// Mirror sends every timeline.log call into the main `content` widget too, so the full
|
|
693
|
-
// run history survives exitPipelineMode (pipelineLog gets hidden, but content keeps it).
|
|
694
|
-
pipelineWidgets: {
|
|
695
|
-
screen,
|
|
696
|
-
logPanel: pipelineLog,
|
|
697
|
-
statusBox: pipelineStatus,
|
|
698
|
-
setLabel: applyPipelineLabel,
|
|
699
|
-
mirror: (text) => content.log(text),
|
|
700
|
-
},
|
|
701
|
-
|
|
702
|
-
enterPipelineMode() {
|
|
703
|
-
pipelineMode = true;
|
|
704
|
-
content.hide();
|
|
705
|
-
pipelineLog.setContent('');
|
|
706
|
-
pipelineStatus.setContent('');
|
|
707
|
-
pipelineLog.show();
|
|
708
|
-
pipelineStatus.show();
|
|
709
|
-
pipelineLabel.show();
|
|
710
|
-
promptBox.setContent('');
|
|
711
|
-
screen.render();
|
|
712
|
-
},
|
|
713
|
-
|
|
714
|
-
exitPipelineMode() {
|
|
715
|
-
pipelineMode = false;
|
|
716
|
-
pipelineLabelOverride = null;
|
|
717
|
-
pipelineLog.hide();
|
|
718
|
-
pipelineStatus.hide();
|
|
719
|
-
pipelineLabel.hide();
|
|
720
|
-
pipelineLabel.setContent('');
|
|
721
|
-
content.show();
|
|
722
|
-
updatePrompt();
|
|
723
|
-
screen.render();
|
|
724
|
-
},
|
|
725
|
-
|
|
726
|
-
screen,
|
|
727
|
-
destroy() {
|
|
728
|
-
clearInterval(hintTicker);
|
|
729
|
-
screen.destroy();
|
|
730
|
-
}
|
|
731
|
-
};
|
|
732
|
-
}
|