@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
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Three presentation surfaces, decided per call (headless first):
|
|
9
9
|
* - headless → fail-fast decline, no prompt (B5)
|
|
10
|
-
* - Ink →
|
|
11
|
-
* inquirer must NEVER run under Ink
|
|
12
|
-
* React frame for stdin and paints over it
|
|
10
|
+
* - Ink → confirm card in the card slot via askConfirm
|
|
11
|
+
* (piece 2 §5) — inquirer must NEVER run under Ink
|
|
13
12
|
* - classic → inquirer select via withInquirer (unchanged)
|
|
14
13
|
*/
|
|
15
14
|
import chalk from 'chalk';
|
|
@@ -17,9 +16,11 @@ import { select } from '@inquirer/prompts';
|
|
|
17
16
|
import { withInquirer } from './inquirer-guard.js';
|
|
18
17
|
import { inquirerTheme } from './inquirer-theme.js';
|
|
19
18
|
import { isHeadless, shouldUseInk } from '../renderer/tty.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
19
|
+
import { askConfirm } from '../ui/confirm-request.js';
|
|
20
|
+
import { emitTrace } from '../renderer/trace.js';
|
|
21
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
22
|
+
import { clean, hasToolLabel, toolActivityLabel, toolLabel } from '../renderer/tool-labels.js';
|
|
23
|
+
import { cardSlotEmitter, withFocusClaim } from '../ui/card-slot.js';
|
|
23
24
|
const RULE_LABEL = {
|
|
24
25
|
RULE_7_SNAPSHOT_BEFORE_WRITE: 'RULE 7 — snapshot before write',
|
|
25
26
|
RULE_7A_USE_UPDATE_METHOD: 'RULE 7a — use sap_update_method (not sap_set_source)',
|
|
@@ -27,7 +28,98 @@ const RULE_LABEL = {
|
|
|
27
28
|
RULE_9_TRANSPORT_RELEASE: 'RULE 9 — transport release (irreversible)',
|
|
28
29
|
RULE_9_TRANSPORT_ISOLATION: 'RULE 9 — transport isolation',
|
|
29
30
|
RULE_10_VERIFY_AFTER_WRITE: 'RULE 10 — verify after write',
|
|
31
|
+
CREDENTIAL_HANDOVER: 'SAP login handed to a local process',
|
|
30
32
|
};
|
|
33
|
+
/** Piece 2 §5 — plain-words heading: what could go wrong. No rule codes, no tool names. */
|
|
34
|
+
export const SAFETY_HEADING = {
|
|
35
|
+
RULE_7_SNAPSHOT_BEFORE_WRITE: 'This replaces an object in SAP',
|
|
36
|
+
RULE_7A_USE_UPDATE_METHOD: 'A full rewrite could change code you did not ask to change',
|
|
37
|
+
RULE_8_BATCH_PLAN: 'Another SAP change in this turn, without an approved plan',
|
|
38
|
+
RULE_9_TRANSPORT_RELEASE: 'Releasing a transport cannot be undone',
|
|
39
|
+
RULE_9_TRANSPORT_ISOLATION: 'This change may land in a transport shared with other work',
|
|
40
|
+
RULE_10_VERIFY_AFTER_WRITE: 'This change is not proven until it is checked',
|
|
41
|
+
CREDENTIAL_HANDOVER: 'Your SAP login will be handed to a local program',
|
|
42
|
+
};
|
|
43
|
+
/** Bottom-edge note: which rule is asking. */
|
|
44
|
+
export const SAFETY_FOOTNOTE = {
|
|
45
|
+
RULE_7_SNAPSHOT_BEFORE_WRITE: 'Forge Rule 7',
|
|
46
|
+
RULE_7A_USE_UPDATE_METHOD: 'Forge Rule 7a',
|
|
47
|
+
RULE_8_BATCH_PLAN: 'Forge Rule 8',
|
|
48
|
+
RULE_9_TRANSPORT_RELEASE: 'Forge Rule 9',
|
|
49
|
+
RULE_9_TRANSPORT_ISOLATION: 'Forge Rule 9',
|
|
50
|
+
RULE_10_VERIFY_AFTER_WRITE: 'Forge Rule 10',
|
|
51
|
+
CREDENTIAL_HANDOVER: 'Credential handover',
|
|
52
|
+
};
|
|
53
|
+
/** Rule 7 covers both overwrite and delete; the delete says so. */
|
|
54
|
+
export function safetyHeading(a) {
|
|
55
|
+
if (a.rule === 'RULE_7_SNAPSHOT_BEFORE_WRITE' && /delete/.test(a.op))
|
|
56
|
+
return 'This deletes an object from SAP';
|
|
57
|
+
return SAFETY_HEADING[a.rule];
|
|
58
|
+
}
|
|
59
|
+
/** 'set_source' → 'sap_set_source' so piece 1's tool labels apply. */
|
|
60
|
+
function toolNameFor(op) {
|
|
61
|
+
if (hasToolLabel(op))
|
|
62
|
+
return op;
|
|
63
|
+
if (hasToolLabel(`sap_${op}`))
|
|
64
|
+
return `sap_${op}`;
|
|
65
|
+
return op;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The card keeps at most this many what_will_happen lines (the Rule 8 batch
|
|
69
|
+
* summary lists every write this turn — 15+ on a Fiori build). The spec §2
|
|
70
|
+
* height cap (rows − 2) holds only while fact rows stay bounded; the full
|
|
71
|
+
* text goes to scrollback first (presentSafetyConfirmation), like the plan gate.
|
|
72
|
+
*/
|
|
73
|
+
export const SAFETY_WHAT_MAX_LINES = 4;
|
|
74
|
+
/** what_will_happen as display-safe, non-blank lines. */
|
|
75
|
+
function whatLines(a) {
|
|
76
|
+
return a.what_will_happen.split('\n').map(clean).filter((t) => t.length > 0);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* At most SAFETY_WHAT_MAX_LINES rows. The Rule 8 summary puts the op being
|
|
80
|
+
* approved on its LAST line ("… ← about to dispatch"), so an over-long text
|
|
81
|
+
* keeps its header line, a "… N earlier, listed above" row, and the last lines.
|
|
82
|
+
*/
|
|
83
|
+
function boundedWhatLines(lines) {
|
|
84
|
+
if (lines.length <= SAFETY_WHAT_MAX_LINES)
|
|
85
|
+
return lines;
|
|
86
|
+
const tail = SAFETY_WHAT_MAX_LINES - 2;
|
|
87
|
+
const hidden = lines.length - 1 - tail;
|
|
88
|
+
return [lines[0], `${getGlyphs().more} ${hidden} earlier, listed above`, ...lines.slice(lines.length - tail)];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Piece 1's present-tense tool label, fed the real tool args when the caller
|
|
92
|
+
* passed them. A label with no readable object (sap_update_method without
|
|
93
|
+
* class_name/method_name renders "Updating → ") falls back to the op words
|
|
94
|
+
* plus the object type and name.
|
|
95
|
+
*/
|
|
96
|
+
function whatLabel(a) {
|
|
97
|
+
const tool = toolNameFor(a.op);
|
|
98
|
+
const fallback = clean(`${a.op.replace(/_/g, ' ')} ${a.object.type} ${a.object.name}`);
|
|
99
|
+
if (!hasToolLabel(tool))
|
|
100
|
+
return fallback;
|
|
101
|
+
const args = a.args ?? { name: a.object.name, type: a.object.type, transport: a.object.name, path: a.object.name };
|
|
102
|
+
if (!/[\p{L}\p{N}]/u.test(toolLabel(tool, args).object))
|
|
103
|
+
return fallback;
|
|
104
|
+
return toolActivityLabel(tool, args);
|
|
105
|
+
}
|
|
106
|
+
/** What / Where / Undo rows (plain text; the card paints them dim). */
|
|
107
|
+
export function safetyFacts(a) {
|
|
108
|
+
const what = a.rule === 'CREDENTIAL_HANDOVER'
|
|
109
|
+
? `Hand the SAP login for ${clean(a.object.name)} to a local program`
|
|
110
|
+
: whatLabel(a);
|
|
111
|
+
const facts = [`What ${what}`];
|
|
112
|
+
for (const t of boundedWhatLines(whatLines(a)))
|
|
113
|
+
facts.push(` ${t}`);
|
|
114
|
+
const transport = clean(a.transport ?? '');
|
|
115
|
+
const where = transport.length > 0 ? `transport ${transport}` : 'no transport';
|
|
116
|
+
const batch = a.rule === 'RULE_8_BATCH_PLAN' && typeof a.batch_count === 'number'
|
|
117
|
+
? ` · ${a.batch_count} objects this turn`
|
|
118
|
+
: '';
|
|
119
|
+
facts.push(`Where ${where}${batch}`);
|
|
120
|
+
facts.push(`Undo ${a.rollback_available ? 'A snapshot is taken first, so it can be restored' : 'Cannot be undone'}`);
|
|
121
|
+
return facts;
|
|
122
|
+
}
|
|
31
123
|
export async function presentSafetyConfirmation(action) {
|
|
32
124
|
// B5 (2026-06-11) — headless fail-fast. This card is a stdin modal (the
|
|
33
125
|
// Rule 8 batch gate reaches it on the 2nd+ mutating call per turn even in
|
|
@@ -43,86 +135,70 @@ export async function presentSafetyConfirmation(action) {
|
|
|
43
135
|
headless: true,
|
|
44
136
|
};
|
|
45
137
|
}
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// codes (they'd show as raw escape text). Same choice semantics as the
|
|
54
|
-
// classic path below: Apply/Cancel, plus the Rule 9 irreversibility ack.
|
|
55
|
-
// Esc / no listener resolve as cancelled → decline (safe default).
|
|
138
|
+
// Piece 2 §5 — Ink: a SAFETY CHECK (or CANNOT BE UNDONE) confirm card in
|
|
139
|
+
// the card slot. The card slot claims input focus first (requestConfirm
|
|
140
|
+
// runs inside withFocusClaim: suspendActiveSpinner + turnStatusEmitter.pause,
|
|
141
|
+
// both resumed after) — the 2026-06-13 doubled-card / Enter-leak / wedge fix,
|
|
142
|
+
// now in one place.
|
|
143
|
+
// Same values as the classic path: anything but 'apply' declines; Rule 9
|
|
144
|
+
// release asks a second time with Cancel first. Reasons byte-identical.
|
|
56
145
|
if (shouldUseInk()) {
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// row ticking — none of which it pauses (only ask_question /
|
|
63
|
-
// request_approval get paused at the loop level). So when this modal
|
|
64
|
-
// opens, three render sources are still repainting the Ink frame
|
|
65
|
-
// underneath it. That produced the observed live bug: the RULE 8 card
|
|
66
|
-
// rendered DOUBLED, Enter was not consumed by the modal, the keystroke
|
|
67
|
-
// LEAKED to the outer prompt (history-replay of the slash command), and
|
|
68
|
-
// the turn wedged for ~10 minutes while the tool spinner kept ticking.
|
|
69
|
-
//
|
|
70
|
-
// The gate must claim exclusive input focus itself (it can't rely on the
|
|
71
|
-
// loop, which doesn't know a non-interactive tool is about to block on
|
|
72
|
-
// the user). Mirror what the loop does for interactive tools:
|
|
73
|
-
// - clearActiveSpinner(): stop the live in-place tool spinner so only
|
|
74
|
-
// ONE surface (this modal) renders.
|
|
75
|
-
// - turnStatusEmitter.pause(): freeze the ticking status row so it
|
|
76
|
-
// stops repainting the dynamic frame and overdrawing the modal.
|
|
77
|
-
// resume() in the finally restores normal rendering the moment the gate
|
|
78
|
-
// resolves. (The per-second heartbeat is loop-local and is stopped on
|
|
79
|
-
// the loop side by ordering the gate ahead of heartbeat start — see
|
|
80
|
-
// tool-dispatch.ts / loop.ts.)
|
|
81
|
-
clearActiveSpinner();
|
|
82
|
-
const statusWasActive = turnStatusEmitter.snapshot().active;
|
|
83
|
-
if (statusWasActive)
|
|
84
|
-
turnStatusEmitter.pause();
|
|
85
|
-
try {
|
|
86
|
-
const first = await askQuestionEmitter.request({
|
|
87
|
-
id: 'safety-confirm',
|
|
88
|
-
question: `⚠ ${RULE_LABEL[action.rule]}`,
|
|
89
|
-
context: stripAnsi(card).trim(),
|
|
90
|
-
kind: 'choice',
|
|
91
|
-
choices: [
|
|
92
|
-
{ value: 'apply', label: 'Apply' },
|
|
93
|
-
{ value: 'cancel', label: 'Cancel' },
|
|
94
|
-
],
|
|
95
|
-
});
|
|
96
|
-
if (first.cancelled || first.answer !== 'apply') {
|
|
97
|
-
return { confirmed: false, reason: 'user declined first prompt' };
|
|
98
|
-
}
|
|
99
|
-
// RULE 9 transport-release is irreversible — require a second explicit ack.
|
|
100
|
-
if (action.rule === 'RULE_9_TRANSPORT_RELEASE') {
|
|
101
|
-
const second = await askQuestionEmitter.request({
|
|
102
|
-
id: 'safety-confirm-irreversible',
|
|
103
|
-
question: `IRREVERSIBLE — releasing ${action.transport} cannot be undone from this side. ` +
|
|
104
|
-
`Are you absolutely sure?`,
|
|
105
|
-
kind: 'choice',
|
|
106
|
-
// Cancel first → the modal cursor starts there (mirrors the classic
|
|
107
|
-
// path's `default: 'cancel'`).
|
|
108
|
-
choices: [
|
|
109
|
-
{ value: 'cancel', label: 'Cancel' },
|
|
110
|
-
{ value: 'release', label: 'Release transport' },
|
|
111
|
-
],
|
|
112
|
-
});
|
|
113
|
-
if (second.cancelled || second.answer !== 'release') {
|
|
114
|
-
return { confirmed: false, reason: 'user declined irreversibility ack' };
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return { confirmed: true };
|
|
146
|
+
// Rule 9 asks twice. One focus claim spans both cards (refcounted with
|
|
147
|
+
// the claim each card takes), so the status row never resumes and ticks
|
|
148
|
+
// for a moment between them (C2 fix round 1). No listener → no claim.
|
|
149
|
+
if (action.rule === 'RULE_9_TRANSPORT_RELEASE' && cardSlotEmitter.listenerCount('confirm') > 0) {
|
|
150
|
+
return withFocusClaim(() => confirmOnCards(action));
|
|
118
151
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
152
|
+
return confirmOnCards(action);
|
|
153
|
+
}
|
|
154
|
+
const card = renderCard(action);
|
|
155
|
+
return confirmClassic(action, card);
|
|
156
|
+
}
|
|
157
|
+
/** The Ink cards (see presentSafetyConfirmation). */
|
|
158
|
+
async function confirmOnCards(action) {
|
|
159
|
+
const release = action.rule === 'RULE_9_TRANSPORT_RELEASE';
|
|
160
|
+
const what = whatLines(action);
|
|
161
|
+
if (what.length > SAFETY_WHAT_MAX_LINES) {
|
|
162
|
+
// The card keeps the header and the last lines; the whole text lands above it.
|
|
163
|
+
emitTrace(what.join('\n'));
|
|
164
|
+
}
|
|
165
|
+
const first = await askConfirm({
|
|
166
|
+
id: 'safety-confirm',
|
|
167
|
+
tag: release ? 'CANNOT BE UNDONE' : 'SAFETY CHECK',
|
|
168
|
+
title: safetyHeading(action),
|
|
169
|
+
facts: safetyFacts(action),
|
|
170
|
+
choices: [
|
|
171
|
+
{ value: 'apply', label: 'Apply' },
|
|
172
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
173
|
+
],
|
|
174
|
+
initial: 0,
|
|
175
|
+
footnote: SAFETY_FOOTNOTE[action.rule],
|
|
176
|
+
});
|
|
177
|
+
if (first.cancelled || first.value !== 'apply') {
|
|
178
|
+
return { confirmed: false, reason: 'user declined first prompt' };
|
|
179
|
+
}
|
|
180
|
+
if (release) {
|
|
181
|
+
const second = await askConfirm({
|
|
182
|
+
id: 'safety-confirm-irreversible',
|
|
183
|
+
tag: 'CANNOT BE UNDONE',
|
|
184
|
+
title: `Release ${clean(action.transport)}? This cannot be undone from here.`,
|
|
185
|
+
facts: ['Its objects move on to the next system, and the transport can no longer be changed.'],
|
|
186
|
+
// Cancel first → the cursor starts on the safe option (classic: default 'cancel').
|
|
187
|
+
choices: [
|
|
188
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
189
|
+
{ value: 'release', label: 'Release transport' },
|
|
190
|
+
],
|
|
191
|
+
initial: 0,
|
|
192
|
+
footnote: SAFETY_FOOTNOTE[action.rule],
|
|
193
|
+
});
|
|
194
|
+
if (second.cancelled || second.value !== 'release') {
|
|
195
|
+
return { confirmed: false, reason: 'user declined irreversibility ack' };
|
|
124
196
|
}
|
|
125
197
|
}
|
|
198
|
+
return { confirmed: true };
|
|
199
|
+
}
|
|
200
|
+
/** The classic inquirer path (unchanged). */
|
|
201
|
+
async function confirmClassic(action, card) {
|
|
126
202
|
// 2026-05-01: switched from confirm() to select() to fix the "press Enter
|
|
127
203
|
// first, then 'y'" race observed in dispatched-tool rule-8 prompts. confirm
|
|
128
204
|
// has a brief window after rendering its multi-line message before raw
|
|
@@ -161,19 +237,11 @@ export async function presentSafetyConfirmation(action) {
|
|
|
161
237
|
}
|
|
162
238
|
return { confirmed: true };
|
|
163
239
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Strip chalk ANSI escape codes. The Ink ask-question modal renders the
|
|
166
|
-
* card via its own <Text dimColor> — embedded escapes would display as
|
|
167
|
-
* literal `[33m` noise there. (Same approach as renderPlanGate.)
|
|
168
|
-
*/
|
|
169
|
-
function stripAnsi(s) {
|
|
170
|
-
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
171
|
-
}
|
|
172
240
|
/**
|
|
173
241
|
* Minimal indented summary block — replaces the chunky ASCII-art box.
|
|
174
242
|
* Renders as:
|
|
175
243
|
*
|
|
176
|
-
*
|
|
244
|
+
* ! RULE 8 — no batch without plan
|
|
177
245
|
* op: sap_set_source
|
|
178
246
|
* object: PROG ZTEST_FIXED_2
|
|
179
247
|
* transport: (none)
|
|
@@ -181,7 +249,7 @@ function stripAnsi(s) {
|
|
|
181
249
|
* what: Batch in progress (...)
|
|
182
250
|
* rollback: available (snapshot)
|
|
183
251
|
*
|
|
184
|
-
* The
|
|
252
|
+
* The `!` glyph is yellow (alert) — distinct from peach (brand) and red
|
|
185
253
|
* (error) so the user immediately recognises a Forge Rule prompt. Body
|
|
186
254
|
* lines are indented under the header for visual grouping without a box.
|
|
187
255
|
*/
|
|
@@ -190,7 +258,7 @@ function renderCard(action) {
|
|
|
190
258
|
const transport = action.transport && action.transport.length > 0 ? action.transport : '(none)';
|
|
191
259
|
const rollback = action.rollback_available ? 'available (snapshot)' : chalk.red('NOT available');
|
|
192
260
|
lines.push('');
|
|
193
|
-
lines.push(`${chalk.yellow(
|
|
261
|
+
lines.push(`${chalk.yellow(getGlyphs().attention)} ${chalk.yellow.bold(RULE_LABEL[action.rule])}`);
|
|
194
262
|
lines.push(` ${chalk.dim('op: ')} ${action.op}`);
|
|
195
263
|
lines.push(` ${chalk.dim('object: ')} ${action.object.type} ${action.object.name}`);
|
|
196
264
|
lines.push(` ${chalk.dim('transport:')} ${transport}`);
|
|
@@ -7,14 +7,18 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { formatCost } from '../cost/pricing.js';
|
|
9
9
|
import { creditsActive, formatCredits, CREDITS_PENDING } from '../cost/credits.js';
|
|
10
|
-
|
|
10
|
+
/** The spend VALUE in the current display mode (piece 2: goodbye block). */
|
|
11
|
+
export function formatSessionSpendValue(sessionCostUsd, credits) {
|
|
11
12
|
if (!creditsActive())
|
|
12
|
-
return
|
|
13
|
+
return formatCost(sessionCostUsd);
|
|
13
14
|
if (!credits)
|
|
14
|
-
return
|
|
15
|
+
return CREDITS_PENDING;
|
|
15
16
|
const value = formatCredits(credits.sessionTotal, credits.unitLabel);
|
|
16
17
|
const balance = credits.balance !== null
|
|
17
18
|
? ` · balance ${Math.round(credits.balance).toLocaleString('en-US')}`
|
|
18
19
|
: '';
|
|
19
|
-
return
|
|
20
|
+
return `${value}${balance}`;
|
|
21
|
+
}
|
|
22
|
+
export function formatSessionSpendLine(sessionCostUsd, credits) {
|
|
23
|
+
return ` spend: ${formatSessionSpendValue(sessionCostUsd, credits)}`;
|
|
20
24
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/repl/themed-prompts.ts
|
|
2
|
+
//
|
|
3
|
+
// Piece 2 §10: the inquirer prompts with the CSPeach theme applied by default.
|
|
4
|
+
// Wizards import from here instead of '@inquirer/prompts'. The flow, choices,
|
|
5
|
+
// defaults and returned values are exactly the underlying prompt's; a
|
|
6
|
+
// caller-supplied `theme` still wins.
|
|
7
|
+
//
|
|
8
|
+
// The underlying prompt is looked up when the wrapper is CALLED (not at import
|
|
9
|
+
// time), so a test that mocks '@inquirer/prompts' with only some of the four
|
|
10
|
+
// prompts can still load a module that imports this one.
|
|
11
|
+
import * as raw from '@inquirer/prompts';
|
|
12
|
+
import { inquirerTheme } from './inquirer-theme.js';
|
|
13
|
+
function themed(pick) {
|
|
14
|
+
return ((config, context) => pick()({ theme: inquirerTheme, ...config }, context));
|
|
15
|
+
}
|
|
16
|
+
export const select = themed(() => raw.select);
|
|
17
|
+
export const input = themed(() => raw.input);
|
|
18
|
+
export const confirm = themed(() => raw.confirm);
|
|
19
|
+
export const password = themed(() => raw.password);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/repl/ui-look-command.ts — `/ui look [panels|legacy]` and `/ui [auto|ink]`,
|
|
2
|
+
// shared by the Ink and classic /ui handlers. A look change applies immediately
|
|
3
|
+
// (no relaunch needed) and persists; a rendering-mode change applies after relaunch.
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { setLook } from '../renderer/look.js';
|
|
6
|
+
import { formatFailure, formatInfo } from '../renderer/notice-shape.js';
|
|
7
|
+
import { panelHeading, panelRow } from '../renderer/panel-rows.js';
|
|
8
|
+
import { paint } from '../renderer/theme.js';
|
|
9
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
10
|
+
import { CLASSIC_ENV } from '../renderer/rendering-mode.js';
|
|
11
|
+
export async function handleUiLook(arg, cfg, save, emit) {
|
|
12
|
+
const m = /^look(?:\s+(\S+))?$/.exec(arg.trim().toLowerCase());
|
|
13
|
+
if (!m)
|
|
14
|
+
return false;
|
|
15
|
+
const value = m[1];
|
|
16
|
+
if (!value) {
|
|
17
|
+
emit(`Current look: ${chalk.bold(cfg.ui.look)}`);
|
|
18
|
+
emit(panelRow('/ui look panels', 'the new look (default)', 16));
|
|
19
|
+
emit(panelRow('/ui look legacy', 'the previous look', 16));
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (value !== 'panels' && value !== 'legacy') {
|
|
23
|
+
emit(formatFailure({ what: `Unknown look: ${value}. Try panels or legacy.` }).trimEnd());
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
cfg.ui.look = value;
|
|
27
|
+
setLook(value);
|
|
28
|
+
try {
|
|
29
|
+
await save(cfg);
|
|
30
|
+
emit(`${paint('ok', getGlyphs().done)} Look set to ${value}.`);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
emit(formatFailure({ what: `Failed to save config: ${err.message} (applied for this session only)` }).trimEnd());
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `/ui [auto|ink]` — shared by the Ink and classic /ui handlers.
|
|
39
|
+
*
|
|
40
|
+
* Owner ruling 2026-09-27: the classic renderer is retiring. `/ui classic` no
|
|
41
|
+
* longer persists (a stored `classic` is not honoured anyway — see
|
|
42
|
+
* renderer/rendering-mode.ts); the renderer cannot switch mid-session, so it
|
|
43
|
+
* says how to get classic for one launch: the CSPEACH_UI=classic env var.
|
|
44
|
+
*/
|
|
45
|
+
export async function handleUiRendering(arg, cfg, save, emit, effective) {
|
|
46
|
+
const g = getGlyphs();
|
|
47
|
+
if (arg === '') {
|
|
48
|
+
emit('');
|
|
49
|
+
emit(panelHeading('UI mode', `${cfg.ui.rendering} · effective now: ${effective}`));
|
|
50
|
+
emit(panelRow('/ui ink', 'the Ink frame', 9));
|
|
51
|
+
emit(panelRow('/ui auto', 'choose from the terminal', 9));
|
|
52
|
+
emit(panelRow('/ui look', 'panels (default) or legacy', 9));
|
|
53
|
+
emit(formatInfo('Mode changes apply after /exit and a relaunch; look changes apply now').trimEnd());
|
|
54
|
+
emit(formatInfo(`Classic display is retiring · for one launch only: ${CLASSIC_ENV}=classic cspeach`).trimEnd());
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (arg === 'classic') {
|
|
58
|
+
emit(formatInfo('Classic display is retiring · /ui classic is no longer saved').trimEnd());
|
|
59
|
+
emit(formatInfo(`For one launch only, start cspeach with the environment variable ${CLASSIC_ENV}=classic`).trimEnd());
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (arg === 'auto' || arg === 'ink') {
|
|
63
|
+
cfg.ui.rendering = arg;
|
|
64
|
+
try {
|
|
65
|
+
await save(cfg);
|
|
66
|
+
emit(`${paint('ok', g.done)} UI mode set to ${arg}`);
|
|
67
|
+
emit(formatInfo('Relaunch cspeach (/exit, then cspeach) to apply').trimEnd());
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
emit(formatFailure({ what: 'Could not save the config', detail: [err.message] }).trimEnd());
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
emit(formatFailure({ what: `Unknown UI mode: ${arg}`, next: '/ui auto or /ui ink' }).trimEnd());
|
|
75
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* - replPreviewHook — interactive REPL: renders diff + asks confirm
|
|
6
6
|
* - noopPreviewHook — auto-confirm (reserved for trusted paths)
|
|
7
7
|
* - alwaysDeclinePreviewHook — refuse every confirm (one-shot / non-interactive)
|
|
8
|
+
* - inkPreviewHook — Ink REPL: diff to scrollback + Apply/Cancel confirm card
|
|
8
9
|
*
|
|
9
10
|
* B.0a plumbing: factories are wired into ToolContext.previewHook at all 5
|
|
10
11
|
* construction sites. B.1 will consume ctx.previewHook inside sap_update_method.
|
|
@@ -12,6 +13,9 @@
|
|
|
12
13
|
import { confirm } from '@inquirer/prompts';
|
|
13
14
|
import { renderDiff } from './diff-display.js';
|
|
14
15
|
import { withInquirer } from './inquirer-guard.js';
|
|
16
|
+
import { askConfirm } from '../ui/confirm-request.js';
|
|
17
|
+
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
18
|
+
import { withSpinnerSuspended } from '../renderer/tool-widget.js';
|
|
15
19
|
/**
|
|
16
20
|
* B.1 real interactive hook — composes renderDiff + @inquirer/prompts.confirm.
|
|
17
21
|
* Renders a unified diff inside the REPL and asks the user "Apply change?".
|
|
@@ -21,13 +25,52 @@ export const replPreviewHook = async ({ className, methodName, currentBody, prop
|
|
|
21
25
|
label: `${className}->${methodName}`,
|
|
22
26
|
syntaxHighlight: true,
|
|
23
27
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
// Review M1: diff + prompt with the tool spinner suspended throughout, so
|
|
29
|
+
// no spinner frame repaints around the diff before the prompt opens.
|
|
30
|
+
const confirmed = await withSpinnerSuspended(async () => {
|
|
31
|
+
process.stdout.write(`\n${diff}\n`);
|
|
32
|
+
return withInquirer(() => confirm({
|
|
33
|
+
message: `Apply change to ${className}->${methodName}?`,
|
|
34
|
+
default: false,
|
|
35
|
+
}));
|
|
36
|
+
});
|
|
29
37
|
return { confirmed };
|
|
30
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Piece 2 §6.3 — the Ink twin of replPreviewHook. inquirer must never run
|
|
41
|
+
* while Ink is mounted (it steals keystrokes), so the diff goes to
|
|
42
|
+
* scrollback through the chunk emitter and an Apply / Cancel confirm card
|
|
43
|
+
* decides. Cursor starts on Cancel and Esc / no listener = false — the
|
|
44
|
+
* same answer as today's confirm({ default: false }).
|
|
45
|
+
*/
|
|
46
|
+
export function inkPreviewHook(chunkEmitter) {
|
|
47
|
+
return async ({ className, methodName, currentBody, proposedBody }) => {
|
|
48
|
+
// Model / SAP text goes to Ink's <Static> scrollback: neutralise ESC, BEL,
|
|
49
|
+
// lone CR etc. BEFORE diffing (newlines and tabs survive, so the diff is
|
|
50
|
+
// still line-accurate). renderDiff itself does not sanitize.
|
|
51
|
+
const diff = renderDiff(sanitizeControlChars(currentBody), sanitizeControlChars(proposedBody), {
|
|
52
|
+
label: sanitizeControlChars(`${className}->${methodName}`),
|
|
53
|
+
syntaxHighlight: true,
|
|
54
|
+
});
|
|
55
|
+
chunkEmitter.emit('chunk', `\n${diff}\n`);
|
|
56
|
+
const res = await askConfirm({
|
|
57
|
+
id: 'update-method-preview',
|
|
58
|
+
tag: 'APPROVAL',
|
|
59
|
+
title: `Apply this change to method ${methodName} in ${className}?`,
|
|
60
|
+
facts: [
|
|
61
|
+
'The change is shown above.',
|
|
62
|
+
'Only this method is replaced. The rest of the class is untouched.',
|
|
63
|
+
],
|
|
64
|
+
choices: [
|
|
65
|
+
{ value: 'apply', label: 'Apply' },
|
|
66
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
67
|
+
],
|
|
68
|
+
initial: 1,
|
|
69
|
+
footnote: 'Forge Rule 7a',
|
|
70
|
+
});
|
|
71
|
+
return { confirmed: res.value === 'apply' };
|
|
72
|
+
};
|
|
73
|
+
}
|
|
31
74
|
/**
|
|
32
75
|
* No-op hook — confirms automatically without prompting. Use for paths where we
|
|
33
76
|
* trust the LLM (classifier; doctor probe-write where the developer initiated
|