@cspeach/cli 1.1.34 → 1.1.36
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/.gitattributes +1 -0
- package/dist/agent/loop.js +127 -31
- package/dist/agent/parallel-write-guard.js +3 -1
- package/dist/commands/plan-audit-evidence.js +6 -1
- package/dist/commands/plan-gate.js +10 -1
- package/dist/commands/plan-resume.js +9 -0
- package/dist/lib/graceful-exit.js +17 -0
- package/dist/one-shot.js +3 -0
- package/dist/projects/answer-blockers.js +25 -12
- package/dist/projects/extract-plan.js +6 -0
- package/dist/projects/plan-objects.js +1 -1
- package/dist/projects/plan-skeleton.js +93 -0
- package/dist/projects/save-command.js +17 -0
- package/dist/rap/finish-checks.js +389 -0
- package/dist/rap/generator-names.js +57 -0
- package/dist/rap/skeleton-card.js +31 -0
- package/dist/rap/skeleton-rules.js +38 -0
- package/dist/rap/skeleton-session.js +21 -0
- package/dist/rap/skeleton-store.js +40 -0
- package/dist/renderer/change-card.js +28 -4
- package/dist/renderer/notices.js +27 -13
- package/dist/renderer/progress-chatter.js +3 -1
- package/dist/renderer/tool-labels.js +12 -3
- package/dist/renderer/verify-chain.js +41 -18
- package/dist/renderer/wrap-rows.js +87 -0
- package/dist/repl/rule8-detector.js +3 -2
- package/dist/repl.js +11 -2
- package/dist/sap/connection-manager.js +15 -0
- package/dist/sap/generator-fact.js +21 -0
- package/dist/sap/session-end.js +66 -0
- package/dist/session/audit-export.js +19 -1
- package/dist/tools/ask-question.js +1 -1
- package/dist/tools/rap/finish-check.js +118 -0
- package/dist/tools/rap/generate-objects.js +317 -0
- package/dist/tools/rap/skeleton-resolve.js +283 -0
- package/dist/tools/sap-read.js +22 -22
- package/dist/tools/sap-write.js +249 -18
- package/dist/tools/snapshot.js +26 -0
- package/dist/tools/tool-loading.js +7 -1
- package/dist/tools/write-mode.js +2 -0
- package/dist/ui/app.js +19 -9
- package/dist/ui/approval-modal.js +37 -5
- package/dist/ui/live-area.js +14 -1
- package/dist/ui/widgets/ask-card.js +183 -107
- package/dist/ui/widgets/coaching-picker.js +22 -1
- package/dist/ui/widgets/confirm-card.js +102 -76
- package/package.json +2 -2
|
@@ -19,79 +19,78 @@ import stringWidth from 'string-width';
|
|
|
19
19
|
import { roleProps, selectedProps } from '../role-props.js';
|
|
20
20
|
import { getBoxChars } from '../ascii-chars.js';
|
|
21
21
|
import { getGlyphs } from '../../renderer/glyphs.js';
|
|
22
|
-
import { truncateToWidth } from '../../renderer/ui-width.js';
|
|
22
|
+
import { truncateToWidth, uiWidth } from '../../renderer/ui-width.js';
|
|
23
|
+
import { wrapRows } from '../../renderer/wrap-rows.js';
|
|
23
24
|
import { sanitizeControlChars } from '../../renderer/sanitize.js';
|
|
24
25
|
// Q1b's shared hook — the one raw-stdin reclaim for every card (no second copy).
|
|
25
26
|
import { useReclaimRawStdin } from './reclaim-raw-stdin.js';
|
|
26
27
|
export const CONFIRM_INPUT_GUARD_MS = 150;
|
|
28
|
+
/** Facts wrap onto this many rows unless the request says otherwise (fix batch 2026-10-12). */
|
|
29
|
+
export const DEFAULT_WRAP_FACTS = 3;
|
|
30
|
+
/** A title wraps onto at most this many rows. */
|
|
31
|
+
export const TITLE_MAX_ROWS = 2;
|
|
32
|
+
/** A choice label wraps onto at most this many rows. */
|
|
33
|
+
export const LABEL_MAX_ROWS = 2;
|
|
34
|
+
/** `${cursor} ${n}. ` — the same width whether the cursor is on the row or not. */
|
|
35
|
+
const choicePrefixWidth = (i) => 2 + `${i + 1}. `.length;
|
|
27
36
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* card that asks for `wrapFacts`: with `width` given, each counts the rows
|
|
33
|
-
* it is drawn on.
|
|
37
|
+
* A choice's rows. The label wraps under its own text (continuation rows are
|
|
38
|
+
* indented past the number). The dim hint stays beside the last label row
|
|
39
|
+
* when it fits; otherwise it gets a row of its own — a hint such as "DEV
|
|
40
|
+
* systems only" is a safety cue and is never dropped.
|
|
34
41
|
*/
|
|
35
|
-
export function
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
export function choiceRows(label, hint, i, inner, labelRows) {
|
|
43
|
+
const p = choicePrefixWidth(i);
|
|
44
|
+
const rows = wrapRows(label, Math.max(1, inner - p), labelRows);
|
|
45
|
+
if (!hint)
|
|
46
|
+
return { label: rows, hint: '', hintOwnRow: false };
|
|
47
|
+
const room = inner - p - stringWidth(rows[rows.length - 1]);
|
|
48
|
+
const beside = ` ${sanitizeControlChars(hint)}`;
|
|
49
|
+
if (stringWidth(beside) <= room)
|
|
50
|
+
return { label: rows, hint: beside, hintOwnRow: false };
|
|
51
|
+
return { label: rows, hint: truncateToWidth(sanitizeControlChars(hint), Math.max(1, inner - p)), hintOwnRow: true };
|
|
52
|
+
}
|
|
53
|
+
function rowsFor(req, inner, l) {
|
|
54
|
+
const title = wrapRows(req.title, inner, l.titleRows).length;
|
|
55
|
+
const facts = (req.facts ?? []).reduce((n, f) => n + wrapRows(f, inner, l.wrap).length, 0);
|
|
56
|
+
const choices = req.choices.reduce((n, c, i) => {
|
|
57
|
+
const r = choiceRows(c.label, c.hint, i, inner, l.labelRows);
|
|
58
|
+
return n + r.label.length + (r.hintOwnRow ? 1 : 0);
|
|
59
|
+
}, 0);
|
|
60
|
+
// top edge + title + facts + gap + choices + hint + bottom edge
|
|
61
|
+
return 4 + title + facts + choices;
|
|
42
62
|
}
|
|
43
63
|
/**
|
|
44
|
-
* The
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* row
|
|
64
|
+
* The layout that keeps the whole card within `maxRows` (App: rows − 6, so
|
|
65
|
+
* Ink never reaches its full-clear path). Card content wraps; when it would
|
|
66
|
+
* not fit, the facts give up rows first (one less each time, down to one
|
|
67
|
+
* row cut with …), then the title, then the choice labels.
|
|
48
68
|
*/
|
|
49
|
-
export function
|
|
50
|
-
|
|
69
|
+
export function confirmLayout(req, width, maxRows) {
|
|
70
|
+
const l = { wrap: Math.max(1, req.wrapFacts ?? DEFAULT_WRAP_FACTS), titleRows: TITLE_MAX_ROWS, labelRows: LABEL_MAX_ROWS };
|
|
51
71
|
if (maxRows === undefined)
|
|
52
|
-
return
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
return l;
|
|
73
|
+
const inner = Math.max(1, width - 4);
|
|
74
|
+
while (l.wrap > 1 && rowsFor(req, inner, l) > maxRows)
|
|
75
|
+
l.wrap -= 1;
|
|
76
|
+
if (rowsFor(req, inner, l) > maxRows)
|
|
77
|
+
l.titleRows = 1;
|
|
78
|
+
if (rowsFor(req, inner, l) > maxRows)
|
|
79
|
+
l.labelRows = 1;
|
|
80
|
+
return l;
|
|
58
81
|
}
|
|
59
|
-
const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
60
82
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
83
|
+
* The card's full rendered height for the live-area budget (F3 liveBudget
|
|
84
|
+
* `cardRows`): top edge + title rows + every fact row + the gap above the
|
|
85
|
+
* choices + every choice row + the hint + bottom edge. Every wrapped row is
|
|
86
|
+
* counted; `width` defaults to uiWidth().
|
|
65
87
|
*/
|
|
66
|
-
export function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
while (out.length < rows - 1 && stringWidth(rest) > cells) {
|
|
73
|
-
let used = 0;
|
|
74
|
-
let space = -1;
|
|
75
|
-
let end = rest.length;
|
|
76
|
-
for (const { segment, index } of graphemes.segment(rest)) {
|
|
77
|
-
const w = stringWidth(segment);
|
|
78
|
-
if (used + w > cells) {
|
|
79
|
-
end = index;
|
|
80
|
-
if (segment === ' ')
|
|
81
|
-
space = index;
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
used += w;
|
|
85
|
-
if (segment === ' ')
|
|
86
|
-
space = index;
|
|
87
|
-
}
|
|
88
|
-
const at = space > 0 ? space : Math.max(1, end);
|
|
89
|
-
out.push(rest.slice(0, at).trimEnd());
|
|
90
|
-
rest = rest.slice(at).trimStart();
|
|
91
|
-
}
|
|
92
|
-
if (rest)
|
|
93
|
-
out.push(truncateToWidth(rest, cells));
|
|
94
|
-
return out;
|
|
88
|
+
export function estimateConfirmCardRows(req, width = uiWidth(), maxRows) {
|
|
89
|
+
return rowsFor(req, Math.max(1, width - 4), confirmLayout(req, width, maxRows));
|
|
90
|
+
}
|
|
91
|
+
/** The rows a long fact may wrap onto so the whole card stays within `maxRows`. */
|
|
92
|
+
export function fittedWrap(req, width, maxRows) {
|
|
93
|
+
return confirmLayout(req, width, maxRows).wrap;
|
|
95
94
|
}
|
|
96
95
|
/**
|
|
97
96
|
* Owner decision (fix-round-3 deferred M1): a digit PICKS only on ordinary
|
|
@@ -155,17 +154,43 @@ export function matchAlias(aliases, input, word) {
|
|
|
155
154
|
function fit(text, cells) {
|
|
156
155
|
return truncateToWidth(sanitizeControlChars(text), cells);
|
|
157
156
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
157
|
+
/**
|
|
158
|
+
* One border object per box-character set, reused on every render. Ink diffs
|
|
159
|
+
* style props by identity: a new borderStyle object on a re-render is sent
|
|
160
|
+
* alone, without borderTop/borderBottom false, and Ink then gives the box a
|
|
161
|
+
* top and bottom border again (two blank rows inside the frame after the
|
|
162
|
+
* first key press). Task 4 found it when AskCard moved into this frame.
|
|
163
|
+
*/
|
|
164
|
+
const borderCache = new Map();
|
|
165
|
+
function makeInkBorder(b) {
|
|
166
|
+
return {
|
|
167
|
+
topLeft: b.tl, top: b.h, topRight: b.tr, right: b.v,
|
|
168
|
+
bottomRight: b.br, bottom: b.h, bottomLeft: b.bl, left: b.v,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const inkBorder = (b) => {
|
|
172
|
+
let out = borderCache.get(b);
|
|
173
|
+
if (!out) {
|
|
174
|
+
out = makeInkBorder(b);
|
|
175
|
+
borderCache.set(b, out);
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* The frame every card in the slot shares: tag chip top edge, footnote bottom
|
|
181
|
+
* edge. `header` (Task 4, e.g. "Blocker 2 of 4") follows the tag on the top
|
|
182
|
+
* edge, dim: "╭─ QUESTION · Blocker 2 of 4 ───╮"; it is cut first when narrow.
|
|
183
|
+
*/
|
|
184
|
+
export function CardFrame({ tag, header, footnote, width, children }) {
|
|
164
185
|
const box = getBoxChars();
|
|
165
186
|
const frame = roleProps('frame');
|
|
166
|
-
const
|
|
187
|
+
const extra = header ? ` · ${sanitizeControlChars(header).replace(/[\t\n]+/g, ' ')}` : '';
|
|
188
|
+
const top = edgeSegments([box.tl, box.tr], box.h, `${tag}${extra}`, width);
|
|
189
|
+
// The shown label is cut from the end, so the tag (if whole) leads it.
|
|
190
|
+
const tagShown = top.label.startsWith(tag) ? tag : top.label;
|
|
191
|
+
const headerShown = top.label.slice(tagShown.length);
|
|
167
192
|
const bottom = edgeSegments([box.bl, box.br], box.h, footnote, width);
|
|
168
|
-
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...frame, children: top.lead }), _jsx(Text, { ...roleProps(TAG_ROLE[tag], { bold: true }), children:
|
|
193
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...frame, children: top.lead }), _jsx(Text, { ...roleProps(TAG_ROLE[tag], { bold: true }), children: tagShown }), headerShown ? _jsx(Text, { ...roleProps('dim'), children: headerShown }) : null, _jsx(Text, { ...frame, children: top.tail })] }), _jsx(Box, { flexDirection: "column", width: width, paddingX: 1, borderStyle: inkBorder(box), borderTop: false, borderBottom: false, borderColor: frame.color, children: children }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...frame, children: bottom.lead }), _jsx(Text, { ...roleProps('dim'), children: bottom.label }), _jsx(Text, { ...frame, children: bottom.tail })] })] }));
|
|
169
194
|
}
|
|
170
195
|
export function ConfirmCard({ request, width, maxRows, onResolve, onInterruptTurn }) {
|
|
171
196
|
// Re-assert raw stdin on mount: an earlier inquirer prompt can leave Ink's
|
|
@@ -235,7 +260,7 @@ export function ConfirmCard({ request, width, maxRows, onResolve, onInterruptTur
|
|
|
235
260
|
finish({ value: hit });
|
|
236
261
|
});
|
|
237
262
|
const inner = Math.max(1, width - 4);
|
|
238
|
-
const
|
|
263
|
+
const layout = confirmLayout(request, width, maxRows);
|
|
239
264
|
const dim = roleProps('dim');
|
|
240
265
|
const titleProps = request.tag === 'CANNOT BE UNDONE' ? roleProps('bad', { bold: true }) : { bold: true };
|
|
241
266
|
const letters = Object.keys(request.aliases ?? {}).filter((k) => k.length === 1);
|
|
@@ -244,13 +269,14 @@ export function ConfirmCard({ request, width, maxRows, onResolve, onInterruptTur
|
|
|
244
269
|
? `1-${digits} picks · ↑↓ move · Enter chooses · Esc cancels`
|
|
245
270
|
: `Enter chooses · Esc cancels · ↑↓ or 1-${digits} move`)
|
|
246
271
|
+ (letters.length > 0 ? ` · keys ${letters.join('/')}` : '');
|
|
247
|
-
return (_jsxs(CardFrame, { tag: request.tag, footnote: request.footnote, width: width, children: [_jsx(Text, { wrap: "truncate-end", ...titleProps, children:
|
|
248
|
-
? factRows(f, inner, wrap).map((row, j) => (_jsx(Text, { wrap: "truncate-end", ...dim, children: row }, `fact-${i}-${j}`)))
|
|
249
|
-
: [_jsx(Text, { wrap: "truncate-end", ...dim, children: fit(f, inner) }, `fact-${i}`)])), _jsx(Box, { flexDirection: "column", marginTop: 1, children: request.choices.map((c, i) => {
|
|
272
|
+
return (_jsxs(CardFrame, { tag: request.tag, footnote: request.footnote, width: width, children: [wrapRows(request.title, inner, layout.titleRows).map((row, j) => (_jsx(Text, { wrap: "truncate-end", ...titleProps, children: row }, `title-${j}`))), (request.facts ?? []).flatMap((f, i) => wrapRows(f, inner, layout.wrap).map((row, j) => (_jsx(Text, { wrap: "truncate-end", ...dim, children: row }, `fact-${i}-${j}`)))), _jsx(Box, { flexDirection: "column", marginTop: 1, children: request.choices.flatMap((c, i) => {
|
|
250
273
|
const on = i === cursor;
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
|
|
274
|
+
const r = choiceRows(c.label, c.hint, i, inner, layout.labelRows);
|
|
275
|
+
const pad = ' '.repeat(choicePrefixWidth(i));
|
|
276
|
+
const sel = on ? selectedProps() : {};
|
|
277
|
+
const out = r.label.map((row, j) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...sel, children: j === 0 ? `${on ? g.cursor : ' '} ${i + 1}. ${row}` : `${pad}${row}` }), j === r.label.length - 1 && r.hint && !r.hintOwnRow ? _jsx(Text, { ...dim, children: r.hint }) : null] }, `${c.value}-${j}`)));
|
|
278
|
+
if (r.hintOwnRow)
|
|
279
|
+
out.push(_jsx(Text, { wrap: "truncate-end", ...dim, children: `${pad}${r.hint}` }, `${c.value}-hint`));
|
|
280
|
+
return out;
|
|
255
281
|
}) }), _jsx(Text, { wrap: "truncate-end", ...dim, children: fit(hint, inner) })] }));
|
|
256
282
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cspeach/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.36",
|
|
4
4
|
"description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, signed skills, safety gates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abap",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@anthropic-ai/sdk": "^0.90.0",
|
|
55
55
|
"@azure/msal-node": "6.0.1",
|
|
56
56
|
"@cspeach/register-core": "0.4.0",
|
|
57
|
-
"@cspeach/sap-client": "0.1.
|
|
57
|
+
"@cspeach/sap-client": "0.1.21",
|
|
58
58
|
"@iarna/toml": "^2.2.5",
|
|
59
59
|
"@inkjs/ui": "^2.0.0",
|
|
60
60
|
"@inquirer/prompts": "^7.0.0",
|