@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,41 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared @inquirer/prompts theme
|
|
3
|
-
* so
|
|
4
|
-
*
|
|
2
|
+
* Shared @inquirer/prompts theme (piece 2 §10) — built from the theme roles
|
|
3
|
+
* and the glyph set, so every wizard (system picker, resume picker, first-run,
|
|
4
|
+
* onboarding, standalone profile, standards init, and the classic pickers)
|
|
5
|
+
* looks like the rest of CSPeach on dark AND light terminals and under
|
|
6
|
+
* NO_COLOR. The answered line reads "✓ <message> <answer>".
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* style to default white so the only colored thing on a finished
|
|
11
|
-
* prompt is the leading green ✔ check.
|
|
12
|
-
*
|
|
13
|
-
* 2026-05-01 (CC-parity color audit): introduced as part of the cuts
|
|
14
|
-
* after screenshot review showed 9 distinct hues on a single screen.
|
|
15
|
-
* Goal is to land at ~5: default, dim, brand peach, green outcome,
|
|
16
|
-
* red error.
|
|
8
|
+
* `prefix` and `icon` are GETTERS: inquirer merges the theme when a prompt
|
|
9
|
+
* starts, so chalk level (NO_COLOR) and the Unicode/ASCII glyph choice are
|
|
10
|
+
* read at that moment, not at import time. `answer` is plain text: the old
|
|
11
|
+
* chalk.white was invisible on a light terminal.
|
|
17
12
|
*/
|
|
18
13
|
import chalk from 'chalk';
|
|
19
|
-
|
|
14
|
+
import { paint } from '../renderer/theme.js';
|
|
15
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
20
16
|
export const inquirerTheme = {
|
|
17
|
+
get prefix() {
|
|
18
|
+
return { idle: paint('sap', '?'), done: paint('ok', getGlyphs().done) };
|
|
19
|
+
},
|
|
20
|
+
get icon() {
|
|
21
|
+
return { cursor: getGlyphs().cursor };
|
|
22
|
+
},
|
|
21
23
|
style: {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*/
|
|
33
|
-
help: (s) => chalk.dim(s),
|
|
34
|
-
/**
|
|
35
|
-
* Highlighted (cursor-on) choice in select / checkbox. Default is
|
|
36
|
-
* cyan. Use a subtle inverse instead — the cursor `❯` glyph is
|
|
37
|
-
* the primary signal anyway.
|
|
38
|
-
*/
|
|
39
|
-
highlight: (s) => chalk.bold(s),
|
|
24
|
+
answer: (s) => s,
|
|
25
|
+
// Round 6 item 4a — once answered, only the message's first line: a
|
|
26
|
+
// multi-line message (the resume picker's column header) glued the
|
|
27
|
+
// header to the answer on the "✓" line.
|
|
28
|
+
message: (s, status) => chalk.bold(status === 'done' ? (s.split('\n')[0] ?? s) : s),
|
|
29
|
+
help: (s) => paint('dim', s),
|
|
30
|
+
highlight: (s) => paint('sap', chalk.bold(s)),
|
|
31
|
+
description: (s) => paint('dim', s),
|
|
32
|
+
error: (s) => paint('bad', `${getGlyphs().failed} ${s}`),
|
|
33
|
+
defaultAnswer: (s) => paint('dim', `(${s})`),
|
|
40
34
|
},
|
|
41
35
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/repl/paste-marker.ts
|
|
2
|
+
//
|
|
3
|
+
// Piece 2 (spec §8) — "pasted text never triggers the shortcuts sheet". The
|
|
4
|
+
// Ink paste guard (ink-stdin-guard.ts / bracketed-paste.ts) strips the paste
|
|
5
|
+
// markers before Ink parses anything, so a pasted '?' is otherwise identical
|
|
6
|
+
// to a typed one. The decoder marks every paste span it re-emits; App's `?`
|
|
7
|
+
// trigger asks wasPasted() before opening the sheet.
|
|
8
|
+
/** A paste chunk reaches useInput within one drain — 500 ms is ample and far below human retyping. */
|
|
9
|
+
export const PASTE_MARK_TTL_MS = 500;
|
|
10
|
+
let last = null;
|
|
11
|
+
export function markPastedChunk(text, now = Date.now()) {
|
|
12
|
+
last = { text, at: now };
|
|
13
|
+
}
|
|
14
|
+
export function wasPasted(input, now = Date.now()) {
|
|
15
|
+
return last !== null && last.text === input && now - last.at <= PASTE_MARK_TTL_MS;
|
|
16
|
+
}
|
|
17
|
+
export function resetPasteMarkForTest() {
|
|
18
|
+
last = null;
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { checklistRenderMode, formatPlanClose, isPlanFinished } from '../renderer/checklist-format.js';
|
|
2
|
+
import { formatTodoChecklistBlock } from '../renderer/todo-block.js';
|
|
3
|
+
import { emitTrace } from '../renderer/trace.js';
|
|
4
|
+
import { todoEmitter, consumeTodoSetThisTurn } from '../ui/todo-emitter.js';
|
|
5
|
+
export function emitPlanTurnEnd(session, write = (s) => { process.stdout.write(s); }) {
|
|
6
|
+
if (!consumeTodoSetThisTurn())
|
|
7
|
+
return;
|
|
8
|
+
const todos = session.todos ?? [];
|
|
9
|
+
if (todos.length === 0)
|
|
10
|
+
return;
|
|
11
|
+
const mode = checklistRenderMode();
|
|
12
|
+
if (mode === 'live') {
|
|
13
|
+
emitTrace(formatPlanClose(todos));
|
|
14
|
+
if (isPlanFinished(todos))
|
|
15
|
+
todoEmitter.emit('close');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (mode === 'turn-end')
|
|
19
|
+
write(formatTodoChecklistBlock(todos));
|
|
20
|
+
}
|
|
@@ -24,6 +24,7 @@ import { globalCredits } from '../cost/credits.js';
|
|
|
24
24
|
import { printStatusFooter, formatStatusFooter } from '../renderer/status-footer.js';
|
|
25
25
|
import { shouldUseInk } from '../renderer/tty.js';
|
|
26
26
|
import { isUserPrompt } from '../session/user-prompt.js';
|
|
27
|
+
import { emitPlanTurnEnd } from './plan-turn-end.js';
|
|
27
28
|
export function snapshotTurnStart(session) {
|
|
28
29
|
return {
|
|
29
30
|
input: session.usage.input_tokens,
|
|
@@ -33,22 +34,34 @@ export function snapshotTurnStart(session) {
|
|
|
33
34
|
startedAt: Date.now(),
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The per-turn numbers every receipt prints — extracted (piece 2 I3a) so the
|
|
39
|
+
* REPL receipt and the one-shot receipt compute them the same way.
|
|
40
|
+
*/
|
|
41
|
+
export function computeTurnFacts(session, snapshot, turnModel) {
|
|
38
42
|
const userTurns = session.messages.filter(isUserPrompt).length;
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const turnCostTokens = {
|
|
43
|
+
const lifetimeAfter = session.usage.input_tokens + session.usage.output_tokens;
|
|
44
|
+
const turnTokens = Math.max(0, lifetimeAfter - (snapshot.input + snapshot.output));
|
|
45
|
+
const turnCost = computeCost(turnModel ?? session.model, {
|
|
43
46
|
input: session.usage.input_tokens - snapshot.input,
|
|
44
47
|
output: session.usage.output_tokens - snapshot.output,
|
|
45
48
|
cacheRead: session.usage.cache_read_input_tokens - snapshot.cacheRead,
|
|
46
49
|
cacheCreate: (session.usage.cache_creation_input_tokens ?? 0) - snapshot.cacheCreate,
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
userTurns,
|
|
53
|
+
turnTokens,
|
|
54
|
+
turnCost,
|
|
55
|
+
// F-L2-2: cost/session-cost.ts so the agent loop can push the SAME total mid-stream.
|
|
56
|
+
sessionCost: computeSessionCost(session),
|
|
57
|
+
elapsedMs: Date.now() - snapshot.startedAt,
|
|
47
58
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
}
|
|
60
|
+
export function pushPostTurnStatus(params) {
|
|
61
|
+
const { session, selectedAlias, snapshot, chunkEmitter, turnModel, cancelled } = params;
|
|
62
|
+
// Piece 2 (spec §4) — the plan's turn-end line lands just above the receipt.
|
|
63
|
+
emitPlanTurnEnd(session);
|
|
64
|
+
const { userTurns, turnTokens, turnCost, sessionCost, elapsedMs } = computeTurnFacts(session, snapshot, turnModel);
|
|
52
65
|
// Task 9 (ux-wave1) — statusline spend wire. Honest semantics note: the
|
|
53
66
|
// store field is named `dollarSpendToday` (legacy sidebar naming, kept to
|
|
54
67
|
// avoid rename churn) but the value written is SESSION-scoped — the same
|
|
@@ -62,6 +75,10 @@ export function pushPostTurnStatus(params) {
|
|
|
62
75
|
// stays as the authoritative end-of-turn settle (the loop's pushes are
|
|
63
76
|
// gated to REPL turns; this one runs on every path that reaches here).
|
|
64
77
|
globalStore.update({ dollarSpendToday: sessionCost });
|
|
78
|
+
// Round 6 item 4d — a cancelled turn that used no tokens has nothing to
|
|
79
|
+
// report ("/abap-explain · 0 tokens · <$0.01" read like a charge).
|
|
80
|
+
if (cancelled && turnTokens === 0)
|
|
81
|
+
return;
|
|
65
82
|
const statusPayload = {
|
|
66
83
|
sessionId: session.id,
|
|
67
84
|
sapAlias: selectedAlias,
|
|
@@ -69,7 +86,7 @@ export function pushPostTurnStatus(params) {
|
|
|
69
86
|
turnCount: userTurns,
|
|
70
87
|
tokens: turnTokens,
|
|
71
88
|
cachedTokens: session.usage.cache_read_input_tokens,
|
|
72
|
-
elapsedMs:
|
|
89
|
+
elapsedMs: elapsedMs,
|
|
73
90
|
costThisTurn: turnCost,
|
|
74
91
|
costSession: sessionCost,
|
|
75
92
|
sessionInputTokens: session.usage.input_tokens,
|
|
@@ -95,3 +112,11 @@ export function pushPostTurnStatus(params) {
|
|
|
95
112
|
printStatusFooter(statusPayload);
|
|
96
113
|
}
|
|
97
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Round 7 item 8c — seed the footer's live spend from a resumed session's own
|
|
117
|
+
* usage, so it does not read $0.00 until the first turn and then jump to the
|
|
118
|
+
* session total. A new session has no usage and stays at $0.00.
|
|
119
|
+
*/
|
|
120
|
+
export function seedFooterSpend(session) {
|
|
121
|
+
globalStore.update({ dollarSpendToday: computeSessionCost(session) });
|
|
122
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/repl/reroute-turn.ts
|
|
2
|
+
//
|
|
3
|
+
// The /reroute turn wrapper (final review, pre-existing gap). Both /reroute
|
|
4
|
+
// handlers in repl.tsx (Ink + classic) run a turn outside the normal turn
|
|
5
|
+
// path, and had no catch: the Ink loop runs `void handleSubmit(line)`, so a
|
|
6
|
+
// provider error during /reroute became an unhandled rejection and crashed the
|
|
7
|
+
// REPL. This wrapper renders the error with the same turn-error block the
|
|
8
|
+
// normal turn paths use, then always runs the caller's turn-end step.
|
|
9
|
+
import { renderTurnError } from '../agent/turn-error-ux.js';
|
|
10
|
+
/** Never rejects because of the turn: a turn error is rendered, not thrown. */
|
|
11
|
+
export async function runRerouteTurn(p) {
|
|
12
|
+
try {
|
|
13
|
+
await p.run();
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
await renderTurnError({ err, session: p.session, userInterrupted: p.signal?.aborted ?? false, inSession: true, print: p.print });
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
p.onFinally();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resetTtyStdin — revive Windows console stdin after an @inquirer prompt.
|
|
3
|
+
*
|
|
4
|
+
* Live fix 2026-09-26: after the `cspeach --resume` session picker (an
|
|
5
|
+
* @inquirer/prompts `select`) closes, Windows console stdin delivers NO data
|
|
6
|
+
* to the Ink pipe even though the stream reports flowing:true / raw:true, so
|
|
7
|
+
* the Ink input box receives no keystrokes. pty experiments showed that
|
|
8
|
+
* `resume()`, `setRawMode(true)+resume()`, a 300 ms wait and
|
|
9
|
+
* pause/setImmediate/resume all fail; ONLY the full cycle
|
|
10
|
+
* `setRawMode(false) → pause() → setRawMode(true) → resume()` restores input.
|
|
11
|
+
*
|
|
12
|
+
* Called in runRepl immediately before `process.stdin.pipe(inkStdin)`, which
|
|
13
|
+
* runs after every pre-Ink inquirer prompt (resume picker, SAP system picker,
|
|
14
|
+
* API-key prompt, first-run choice, onboarding). On a plain start it is
|
|
15
|
+
* harmless: stdin ends raw + flowing and the pipe attaches synchronously
|
|
16
|
+
* right after, then Ink manages raw mode itself.
|
|
17
|
+
*
|
|
18
|
+
* Returns true when the cycle ran. Never throws.
|
|
19
|
+
*/
|
|
20
|
+
export function resetTtyStdin(stdin = process.stdin) {
|
|
21
|
+
if (stdin.isTTY !== true || typeof stdin.setRawMode !== 'function')
|
|
22
|
+
return false;
|
|
23
|
+
try {
|
|
24
|
+
stdin.setRawMode(false);
|
|
25
|
+
stdin.pause();
|
|
26
|
+
stdin.setRawMode(true);
|
|
27
|
+
stdin.resume();
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Review I2 (2026-09-27): run the reset cycle, then pipe stdin into the REPL
|
|
36
|
+
* input. Used by BOTH runRepl branches — Ink (`inkStdin`) and classic
|
|
37
|
+
* (`pasteInput`) — because both are reached through the same pre-REPL
|
|
38
|
+
* inquirer prompts. The pipe happens even when the reset is a no-op
|
|
39
|
+
* (non-TTY / no setRawMode).
|
|
40
|
+
*/
|
|
41
|
+
export function pipeRevivedStdin(dest, stdin = process.stdin) {
|
|
42
|
+
resetTtyStdin(stdin);
|
|
43
|
+
return stdin.pipe(dest);
|
|
44
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function createRestoreGuard(hooks) {
|
|
2
|
+
let active = false;
|
|
3
|
+
let exitAfter = false;
|
|
4
|
+
return {
|
|
5
|
+
get active() { return active; },
|
|
6
|
+
requestExit() { exitAfter = true; },
|
|
7
|
+
async run(restore) {
|
|
8
|
+
active = true;
|
|
9
|
+
try {
|
|
10
|
+
await restore();
|
|
11
|
+
}
|
|
12
|
+
finally {
|
|
13
|
+
active = false;
|
|
14
|
+
hooks.resetDispatchState();
|
|
15
|
+
if (exitAfter) {
|
|
16
|
+
exitAfter = false;
|
|
17
|
+
hooks.exit();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { STANDALONE_ALIAS } from '../tools/index.js';
|
|
2
|
+
import { formatInfo } from '../renderer/notice-shape.js';
|
|
3
|
+
/**
|
|
4
|
+
* Round 6 item 2 + review I3 — standalone or not, for a start that may be a
|
|
5
|
+
* resume. `--no-sap` (or persisted standalone) always wins. A resumed session
|
|
6
|
+
* keeps its system — unless an explicit `--sap-alias` names a DIFFERENT one:
|
|
7
|
+
* then the flag wins and the session moves to it (standalone → SAP, or one
|
|
8
|
+
* SAP system to another). The same alias is a no-op.
|
|
9
|
+
*/
|
|
10
|
+
export function resumeStandaloneDecision(p) {
|
|
11
|
+
if (p.requestedStandalone) {
|
|
12
|
+
// Final batch 4 — standalone requested for a resumed SAP session: the
|
|
13
|
+
// session itself moves to standalone (consistently, and it says so).
|
|
14
|
+
return p.resumedSapSystem && p.resumedSapSystem !== STANDALONE_ALIAS
|
|
15
|
+
? { standalone: true, switchedFrom: p.resumedSapSystem, switchedTo: STANDALONE_ALIAS, standaloneSource: p.standaloneSource ?? '--no-sap' }
|
|
16
|
+
: { standalone: true };
|
|
17
|
+
}
|
|
18
|
+
if (p.resumedSapSystem === STANDALONE_ALIAS) {
|
|
19
|
+
return p.sapAlias
|
|
20
|
+
? { standalone: false, switchedFrom: STANDALONE_ALIAS, switchedTo: p.sapAlias }
|
|
21
|
+
: { standalone: true };
|
|
22
|
+
}
|
|
23
|
+
if (p.resumedSapSystem && p.sapAlias && p.sapAlias !== p.resumedSapSystem) {
|
|
24
|
+
return { standalone: false, switchedFrom: p.resumedSapSystem, switchedTo: p.sapAlias };
|
|
25
|
+
}
|
|
26
|
+
return { standalone: false };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Apply a switch decided above: the session moves to the selected system, is
|
|
30
|
+
* saved, and one line says so. No switch → nothing happens.
|
|
31
|
+
*/
|
|
32
|
+
export async function applySystemSwitch(p) {
|
|
33
|
+
const { decision } = p;
|
|
34
|
+
if (!decision.switchedTo)
|
|
35
|
+
return;
|
|
36
|
+
// Re-review (defence in depth) — never relabel a session whose SAP changes
|
|
37
|
+
// are unfinished: they belong to the system they were made on.
|
|
38
|
+
if ((p.session.toolCalls ?? []).some((c) => !c.completed))
|
|
39
|
+
return;
|
|
40
|
+
const from = decision.switchedFrom === STANDALONE_ALIAS ? 'standalone' : `on ${decision.switchedFrom}`;
|
|
41
|
+
if (decision.switchedTo === STANDALONE_ALIAS) {
|
|
42
|
+
// Final batch 4 — a SAP session resumed with --no-sap.
|
|
43
|
+
if (!decision.standalone)
|
|
44
|
+
return;
|
|
45
|
+
p.session.sap_system = STANDALONE_ALIAS;
|
|
46
|
+
p.session.systemSwitchNote =
|
|
47
|
+
`This session was ${from} until now. It is now standalone (no SAP system connected). `
|
|
48
|
+
+ 'Do not read from or write to SAP; work from pasted code and hand changes over for manual implementation.';
|
|
49
|
+
await p.save(p.session);
|
|
50
|
+
p.notify(formatInfo(`This session was ${from} · now standalone (${decision.standaloneSource ?? '--no-sap'})`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (decision.standalone)
|
|
54
|
+
return;
|
|
55
|
+
p.session.sap_system = p.selectedAlias;
|
|
56
|
+
// Round 6 review I4 — the model is told once, on the next turn.
|
|
57
|
+
p.session.systemSwitchNote =
|
|
58
|
+
`This session was ${decision.switchedFrom === STANDALONE_ALIAS ? 'standalone' : `on ${decision.switchedFrom}`} until now. `
|
|
59
|
+
+ `It is now connected to ${p.selectedAlias}. Validate packages, transports and objects against the system; `
|
|
60
|
+
+ 'standalone waivers no longer apply.';
|
|
61
|
+
await p.save(p.session);
|
|
62
|
+
p.notify(formatInfo(`This session was ${from} · now on ${p.selectedAlias} (--sap-alias)`));
|
|
63
|
+
}
|
|
@@ -76,12 +76,12 @@ export function renderPlanSummary(proposedNext) {
|
|
|
76
76
|
const lines = [];
|
|
77
77
|
lines.push(`Batch in progress (${writeOpsThisTurn.length + 1} write ops this turn):`);
|
|
78
78
|
writeOpsThisTurn.forEach((op, i) => {
|
|
79
|
-
lines.push(` ${i + 1}. ${op.tool} ${summarizeArgs(op.args)}`);
|
|
79
|
+
lines.push(` ${i + 1}. ${op.tool} ${summarizeArgs(op.args, op.tool)}`);
|
|
80
80
|
});
|
|
81
|
-
lines.push(` ${writeOpsThisTurn.length + 1}. ${proposedNext.tool} ${summarizeArgs(proposedNext.args)} ← about to dispatch`);
|
|
81
|
+
lines.push(` ${writeOpsThisTurn.length + 1}. ${proposedNext.tool} ${summarizeArgs(proposedNext.args, proposedNext.tool)} ← about to dispatch`);
|
|
82
82
|
return lines.join('\n');
|
|
83
83
|
}
|
|
84
|
-
function summarizeArgs(args) {
|
|
84
|
+
function summarizeArgs(args, tool) {
|
|
85
85
|
// Best-effort one-line summary. Pulls the most useful identifier fields so a
|
|
86
86
|
// customer reviewing the batch gate sees WHAT each op targets (Forge Rule 8
|
|
87
87
|
// is unauditable theatre otherwise):
|
|
@@ -90,6 +90,14 @@ function summarizeArgs(args) {
|
|
|
90
90
|
// - SAP object writes → type+name, class_name->method_name, transport
|
|
91
91
|
const a = args;
|
|
92
92
|
const parts = [];
|
|
93
|
+
// L1-6 R1-6: a preview start hands the stored SAP login to a local process.
|
|
94
|
+
// Say so in plain words — the approver must see it, not only the model.
|
|
95
|
+
if (tool === 'fiori_preview' && (a['action'] === undefined || a['action'] === 'start')) {
|
|
96
|
+
const alias = typeof a['alias'] === 'string' && a['alias'] ? a['alias'] : 'the session alias';
|
|
97
|
+
const dir = typeof a['appDir'] === 'string' && a['appDir'] ? `start ${a['appDir']} — ` : 'start — ';
|
|
98
|
+
return (`${dir}the SAP login for ${alias} is passed to the preview process through its environment ` +
|
|
99
|
+
`when the app's ui5 config names a backend (an app with no backend url starts without it)`);
|
|
100
|
+
}
|
|
93
101
|
// Local-FS writes (the fiori-build path: 13× file_write + 2× shell_exec) —
|
|
94
102
|
// the path / command is the only meaningful descriptor; surface it FIRST.
|
|
95
103
|
if (typeof a['path'] === 'string' && a['path']) {
|