@cspeach/cli 1.1.17 → 1.1.19
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 +3 -4
- package/dist/agent/loop.js +143 -25
- package/dist/agent/providers/local-provider.js +4 -1
- package/dist/agent/repair-partial.js +76 -19
- package/dist/agent/tool-dispatch.js +80 -22
- package/dist/agent/turn-error-ux.js +77 -26
- package/dist/agent/turn-stream.js +25 -3
- package/dist/approvals/adt-type.js +27 -0
- package/dist/approvals/advisory-prompt.js +48 -0
- package/dist/approvals/advisory-render.js +25 -11
- package/dist/approvals/approval-prompt.js +74 -24
- package/dist/approvals/jwt.js +2 -1
- package/dist/approvals/render.js +64 -36
- package/dist/approvals/risk-floor.js +53 -2
- package/dist/auth/api-key.js +30 -16
- package/dist/auth/keychain.js +0 -0
- package/dist/auth/me.js +25 -7
- package/dist/cli.js +25 -6
- package/dist/commands/compact.js +12 -9
- package/dist/commands/config-set.js +6 -2
- package/dist/commands/config-show.js +7 -5
- package/dist/commands/cost.js +41 -34
- package/dist/commands/help.js +88 -50
- package/dist/commands/login.js +28 -19
- package/dist/commands/logout.js +7 -3
- package/dist/commands/plan-chain.js +71 -77
- package/dist/commands/plan-continue.js +1 -0
- package/dist/commands/plan-resume.js +20 -12
- package/dist/commands/whoami.js +23 -9
- package/dist/config/loader.js +15 -3
- package/dist/cost/pricing.js +6 -5
- package/dist/doctor/checks/forge-rules.js +13 -3
- package/dist/doctor/checks/keychain.js +6 -4
- package/dist/doctor/run.js +101 -20
- package/dist/index.js +5 -2
- package/dist/lib/graceful-exit.js +56 -0
- package/dist/lib/piped-prompt.js +65 -0
- package/dist/lock-contention.js +2 -2
- package/dist/one-shot.js +135 -40
- package/dist/projects/answer-blockers.js +6 -1
- package/dist/projects/handover-md.js +15 -14
- package/dist/projects/plan-run.js +97 -54
- package/dist/projects/promote-command.js +11 -2
- package/dist/projects/save-command.js +3 -2
- package/dist/projects/status.js +2 -1
- package/dist/projects/yes-no.js +12 -0
- package/dist/renderer/abap-inline.js +18 -14
- package/dist/renderer/answer-trim.js +69 -0
- package/dist/renderer/banners.js +8 -8
- package/dist/renderer/brand-settled.js +19 -0
- package/dist/renderer/change-card.js +156 -0
- package/dist/renderer/checklist-format.js +130 -0
- package/dist/renderer/color-mode.js +45 -0
- package/dist/renderer/error-detail.js +104 -0
- package/dist/renderer/fence-state.js +46 -0
- package/dist/renderer/footer-line.js +96 -0
- package/dist/renderer/glyphs.js +25 -0
- package/dist/renderer/goodbye.js +38 -0
- package/dist/renderer/legacy-palette.js +41 -0
- package/dist/renderer/look.js +30 -0
- package/dist/renderer/markdown.js +299 -92
- package/dist/renderer/notice-log.js +72 -0
- package/dist/renderer/notice-shape.js +48 -0
- package/dist/renderer/notices.js +40 -8
- package/dist/renderer/panel-rows.js +16 -0
- package/dist/renderer/pipeline.js +66 -0
- package/dist/renderer/progress-chatter.js +23 -17
- package/dist/renderer/rendering-mode.js +34 -0
- package/dist/renderer/routing-line.js +14 -0
- package/dist/renderer/sanitize.js +12 -0
- package/dist/renderer/severity.js +2 -7
- package/dist/renderer/startup-lines.js +152 -0
- package/dist/renderer/status-footer.js +52 -34
- package/dist/renderer/steering-echo.js +41 -0
- package/dist/renderer/syntax.js +31 -12
- package/dist/renderer/tables.js +5 -1
- package/dist/renderer/theme.js +44 -0
- package/dist/renderer/thinking-heartbeat.js +33 -2
- package/dist/renderer/todo-block.js +10 -49
- package/dist/renderer/tool-labels.js +238 -0
- package/dist/renderer/tool-widget.js +245 -51
- package/dist/renderer/trace.js +42 -0
- package/dist/renderer/transcript-flow.js +132 -0
- package/dist/renderer/tty.js +34 -1
- package/dist/renderer/ui-width.js +42 -0
- package/dist/renderer/verify-chain.js +4 -2
- package/dist/renderer/widget-fallback.js +58 -65
- package/dist/repl/bracketed-paste.js +7 -1
- package/dist/repl/builtin-commands.js +19 -8
- package/dist/repl/credential-handover-gate.js +28 -0
- package/dist/repl/early-line-buffer.js +5 -2
- package/dist/repl/file-picker.js +54 -12
- package/dist/repl/ink-stdin-guard.js +66 -0
- package/dist/repl/inquirer-guard.js +59 -16
- package/dist/repl/inquirer-theme.js +27 -33
- package/dist/repl/paste-marker.js +19 -0
- package/dist/repl/plan-turn-end.js +20 -0
- package/dist/repl/post-turn-status.js +36 -11
- package/dist/repl/reroute-turn.js +21 -0
- package/dist/repl/reset-tty-stdin.js +44 -0
- package/dist/repl/restore-guard.js +22 -0
- package/dist/repl/resume-standalone.js +63 -0
- package/dist/repl/rule8-detector.js +11 -3
- package/dist/repl/safety-confirm.js +161 -93
- package/dist/repl/session-spend-line.js +8 -4
- package/dist/repl/themed-prompts.js +19 -0
- package/dist/repl/ui-look-command.js +75 -0
- package/dist/repl/update-method-preview-hook.js +48 -5
- package/dist/repl.js +795 -332
- package/dist/rewind/cli.js +3 -2
- package/dist/rewind/format.js +14 -9
- package/dist/rewind/restore.js +11 -0
- package/dist/router/classifier.js +47 -4
- package/dist/router/intent-extractor.js +24 -8
- package/dist/router/piped-routing.js +23 -0
- package/dist/router/resume-routing.js +16 -0
- package/dist/router/routing-failure.js +57 -0
- package/dist/sap/first-run-choice.js +1 -1
- package/dist/sap/onboarding.js +3 -2
- package/dist/sap/standalone-onboarding.js +1 -1
- package/dist/sap/system-info.js +4 -2
- package/dist/sap/unreachable.js +28 -0
- package/dist/sap-errors/clean-error-text.js +182 -0
- package/dist/session/interrupt-reason.js +39 -0
- package/dist/session/recap.js +36 -24
- package/dist/session/resume.js +104 -36
- package/dist/session/store.js +69 -27
- package/dist/session/user-prompt.js +21 -0
- package/dist/skill-catalog.js +7 -0
- package/dist/skills/bundled-skills.js +76 -69
- package/dist/skills/signing-public-key.js +1 -1
- package/dist/standards/standards-init.js +1 -1
- package/dist/test-helpers/answer-any.js +46 -0
- package/dist/tools/approval.js +27 -11
- package/dist/tools/ask-question.js +47 -5
- package/dist/tools/filesystem/file-write.js +19 -2
- package/dist/tools/fiori/fe-extend.js +16 -1
- package/dist/tools/fiori/fe-scaffold.js +105 -3
- package/dist/tools/fiori/html-escapes.js +28 -0
- package/dist/tools/fiori/metadata/audit.js +605 -0
- package/dist/tools/fiori/metadata/references.js +104 -0
- package/dist/tools/fiori/metadata/types.js +8 -0
- package/dist/tools/fiori/preview/app-guard.js +215 -0
- package/dist/tools/fiori/preview/env.js +193 -0
- package/dist/tools/fiori/preview/readiness.js +127 -0
- package/dist/tools/fiori/preview/registry.js +412 -0
- package/dist/tools/fiori/preview/start.js +469 -0
- package/dist/tools/fiori/samples/loader.js +20 -5
- package/dist/tools/fiori/scaffold.js +18 -6
- package/dist/tools/fiori/smoke/app-driver-page.js +372 -0
- package/dist/tools/fiori/smoke/app-driver.js +100 -0
- package/dist/tools/fiori/smoke/assertions.js +161 -24
- package/dist/tools/fiori/smoke/audit-columns.js +21 -0
- package/dist/tools/fiori/smoke/batch.js +273 -0
- package/dist/tools/fiori/smoke/draft-safety.js +241 -0
- package/dist/tools/fiori/smoke/draft-smoke.js +473 -0
- package/dist/tools/fiori/smoke/driver.js +171 -4
- package/dist/tools/fiori/smoke/evidence.js +35 -0
- package/dist/tools/fiori/smoke/labels-codes.js +207 -0
- package/dist/tools/fiori/smoke/nav-error.js +26 -0
- package/dist/tools/fiori/smoke/run-smoke.js +155 -12
- package/dist/tools/fiori/tools.js +651 -7
- package/dist/tools/fiori/ui5-version.js +16 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/local-build.js +6 -0
- package/dist/tools/sap-read.js +16 -1
- package/dist/tools/sap-write.js +122 -10
- package/dist/tools/shell/shell_exec.js +30 -12
- package/dist/tools/syntax-state.js +67 -0
- package/dist/tools/todo.js +23 -18
- package/dist/tools/transport-fit.js +462 -0
- package/dist/tools/transport-resolution.js +3 -1
- package/dist/tools/transport.js +33 -7
- package/dist/tools/verify.js +184 -8
- package/dist/ui/app.js +284 -146
- package/dist/ui/approval-modal.js +41 -15
- package/dist/ui/ask-answer-text.js +17 -0
- package/dist/ui/body.js +25 -6
- package/dist/ui/card-slot.js +98 -0
- package/dist/ui/checklist.js +9 -0
- package/dist/ui/coaching-picker-classic.js +8 -5
- package/dist/ui/command-palette.js +6 -4
- package/dist/ui/confirm-request.js +18 -0
- package/dist/ui/context-grid.js +2 -1
- package/dist/ui/file-palette.js +6 -4
- package/dist/ui/files-card-emitter.js +19 -0
- package/dist/ui/footer-line.js +16 -0
- package/dist/ui/footer.js +105 -104
- package/dist/ui/input-wrap.js +122 -0
- package/dist/ui/interrupt.js +103 -0
- package/dist/ui/key-burst.js +117 -0
- package/dist/ui/line-resolution.js +4 -3
- package/dist/ui/live-area.js +71 -0
- package/dist/ui/login-banner.js +54 -43
- package/dist/ui/rewind-panel.js +76 -24
- package/dist/ui/role-props.js +47 -0
- package/dist/ui/sap-state-store.js +1 -0
- package/dist/ui/session-timeline.js +49 -3
- package/dist/ui/skill-picker.js +5 -47
- package/dist/ui/text-input.js +128 -24
- package/dist/ui/todo-emitter.js +19 -0
- package/dist/ui/turn-status-emitter.js +28 -2
- package/dist/ui/turn-status.js +16 -19
- package/dist/ui/use-card-request.js +44 -0
- package/dist/ui/widgets/ask-card.js +521 -0
- package/dist/ui/widgets/bar-chart.js +11 -5
- package/dist/ui/widgets/coaching-picker.js +50 -55
- package/dist/ui/widgets/confirm-card.js +193 -0
- package/dist/ui/widgets/dep-graph.js +3 -2
- package/dist/ui/widgets/diff-viewer.js +6 -5
- package/dist/ui/widgets/files-card.js +97 -0
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/reclaim-raw-stdin.js +24 -0
- package/dist/ui/widgets/shortcuts-sheet.js +25 -0
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/upgrade-check.js +39 -8
- package/package.json +11 -2
- package/test-harness/fe-draft/app.ts +188 -0
- package/test-harness/fe-draft/fe-draft.e2e.test.ts +461 -0
- package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +51 -0
- package/test-harness/fe-draft/routes.ts +224 -0
- package/test-harness/fe-draft/routes.unit.test.ts +45 -0
- package/test-harness/fe-draft/server.ts +225 -0
- package/test-harness/fe-draft/variants.ts +196 -0
- package/vitest.smoke-e2e.config.ts +20 -0
|
@@ -1,78 +1,71 @@
|
|
|
1
|
+
// src/renderer/widget-fallback.ts
|
|
2
|
+
//
|
|
3
|
+
// Piece 2 §9/§5: the off-Ink question widget is a plain card in the confirm-
|
|
4
|
+
// card frame — QUESTION chip on the top edge, the question in bold, dim
|
|
5
|
+
// context, numbered options, and how to answer on the bottom edge. Every
|
|
6
|
+
// line is exactly `width` cells (uiWidth() by default); text is sanitised
|
|
7
|
+
// and wrapped BEFORE colouring so no cut can land inside an escape code and
|
|
8
|
+
// model text can never drive the terminal.
|
|
9
|
+
//
|
|
10
|
+
// The frame geometry mirrors CardFrame/edgeSegments (ui/widgets/confirm-card.tsx)
|
|
11
|
+
// as plain strings: that module is an Ink component, and this path runs off
|
|
12
|
+
// Ink (piped / CI), so it must not pull Ink and React in.
|
|
1
13
|
import chalk from 'chalk';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return s + ' '.repeat(padding);
|
|
11
|
-
}
|
|
14
|
+
import stringWidth from 'string-width';
|
|
15
|
+
import { paint, chip } from './theme.js';
|
|
16
|
+
import { getBoxChars } from '../ui/ascii-chars.js';
|
|
17
|
+
import { uiWidth, truncateToWidth } from './ui-width.js';
|
|
18
|
+
import { ANSI_RE } from './tool-labels.js';
|
|
19
|
+
import { sanitizeControlChars } from './sanitize.js';
|
|
20
|
+
const vis = (s) => stringWidth(s.replace(ANSI_RE, ''));
|
|
21
|
+
/** Word-wrap plain text to `width` cells; a single over-long word is truncated. */
|
|
12
22
|
function wrap(text, width) {
|
|
13
|
-
|
|
14
|
-
return [text];
|
|
15
|
-
const words = text.split(/\s+/);
|
|
23
|
+
const words = sanitizeControlChars(text).replace(/\t/g, ' ').split(/\s+/).filter((w) => w.length > 0);
|
|
16
24
|
const lines = [];
|
|
17
|
-
let
|
|
18
|
-
for (const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
else if (
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
+
let cur = '';
|
|
26
|
+
for (const raw of words) {
|
|
27
|
+
const word = stringWidth(raw) > width ? truncateToWidth(raw, width) : raw;
|
|
28
|
+
if (cur === '')
|
|
29
|
+
cur = word;
|
|
30
|
+
else if (stringWidth(cur) + 1 + stringWidth(word) <= width)
|
|
31
|
+
cur += ' ' + word;
|
|
25
32
|
else {
|
|
26
|
-
lines.push(
|
|
27
|
-
|
|
33
|
+
lines.push(cur);
|
|
34
|
+
cur = word;
|
|
28
35
|
}
|
|
29
36
|
}
|
|
30
|
-
if (
|
|
31
|
-
lines.push(
|
|
32
|
-
return lines;
|
|
37
|
+
if (cur !== '')
|
|
38
|
+
lines.push(cur);
|
|
39
|
+
return lines.length > 0 ? lines : [''];
|
|
33
40
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
41
|
+
export function renderQuestionCard(widget, width = uiWidth(), box = getBoxChars()) {
|
|
42
|
+
const inner = Math.max(10, width - 4); // "│ " + content + " │"
|
|
43
|
+
const row = (plain, style = (s) => s) => paint('frame', box.v) + ' ' + style(plain) + ' '.repeat(Math.max(0, inner - stringWidth(plain))) + ' ' + paint('frame', box.v);
|
|
44
|
+
// Top edge: ╭─<chip> <id> ───╮ (2 + chip + 1 + id + 1 + fill + 1 = width)
|
|
45
|
+
const tag = chip('QUESTION');
|
|
46
|
+
const idText = truncateToWidth(sanitizeControlChars(widget.id), Math.max(1, width - vis(tag) - 6));
|
|
47
|
+
const topFill = Math.max(0, width - (2 + vis(tag) + 1 + stringWidth(idText) + 1) - 1);
|
|
48
|
+
const top = paint('frame', box.tl + box.h) + tag + ' ' + chalk.bold(idText) + ' ' + paint('frame', box.h.repeat(topFill) + box.tr);
|
|
49
|
+
// Bottom edge: ╰───── <note> ─╯ (1 + fill + 1 + note + 1 + 1 + 1 = width)
|
|
50
|
+
const noteText = widget.kind === 'choice' ? 'reply with a number'
|
|
51
|
+
: widget.kind === 'multi' ? 'reply with numbers, comma-separated'
|
|
52
|
+
: 'reply in your next message';
|
|
53
|
+
const note = truncateToWidth(noteText, Math.max(1, width - 8));
|
|
54
|
+
const bottomFill = Math.max(0, width - stringWidth(note) - 5);
|
|
55
|
+
const bottom = paint('frame', box.bl + box.h.repeat(bottomFill)) + ' ' + paint('dim', note) + ' ' + paint('frame', box.h + box.br);
|
|
48
56
|
const lines = [top];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Optional context
|
|
55
|
-
if (widget.context) {
|
|
56
|
-
lines.push(border('┃ ') + padLine('') + border(' ┃'));
|
|
57
|
-
for (const line of wrap(`Context: ${widget.context}`, CONTENT_WIDTH)) {
|
|
58
|
-
lines.push(border('┃ ') + padLine(dim(line)) + border(' ┃'));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// Choice list for choice/multi kinds
|
|
57
|
+
for (const l of wrap(widget.question, inner))
|
|
58
|
+
lines.push(row(l, (s) => chalk.bold(s)));
|
|
59
|
+
if (widget.context)
|
|
60
|
+
for (const l of wrap(widget.context, inner))
|
|
61
|
+
lines.push(row(l, (s) => paint('dim', s)));
|
|
62
62
|
if (widget.kind === 'choice' || widget.kind === 'multi') {
|
|
63
|
-
lines.push(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const choice = choices[i];
|
|
69
|
-
const line = `[${i + 1}] ${choice.label}`;
|
|
70
|
-
for (const wrapped of wrap(line, CONTENT_WIDTH)) {
|
|
71
|
-
lines.push(border('┃ ') + padLine(wrapped) + border(' ┃'));
|
|
72
|
-
}
|
|
73
|
-
}
|
|
63
|
+
lines.push(row(''));
|
|
64
|
+
(widget.choices ?? []).forEach((c, i) => {
|
|
65
|
+
for (const l of wrap(`${i + 1}. ${c.label}`, inner))
|
|
66
|
+
lines.push(row(l));
|
|
67
|
+
});
|
|
74
68
|
}
|
|
75
|
-
lines.push(border('┃ ') + padLine('') + border(' ┃'));
|
|
76
69
|
lines.push(bottom);
|
|
77
70
|
return '\n' + lines.join('\n') + '\n';
|
|
78
71
|
}
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
* classic REPL path (non-Ink). Ink mode uses its own input component.
|
|
40
40
|
*/
|
|
41
41
|
import { Transform } from 'node:stream';
|
|
42
|
+
import { markPastedChunk } from './paste-marker.js';
|
|
42
43
|
/** DEC Private Mode 2004 — enable bracketed paste. */
|
|
43
44
|
export const BRACKETED_PASTE_ENABLE = '\x1b[?2004h';
|
|
44
45
|
/** DEC Private Mode 2004 — disable bracketed paste. */
|
|
@@ -164,6 +165,8 @@ export class BracketedPasteDecoder extends Transform {
|
|
|
164
165
|
// End of stream. If we were still inside a paste (unterminated),
|
|
165
166
|
// flush the buffered content so it's not lost.
|
|
166
167
|
if (this.inPaste && this.pasteBuf.length > 0) {
|
|
168
|
+
if (this.emitVerbatim)
|
|
169
|
+
markPastedChunk(this.pasteBuf.toString('utf8'));
|
|
167
170
|
this.push(this.emitVerbatim ? this.pasteBuf : this.collapseNewlines(this.pasteBuf));
|
|
168
171
|
this.pasteBuf = Buffer.alloc(0);
|
|
169
172
|
this.inPaste = false;
|
|
@@ -196,8 +199,11 @@ export class BracketedPasteDecoder extends Transform {
|
|
|
196
199
|
// `\r` that arrived as its own stdin chunk INSIDE the span was
|
|
197
200
|
// accumulated here instead of reaching Ink's parseKeypress,
|
|
198
201
|
// which would have flagged it key.return and submitted.
|
|
199
|
-
if (this.pasteBuf.length > 0)
|
|
202
|
+
if (this.pasteBuf.length > 0) {
|
|
203
|
+
// Piece 2 — tell App this chunk was pasted, not typed (the `?` trigger).
|
|
204
|
+
markPastedChunk(this.pasteBuf.toString('utf8'));
|
|
200
205
|
this.push(this.pasteBuf);
|
|
206
|
+
}
|
|
201
207
|
}
|
|
202
208
|
else {
|
|
203
209
|
this.push(this.collapseNewlines(this.pasteBuf));
|
|
@@ -4,13 +4,22 @@
|
|
|
4
4
|
* Extracted from slash-picker.ts (2026-06-11) so the router layer
|
|
5
5
|
* (src/router/classifier.ts) can distinguish "user typed a builtin with
|
|
6
6
|
* arguments" (e.g. `/help me`) from "user typed an unknown skill"
|
|
7
|
-
* WITHOUT importing the inquirer-backed picker module. Pure data
|
|
8
|
-
*
|
|
7
|
+
* WITHOUT importing the inquirer-backed picker module. Pure data plus one
|
|
8
|
+
* read of the process-wide cost display mode (cost/credits.ts); no
|
|
9
|
+
* inquirer, so the router can import it.
|
|
9
10
|
*/
|
|
11
|
+
import { creditsActive } from '../cost/credits.js';
|
|
12
|
+
export const COST_DESCRIPTION_USD = 'Show this session\'s spend so far ($ and tokens)';
|
|
13
|
+
export const COST_DESCRIPTION_CREDITS = 'Show the credits this session has used so far';
|
|
14
|
+
/** Piece 2 §10: managed customers see credits, never "$" (cost/display-mode.ts). */
|
|
15
|
+
export function costCommandDescription() {
|
|
16
|
+
return creditsActive() ? COST_DESCRIPTION_CREDITS : COST_DESCRIPTION_USD;
|
|
17
|
+
}
|
|
10
18
|
export const BUILTIN_ENTRIES = [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
{ name: '/
|
|
19
|
+
// Piece 2: /cancel is not listed — it exists only in the classic loop
|
|
20
|
+
// (repl.tsx), and Esc / Ctrl+C already cancel under Ink.
|
|
21
|
+
{ name: '/cost', get description() { return costCommandDescription(); } },
|
|
22
|
+
{ name: '/compact', description: 'Shrink a long session and keep the facts (later turns cost far less)' },
|
|
14
23
|
{ name: '/export', description: 'Export the signable session audit record — /export audit [filename]' },
|
|
15
24
|
{ name: '/context', description: 'Show what fills the model context window — a composition grid of bars' },
|
|
16
25
|
{ name: '/exit', description: 'Exit CSPeach' },
|
|
@@ -22,19 +31,21 @@ export const BUILTIN_ENTRIES = [
|
|
|
22
31
|
{ name: '/recap', description: 'Show the session recap (last session · open tasks · plan progress)' },
|
|
23
32
|
{ name: '/reset', description: 'Reset routing (alias)' },
|
|
24
33
|
{ name: '/rewind', description: 'Undo a write from this session — restore an object to a pre-write snapshot (Esc Esc)' },
|
|
25
|
-
{ name: '/ui', description: 'View or change rendering mode (auto / ink /
|
|
34
|
+
{ name: '/ui', description: 'View or change rendering mode (auto / ink), or the look: /ui look panels|legacy' },
|
|
26
35
|
{ name: '/reroute', description: 'Re-dispatch the previous prompt to a different skill' },
|
|
27
36
|
{ name: '/transport', description: 'View or set the active transport for this session' },
|
|
28
37
|
{ name: '/safety-mode', description: 'Toggle structured Forge Rule confirmation cards (on / off / status)' },
|
|
29
38
|
];
|
|
30
39
|
/**
|
|
31
40
|
* Builtin command heads for routing checks. Superset of BUILTIN_ENTRIES:
|
|
32
|
-
* `/files` is a real REPL builtin (repl.tsx handles it in both paths)
|
|
33
|
-
* is
|
|
41
|
+
* `/files` is a real REPL builtin (repl.tsx handles it in both paths) and
|
|
42
|
+
* `/cancel` is still handled by the classic loop, but neither is listed in the
|
|
43
|
+
* picker catalogue, so both are added here.
|
|
34
44
|
*/
|
|
35
45
|
const BUILTIN_HEADS = new Set([
|
|
36
46
|
...BUILTIN_ENTRIES.map((e) => e.name),
|
|
37
47
|
'/files',
|
|
48
|
+
'/cancel',
|
|
38
49
|
]);
|
|
39
50
|
/** True when `head` (e.g. '/help', as typed, any case) is a builtin command. */
|
|
40
51
|
export function isBuiltinCommandHead(head) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential handover gate (L1-6 R1-6, owner decision 2026-09-21).
|
|
3
|
+
*
|
|
4
|
+
* A tool that hands the stored SAP login to a local child process (today only
|
|
5
|
+
* `fiori_preview` start) ALWAYS raises a human prompt the first time per
|
|
6
|
+
* session per alias — even as the first mutating call of a turn, where the
|
|
7
|
+
* Rule 8 batch gate stays silent, and whatever auto_approve says. Later calls
|
|
8
|
+
* for the same alias in the same session do not re-ask.
|
|
9
|
+
*
|
|
10
|
+
* The gate itself lives in agent/tool-dispatch.ts; a tool opts in through
|
|
11
|
+
* `Tool.credentialHandover`. This module only remembers the approvals.
|
|
12
|
+
*
|
|
13
|
+
* Module-scope, per CLI process, never persisted: a new CLI process, or a new
|
|
14
|
+
* session id inside one, asks again. Tests call the reset in beforeEach.
|
|
15
|
+
*/
|
|
16
|
+
const approved = new Set();
|
|
17
|
+
function key(sessionId, alias) {
|
|
18
|
+
return `${sessionId ?? '(no session)'}\u0000${alias}`;
|
|
19
|
+
}
|
|
20
|
+
export function isCredentialHandoverApproved(sessionId, alias) {
|
|
21
|
+
return approved.has(key(sessionId, alias));
|
|
22
|
+
}
|
|
23
|
+
export function markCredentialHandoverApproved(sessionId, alias) {
|
|
24
|
+
approved.add(key(sessionId, alias));
|
|
25
|
+
}
|
|
26
|
+
export function __resetCredentialHandoverGateForTests() {
|
|
27
|
+
approved.clear();
|
|
28
|
+
}
|
|
@@ -34,10 +34,13 @@ export function stripBom(s) {
|
|
|
34
34
|
* Call once, immediately after readline creation, BEFORE any awaits —
|
|
35
35
|
* piped data starts flowing on the next tick.
|
|
36
36
|
*/
|
|
37
|
-
export function attachEarlyLineBuffer(rl) {
|
|
37
|
+
export function attachEarlyLineBuffer(rl, input) {
|
|
38
38
|
const queue = [];
|
|
39
39
|
let firstLine = true;
|
|
40
|
-
|
|
40
|
+
// Round 8 item 3 — the piped-prompt probe (lib/piped-prompt.ts) can see an
|
|
41
|
+
// EMPTY stdin reach EOF before readline exists; readline then never emits
|
|
42
|
+
// 'close'. An input that already ended counts as closed from the start.
|
|
43
|
+
let isClosed = input?.readableEnded === true;
|
|
41
44
|
rl.on('line', (l) => {
|
|
42
45
|
let line = String(l);
|
|
43
46
|
if (firstLine) {
|
package/dist/repl/file-picker.js
CHANGED
|
@@ -139,6 +139,25 @@ function textActionsFor() {
|
|
|
139
139
|
{ label: '/abap-explain (plain-language explanation)', command: '/abap-explain' },
|
|
140
140
|
];
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Three discrete outcomes a `/files` invocation can produce. The caller
|
|
144
|
+
* (REPL handler) needs to distinguish them because the right UX differs:
|
|
145
|
+
* - 'command' — user picked a file + action; REPL dispatches it.
|
|
146
|
+
* - 'empty' — workspace has no files; REPL prints a helpful
|
|
147
|
+
* listing ("drop something here to start").
|
|
148
|
+
* - 'cancelled' — user pressed Cancel or Ctrl+C; REPL prints NOTHING
|
|
149
|
+
* and returns to the main prompt cleanly.
|
|
150
|
+
*
|
|
151
|
+
* Earlier revision returned `string | null` for all three, which
|
|
152
|
+
* collapsed 'empty' and 'cancelled' into the same null path — the REPL
|
|
153
|
+
* then dumped the full file list every time the user cancelled. This
|
|
154
|
+
* tagged result removes the conflation.
|
|
155
|
+
*/
|
|
156
|
+
/** Action menu for a picked file (shared by the inquirer and Ink paths). */
|
|
157
|
+
function actionsForFile(file) {
|
|
158
|
+
const designShape = file.artefactType === 'design' ? detectDesignShape(file.path) : 'unknown';
|
|
159
|
+
return file.kind === 'text' ? textActionsFor() : envelopeActionsFor(file.artefactType, designShape);
|
|
160
|
+
}
|
|
142
161
|
/**
|
|
143
162
|
* Open the picker. Returns one of three tagged outcomes — see
|
|
144
163
|
* `FilePickerResult`. Caller is responsible for dispatching the
|
|
@@ -153,11 +172,15 @@ function textActionsFor() {
|
|
|
153
172
|
* slips through and exits the REPL silently. One guard around the
|
|
154
173
|
* whole flow avoids that gap.
|
|
155
174
|
*/
|
|
156
|
-
export async function openFilePicker() {
|
|
175
|
+
export async function openFilePicker(opts = {}) {
|
|
157
176
|
const files = await listProjectFiles();
|
|
158
177
|
if (files.length === 0) {
|
|
159
178
|
return { kind: 'empty' };
|
|
160
179
|
}
|
|
180
|
+
// Piece 2 §6.4 — under Ink, cards in the card slot; inquirer never runs
|
|
181
|
+
// while Ink owns the terminal. Same result shapes as the inquirer flow.
|
|
182
|
+
if (opts.ink)
|
|
183
|
+
return openFilePickerInk(files);
|
|
161
184
|
try {
|
|
162
185
|
return await withInquirer(async () => {
|
|
163
186
|
// Step 1 — pick a file.
|
|
@@ -186,17 +209,8 @@ export async function openFilePicker() {
|
|
|
186
209
|
const pickedFile = files.find((f) => f.filename === fileResult);
|
|
187
210
|
if (!pickedFile)
|
|
188
211
|
return { kind: 'cancelled' };
|
|
189
|
-
// Step 2 — pick an action for the file
|
|
190
|
-
|
|
191
|
-
// skill-input commands that ingest the file contents. For design
|
|
192
|
-
// envelopes, additionally inspect the object list to pick the
|
|
193
|
-
// right BUILD skill recommendation (RAP vs classic).
|
|
194
|
-
const designShape = pickedFile.artefactType === 'design'
|
|
195
|
-
? detectDesignShape(pickedFile.path)
|
|
196
|
-
: 'unknown';
|
|
197
|
-
const actionsList = pickedFile.kind === 'text'
|
|
198
|
-
? textActionsFor()
|
|
199
|
-
: envelopeActionsFor(pickedFile.artefactType, designShape);
|
|
212
|
+
// Step 2 — pick an action for the file (see actionsForFile).
|
|
213
|
+
const actionsList = actionsForFile(pickedFile);
|
|
200
214
|
const action = await select({
|
|
201
215
|
message: `What with ${pickedFile.filename}?`,
|
|
202
216
|
choices: [
|
|
@@ -217,3 +231,31 @@ export async function openFilePicker() {
|
|
|
217
231
|
return { kind: 'cancelled' };
|
|
218
232
|
}
|
|
219
233
|
}
|
|
234
|
+
async function openFilePickerInk(files) {
|
|
235
|
+
const { withFocusClaim } = await import('../ui/card-slot.js');
|
|
236
|
+
const { filesCardEmitter } = await import('../ui/files-card-emitter.js');
|
|
237
|
+
const { askConfirm } = await import('../ui/confirm-request.js');
|
|
238
|
+
const picked = await withFocusClaim(() => filesCardEmitter.requestPick({
|
|
239
|
+
id: 'files',
|
|
240
|
+
title: 'Pick a project file',
|
|
241
|
+
items: files.map((f) => ({ value: f.filename, label: f.filename, detail: `${f.artefactType} v${f.version ?? '?'}` })),
|
|
242
|
+
}));
|
|
243
|
+
if (picked.value == null)
|
|
244
|
+
return { kind: 'cancelled' };
|
|
245
|
+
const pickedFile = files.find((f) => f.filename === picked.value);
|
|
246
|
+
if (!pickedFile)
|
|
247
|
+
return { kind: 'cancelled' };
|
|
248
|
+
const res = await askConfirm({
|
|
249
|
+
id: 'files-action',
|
|
250
|
+
tag: 'QUESTION',
|
|
251
|
+
title: `What with ${pickedFile.filename}?`,
|
|
252
|
+
choices: [
|
|
253
|
+
...actionsForFile(pickedFile).map((a) => ({ value: a.command, label: a.label })),
|
|
254
|
+
{ value: CANCEL_VALUE, label: 'Cancel' },
|
|
255
|
+
],
|
|
256
|
+
initial: 0,
|
|
257
|
+
}, { trace: false });
|
|
258
|
+
if (res.value == null || res.value === CANCEL_VALUE)
|
|
259
|
+
return { kind: 'cancelled' };
|
|
260
|
+
return { kind: 'command', command: `${res.value} @${pickedFile.filename}` };
|
|
261
|
+
}
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* path (no regression, same as the classic-mode caveat).
|
|
42
42
|
*/
|
|
43
43
|
import { BracketedPasteDecoder } from './bracketed-paste.js';
|
|
44
|
+
import { splitControlKeys } from '../ui/key-burst.js';
|
|
44
45
|
export class InkStdinPasteGuard extends BracketedPasteDecoder {
|
|
45
46
|
/**
|
|
46
47
|
* Mirror the REAL stdin's TTY-ness (the base class hardcodes true for
|
|
@@ -52,6 +53,71 @@ export class InkStdinPasteGuard extends BracketedPasteDecoder {
|
|
|
52
53
|
constructor() {
|
|
53
54
|
super({ emitVerbatim: true });
|
|
54
55
|
}
|
|
56
|
+
/** Keys split off a coalesced control-key chunk, handed out one per read(). */
|
|
57
|
+
keyQueue = [];
|
|
58
|
+
/**
|
|
59
|
+
* Fix round 3 (item 6) — ConPTY coalesces held Backspace / arrow repeats
|
|
60
|
+
* into ONE read, and Ink parses a chunk as ONE key (three Lefts moved the
|
|
61
|
+
* cursor once; DEL bytes were inserted as text). Ink drains with a
|
|
62
|
+
* `while (read() !== null)` loop (App.js handleReadable), so returning one
|
|
63
|
+
* key per read() gives every key its own useInput event.
|
|
64
|
+
*/
|
|
65
|
+
/**
|
|
66
|
+
* Round 4 (item 6) — Ink drained a whole split burst in ONE synchronous
|
|
67
|
+
* loop, so 80+ keys meant 80+ nested React updates and React printed
|
|
68
|
+
* "Maximum update depth exceeded" into the transcript. The guard now yields
|
|
69
|
+
* between keys: after a burst key it ends the drain (read() → null) and
|
|
70
|
+
* re-signals 'readable' on the next macrotask, so React commits each key
|
|
71
|
+
* before the next one. Every key still reaches Ink as its own event.
|
|
72
|
+
*/
|
|
73
|
+
yieldNext = false;
|
|
74
|
+
resumeScheduled = false;
|
|
75
|
+
scheduleResume() {
|
|
76
|
+
if (this.resumeScheduled)
|
|
77
|
+
return;
|
|
78
|
+
this.resumeScheduled = true;
|
|
79
|
+
setImmediate(() => {
|
|
80
|
+
this.resumeScheduled = false;
|
|
81
|
+
if (!this.destroyed)
|
|
82
|
+
this.emit('readable');
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
read(size) {
|
|
87
|
+
// Round 6 item 1 — Node's Readable calls read(0) itself (to refill the
|
|
88
|
+
// buffer when new data arrives, e.g. typing right after a burst) and
|
|
89
|
+
// DISCARDS the result. Handing it a queued key threw that key away: one
|
|
90
|
+
// Backspace of the burst was lost. read(0) never takes a key or data.
|
|
91
|
+
if (size === 0)
|
|
92
|
+
return super.read(0);
|
|
93
|
+
if (this.keyQueue.length > 0) {
|
|
94
|
+
if (this.yieldNext) {
|
|
95
|
+
this.yieldNext = false;
|
|
96
|
+
this.scheduleResume();
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
this.yieldNext = true;
|
|
100
|
+
return this.keyQueue.shift();
|
|
101
|
+
}
|
|
102
|
+
if (this.yieldNext) {
|
|
103
|
+
// The burst's last key was just handed out: let React commit it
|
|
104
|
+
// before anything that arrived after the burst.
|
|
105
|
+
this.yieldNext = false;
|
|
106
|
+
if (this.readableLength > 0) {
|
|
107
|
+
this.scheduleResume();
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const chunk = super.read(size);
|
|
112
|
+
if (typeof chunk !== 'string')
|
|
113
|
+
return chunk;
|
|
114
|
+
const keys = splitControlKeys(chunk);
|
|
115
|
+
if (keys.length <= 1)
|
|
116
|
+
return chunk;
|
|
117
|
+
this.keyQueue = keys.slice(1);
|
|
118
|
+
this.yieldNext = true;
|
|
119
|
+
return keys[0];
|
|
120
|
+
}
|
|
55
121
|
/** Ink calls stdin.ref()/unref() around raw-mode enable/disable. */
|
|
56
122
|
ref() {
|
|
57
123
|
process.stdin.ref?.();
|
|
@@ -26,7 +26,14 @@
|
|
|
26
26
|
* function — no pause, no suppress — which is safe because there is no
|
|
27
27
|
* main readline to disturb.
|
|
28
28
|
*/
|
|
29
|
-
import {
|
|
29
|
+
import { suspendActiveSpinner } from '../renderer/tool-widget.js';
|
|
30
|
+
import { resetTtyStdin } from './reset-tty-stdin.js';
|
|
31
|
+
/**
|
|
32
|
+
* Live fix 2026-09-26 (fix 8) — true while the REPL readline's input is
|
|
33
|
+
* unpiped from stdin for an inquirer prompt (sequence). Re-piped by the
|
|
34
|
+
* final release.
|
|
35
|
+
*/
|
|
36
|
+
let inputDetached = false;
|
|
30
37
|
let state = null;
|
|
31
38
|
/**
|
|
32
39
|
* UX-5 #4 (2026-06-12) — sequence-scoped (debounced) release.
|
|
@@ -66,6 +73,12 @@ export const RELEASE_DEBOUNCE_MS = 50;
|
|
|
66
73
|
let pendingRelease = null;
|
|
67
74
|
/** Re-entrancy depth — release is scheduled only when the OUTERMOST call exits. */
|
|
68
75
|
let activeDepth = 0;
|
|
76
|
+
/**
|
|
77
|
+
* Piece 2 §11.3 — one spinner resume per prompt of the current sequence, all
|
|
78
|
+
* fired by the sequence's release (symmetric suspend/resume, fix round 1: a
|
|
79
|
+
* spinner replaced between prompts is resumed too, not left suspended).
|
|
80
|
+
*/
|
|
81
|
+
let spinnerResumes = [];
|
|
69
82
|
function cancelPendingRelease() {
|
|
70
83
|
if (pendingRelease) {
|
|
71
84
|
clearTimeout(pendingRelease);
|
|
@@ -77,13 +90,16 @@ function cancelPendingRelease() {
|
|
|
77
90
|
* callback must flip the same boolean the readline 'close' handler
|
|
78
91
|
* inspects — otherwise the guard has no effect.
|
|
79
92
|
*/
|
|
80
|
-
export function registerReadline(rl, setSuppress) {
|
|
81
|
-
state = { rl, setSuppress };
|
|
93
|
+
export function registerReadline(rl, setSuppress, opts = {}) {
|
|
94
|
+
state = { rl, setSuppress, input: opts.input, stdin: opts.stdin ?? process.stdin };
|
|
95
|
+
inputDetached = false;
|
|
82
96
|
}
|
|
83
97
|
/** Clear the guard — call when the REPL is shutting down. */
|
|
84
98
|
export function unregisterReadline() {
|
|
85
99
|
cancelPendingRelease();
|
|
86
100
|
activeDepth = 0;
|
|
101
|
+
spinnerResumes = [];
|
|
102
|
+
inputDetached = false;
|
|
87
103
|
state = null;
|
|
88
104
|
}
|
|
89
105
|
/**
|
|
@@ -106,18 +122,30 @@ export function unregisterReadline() {
|
|
|
106
122
|
export async function withInquirer(fn) {
|
|
107
123
|
if (!state)
|
|
108
124
|
return fn();
|
|
109
|
-
const { rl, setSuppress } = state;
|
|
110
|
-
// 2026-05-01:
|
|
125
|
+
const { rl, setSuppress, input, stdin } = state;
|
|
126
|
+
// 2026-05-01: pause any active tool-dispatch spinner so it doesn't fight
|
|
111
127
|
// inquirer for stdout/cursor. The spinner uses \r + erase-line every 80ms,
|
|
112
128
|
// which collides with inquirer's prompt rendering — visible as ghost-text
|
|
113
129
|
// and a cursor that hides under the rotating frame while the user types.
|
|
114
|
-
|
|
130
|
+
// Piece 2 §11.3: SUSPEND (not stop) — the tool is still running after the
|
|
131
|
+
// prompt, so the spinner comes back when the terminal is released.
|
|
132
|
+
spinnerResumes.push(suspendActiveSpinner());
|
|
115
133
|
// UX-5 #4 — continuing an ask SEQUENCE: a release from the previous
|
|
116
134
|
// prompt is still pending, so cancel it. The readline stays parked
|
|
117
135
|
// (paused + close-suppressed) across the gap — no stale-line repaint
|
|
118
136
|
// between consecutive prompts.
|
|
119
137
|
cancelPendingRelease();
|
|
120
138
|
activeDepth++;
|
|
139
|
+
// Live fix 2026-09-26 (fix 8) — detach the REPL readline's input from
|
|
140
|
+
// stdin for the prompt. rl.pause() only stops the decoder's READABLE side:
|
|
141
|
+
// stdin kept writing into it, so every key typed into the prompt was
|
|
142
|
+
// buffered there and replayed into the REPL line on release (echoed onto
|
|
143
|
+
// the spinner line: `⠋ thinking…https://…`). Done BEFORE the listener
|
|
144
|
+
// snapshot below so the pipe's own 'data' handler is never re-attached.
|
|
145
|
+
if (input && !inputDetached) {
|
|
146
|
+
stdin.unpipe(input);
|
|
147
|
+
inputDetached = true;
|
|
148
|
+
}
|
|
121
149
|
// 2026-05-08: snapshot stdin 'data' / 'keypress' listeners BEFORE the
|
|
122
150
|
// inquirer prompt runs. Some inquirer widgets (notably `search`, used
|
|
123
151
|
// by /files step 1) strip ALL listeners on cleanup — including
|
|
@@ -125,8 +153,8 @@ export async function withInquirer(fn) {
|
|
|
125
153
|
// rl.question() shows its prompt with terminal echo on but ignores
|
|
126
154
|
// Enter (no input handler is wired). Surfaced after Diff 8 stopped
|
|
127
155
|
// hiding this with an immediate "Goodbye" exit.
|
|
128
|
-
const beforeData =
|
|
129
|
-
const beforeKeypress =
|
|
156
|
+
const beforeData = stdin.listeners('data').slice();
|
|
157
|
+
const beforeKeypress = stdin.listeners('keypress').slice();
|
|
130
158
|
setSuppress(true);
|
|
131
159
|
rl.pause();
|
|
132
160
|
// 2026-05-08: explicitly engage raw mode BEFORE the inquirer prompt
|
|
@@ -146,9 +174,15 @@ export async function withInquirer(fn) {
|
|
|
146
174
|
// intends — matching what every working "consecutive inquirer prompt"
|
|
147
175
|
// path already does (e.g. /files step 1 → step 2). rl.resume() in
|
|
148
176
|
// finally will re-engage readline keypress mode on its own.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
177
|
+
//
|
|
178
|
+
// Live fix 2026-09-26 (fix 8): `setRawMode(true)` alone is not enough. The
|
|
179
|
+
// previous inquirer prompt's readline.close() did setRawMode(false) +
|
|
180
|
+
// pause(), and our finally resume()d — so a COOKED console read is pending,
|
|
181
|
+
// and on Windows it survives setRawMode(true) (isRaw even reports true):
|
|
182
|
+
// keys were held until Enter ("press Enter first, then type"). The fix 1
|
|
183
|
+
// pty experiments showed only the full cycle ends that read. It is
|
|
184
|
+
// harmless when stdin was fine: it ends raw + flowing, as before.
|
|
185
|
+
resetTtyStdin(stdin);
|
|
152
186
|
try {
|
|
153
187
|
return await fn();
|
|
154
188
|
}
|
|
@@ -157,16 +191,16 @@ export async function withInquirer(fn) {
|
|
|
157
191
|
// didn't replace. The has() check avoids double-binding any
|
|
158
192
|
// listener inquirer DID restore (different instance is OK; the
|
|
159
193
|
// same function reference is the only thing we re-add).
|
|
160
|
-
const afterData = new Set(
|
|
194
|
+
const afterData = new Set(stdin.listeners('data'));
|
|
161
195
|
for (const lf of beforeData) {
|
|
162
196
|
if (!afterData.has(lf)) {
|
|
163
|
-
|
|
197
|
+
stdin.on('data', lf);
|
|
164
198
|
}
|
|
165
199
|
}
|
|
166
|
-
const afterKeypress = new Set(
|
|
200
|
+
const afterKeypress = new Set(stdin.listeners('keypress'));
|
|
167
201
|
for (const lf of beforeKeypress) {
|
|
168
202
|
if (!afterKeypress.has(lf)) {
|
|
169
|
-
|
|
203
|
+
stdin.on('keypress', lf);
|
|
170
204
|
}
|
|
171
205
|
}
|
|
172
206
|
// Order matters here:
|
|
@@ -181,13 +215,22 @@ export async function withInquirer(fn) {
|
|
|
181
215
|
// 3. inside the deferred release, still defer the suppress-clear by
|
|
182
216
|
// one tick so the spurious 'close' that inquirer synchronously
|
|
183
217
|
// emits on release is ignored.
|
|
184
|
-
|
|
218
|
+
stdin.resume();
|
|
185
219
|
activeDepth = Math.max(0, activeDepth - 1);
|
|
186
220
|
if (activeDepth === 0) {
|
|
187
221
|
cancelPendingRelease();
|
|
188
222
|
pendingRelease = setTimeout(() => {
|
|
189
223
|
pendingRelease = null;
|
|
224
|
+
// Fix 8: hand stdin back to the REPL readline (only NEW keys from here).
|
|
225
|
+
if (input && inputDetached) {
|
|
226
|
+
inputDetached = false;
|
|
227
|
+
stdin.pipe(input);
|
|
228
|
+
}
|
|
190
229
|
rl.resume();
|
|
230
|
+
const resumes = spinnerResumes;
|
|
231
|
+
spinnerResumes = [];
|
|
232
|
+
for (const resumeSpinner of resumes)
|
|
233
|
+
resumeSpinner();
|
|
191
234
|
process.nextTick(() => setSuppress(false));
|
|
192
235
|
}, RELEASE_DEBOUNCE_MS);
|
|
193
236
|
}
|