@cspeach/cli 1.1.33 → 1.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/anthropic-provider.js +2 -0
- package/dist/agent/azure-profile.js +7 -0
- package/dist/agent/connection-profile.js +49 -0
- package/dist/agent/loop.js +182 -39
- package/dist/agent/provider-shape.js +36 -0
- package/dist/agent/providers/ai-hub-provider.js +6 -5
- package/dist/agent/providers/azure-provider.js +125 -0
- package/dist/agent/providers/byok-provider.js +8 -7
- package/dist/agent/providers/factory.js +8 -3
- package/dist/agent/providers/local-provider.js +18 -22
- package/dist/agent/risk-gate.js +202 -0
- package/dist/agent/summarise-via-provider.js +55 -8
- package/dist/agent/tool-dispatch.js +19 -0
- package/dist/agent/turn-error-ux.js +22 -0
- package/dist/auth/api-key.js +10 -5
- package/dist/auth/auth-file.js +35 -6
- package/dist/auth/azure/azure-key.js +21 -0
- package/dist/auth/azure/azure-log.js +45 -0
- package/dist/auth/azure/cache-store.js +171 -0
- package/dist/auth/azure/clear.js +28 -0
- package/dist/auth/azure/loopback.js +192 -0
- package/dist/auth/azure/msal-client.js +41 -0
- package/dist/auth/azure/sign-in.js +219 -0
- package/dist/auth/azure/startup-sign-in.js +64 -0
- package/dist/auth/azure/token-provider.js +116 -0
- package/dist/auth/key-resolver.js +258 -0
- package/dist/auth/me.js +11 -5
- package/dist/cli.js +21 -2
- package/dist/commands/compact.js +12 -1
- package/dist/commands/config-set.js +74 -3
- package/dist/commands/config-show.js +25 -0
- package/dist/commands/cost.js +19 -6
- package/dist/commands/help.js +2 -2
- package/dist/commands/login-azure.js +33 -0
- package/dist/commands/login.js +56 -8
- package/dist/commands/logout.js +34 -10
- package/dist/commands/plan-boundary.js +4 -4
- package/dist/commands/plan-model-tier.js +8 -3
- package/dist/commands/usage.js +6 -0
- package/dist/commands/whoami.js +113 -9
- package/dist/config/loader.js +5 -0
- package/dist/connection/azure-auth.js +135 -0
- package/dist/connection/azure-errors.js +103 -0
- package/dist/connection/azure-facts.js +21 -0
- package/dist/connection/azure-gate.js +40 -0
- package/dist/connection/azure-last-call.js +5 -0
- package/dist/connection/azure-turn-error.js +25 -0
- package/dist/connection/cache.js +70 -0
- package/dist/connection/fetch.js +33 -0
- package/dist/connection/guard.js +94 -0
- package/dist/connection/limit-gate.js +26 -0
- package/dist/connection/local-usage.js +71 -0
- package/dist/connection/login-step.js +62 -0
- package/dist/connection/startup.js +67 -0
- package/dist/connection/state.js +68 -0
- package/dist/connection/types.js +1 -0
- package/dist/cost/cost-log.js +3 -1
- package/dist/cost/credits-wire.js +1 -1
- package/dist/cost/credits.js +4 -0
- package/dist/cost/display-mode.js +7 -0
- package/dist/cost/router-spend.js +3 -0
- package/dist/cost/session-cost.js +4 -0
- package/dist/doctor/checks/azure.js +100 -0
- package/dist/doctor/checks/credits.js +4 -3
- package/dist/doctor/checks/usage-reporting.js +22 -0
- package/dist/doctor/run.js +7 -0
- package/dist/lib/graceful-exit.js +17 -0
- package/dist/licence/licence-check.js +205 -0
- package/dist/licence/paths.js +6 -0
- package/dist/licence/turn-gate.js +66 -0
- package/dist/models/model-identity.js +65 -0
- package/dist/models/resolve.js +8 -0
- package/dist/models/server-config.js +3 -2
- package/dist/one-shot.js +86 -2
- package/dist/projects/answer-blockers.js +25 -12
- package/dist/renderer/change-card.js +28 -4
- package/dist/renderer/footer-line.js +7 -1
- package/dist/renderer/notices.js +27 -13
- package/dist/renderer/progress-chatter.js +3 -1
- package/dist/renderer/status-footer.js +13 -6
- package/dist/renderer/tool-labels.js +8 -2
- package/dist/renderer/verify-chain.js +41 -18
- package/dist/renderer/wrap-rows.js +87 -0
- package/dist/repl/ask-or-no.js +18 -0
- package/dist/repl/builtin-commands.js +10 -2
- package/dist/repl/post-turn-status.js +3 -2
- package/dist/repl/safety-confirm.js +47 -2
- package/dist/repl/session-spend-line.js +4 -1
- package/dist/repl/startup-account.js +51 -0
- package/dist/repl.js +217 -48
- package/dist/router/byok-classifier.js +41 -14
- package/dist/router/classifier.js +5 -16
- package/dist/router/router-target.js +41 -0
- package/dist/sap/connection-manager.js +15 -0
- package/dist/sap/onboarding.js +70 -64
- package/dist/sap/session-end.js +66 -0
- package/dist/session/audit-export.js +11 -4
- package/dist/skills/manifest-client.js +37 -4
- package/dist/skills/skills-cache.js +115 -0
- package/dist/skills/source-authed.js +276 -0
- package/dist/tools/ask-question.js +1 -1
- package/dist/tools/filesystem/glob.js +12 -3
- package/dist/tools/filesystem/grep.js +14 -3
- 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 +9 -10
- 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/dist/usage/usage-queue.js +323 -0
- package/dist/usage/usage-reporter.js +270 -0
- package/package.json +4 -3
- package/dist/skills/bundled-skills.js +0 -286
- package/dist/skills/source-bundled.js +0 -20
- package/dist/skills/source-manifest.js +0 -36
|
@@ -31,7 +31,9 @@ import { getGlyphs } from '../../renderer/glyphs.js';
|
|
|
31
31
|
import { truncateToWidth, uiWidth } from '../../renderer/ui-width.js';
|
|
32
32
|
import { roleProps, selectedProps } from '../role-props.js';
|
|
33
33
|
import { sanitizeControlChars } from '../../renderer/sanitize.js';
|
|
34
|
+
import { wrapRows } from '../../renderer/wrap-rows.js';
|
|
34
35
|
import { answerDisplayText } from '../ask-answer-text.js';
|
|
36
|
+
import { CardFrame } from './confirm-card.js';
|
|
35
37
|
/** Option rows visible at once — the rest scroll. */
|
|
36
38
|
export const VISIBLE_OPTION_ROWS = 5;
|
|
37
39
|
/**
|
|
@@ -58,75 +60,26 @@ function fit(text, cells) {
|
|
|
58
60
|
export const QUESTION_MAX_ROWS = 3;
|
|
59
61
|
/** Context wraps too; it gets at least one row per line and at most this many in all (or its line count, if larger). */
|
|
60
62
|
export const CONTEXT_MAX_ROWS = 6;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
return out;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Round 4 item 8b — word-wrap card text to `cells` per row, at most `maxRows`
|
|
79
|
-
* rows (the last row then ends with the `more` glyph). A question cut to one
|
|
80
|
-
* row hid the model's suggestion ("…Sugges…"); it now wraps instead.
|
|
81
|
-
*/
|
|
82
|
-
export function wrapCardText(text, cells, maxRows) {
|
|
83
|
-
const clean = sanitize(text).replace(/\t/g, ' ').replace(/\r?\n/g, ' ');
|
|
84
|
-
const width = Math.max(1, cells);
|
|
85
|
-
const indent = /^ */.exec(clean)[0].slice(0, Math.max(0, width - 1));
|
|
86
|
-
const rows = [];
|
|
87
|
-
let cur = indent;
|
|
88
|
-
const curEmpty = () => cur.length === 0 || cur === indent;
|
|
89
|
-
for (let word of clean.trim().split(/ +/).filter((w) => w.length > 0)) {
|
|
90
|
-
while (stringWidth(word) > width) {
|
|
91
|
-
if (!curEmpty()) {
|
|
92
|
-
rows.push(cur);
|
|
93
|
-
cur = '';
|
|
94
|
-
}
|
|
95
|
-
const head = takeCells(word, width - stringWidth(cur));
|
|
96
|
-
rows.push(cur + head);
|
|
97
|
-
cur = '';
|
|
98
|
-
word = word.slice(head.length);
|
|
99
|
-
}
|
|
100
|
-
if (word.length === 0)
|
|
101
|
-
continue;
|
|
102
|
-
if (curEmpty())
|
|
103
|
-
cur += word;
|
|
104
|
-
else if (stringWidth(cur) + 1 + stringWidth(word) <= width)
|
|
105
|
-
cur += ` ${word}`;
|
|
106
|
-
else {
|
|
107
|
-
rows.push(cur);
|
|
108
|
-
cur = word;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (!curEmpty() || rows.length === 0)
|
|
112
|
-
rows.push(cur);
|
|
113
|
-
const max = Math.max(1, maxRows);
|
|
114
|
-
if (rows.length <= max)
|
|
115
|
-
return rows;
|
|
116
|
-
const kept = rows.slice(0, max);
|
|
117
|
-
kept[max - 1] = truncateToWidth(`${rows.slice(max - 1).join(' ')} `, width);
|
|
118
|
-
return kept;
|
|
119
|
-
}
|
|
120
|
-
/** The question rows, with the multi-select suffix kept whole on the last row. */
|
|
121
|
-
function questionRows(question, suffix, cells) {
|
|
63
|
+
/** Fix batch 2026-10-12 — an option label wraps onto at most this many rows. */
|
|
64
|
+
export const LABEL_MAX_ROWS = 2;
|
|
65
|
+
/** …and its description too. */
|
|
66
|
+
export const DESCRIPTION_MAX_ROWS = 2;
|
|
67
|
+
/** Descriptions sit under the label text: paddingX + "❯ 1. ". */
|
|
68
|
+
const DESCRIPTION_INDENT = ' ';
|
|
69
|
+
/** Task 4 — over budget the context folds to this many rows… */
|
|
70
|
+
export const CONTEXT_FOLDED_ROWS = 3;
|
|
71
|
+
/** …and then the question to this many. */
|
|
72
|
+
export const QUESTION_FOLDED_ROWS = 2;
|
|
73
|
+
/** The question rows (at most `maxRows`), with the multi-select suffix kept whole on the last row. */
|
|
74
|
+
function questionRows(question, suffix, cells, maxRows = QUESTION_MAX_ROWS) {
|
|
122
75
|
if (!suffix)
|
|
123
|
-
return
|
|
124
|
-
const rows =
|
|
76
|
+
return wrapRows(question, cells, maxRows);
|
|
77
|
+
const rows = wrapRows(question, cells, maxRows);
|
|
125
78
|
const last = rows[rows.length - 1];
|
|
126
79
|
if (stringWidth(last) + stringWidth(suffix) <= cells) {
|
|
127
80
|
rows[rows.length - 1] = last + suffix;
|
|
128
81
|
}
|
|
129
|
-
else if (rows.length <
|
|
82
|
+
else if (rows.length < maxRows) {
|
|
130
83
|
rows.push(suffix.trimStart());
|
|
131
84
|
}
|
|
132
85
|
else {
|
|
@@ -134,21 +87,70 @@ function questionRows(question, suffix, cells) {
|
|
|
134
87
|
}
|
|
135
88
|
return rows;
|
|
136
89
|
}
|
|
137
|
-
/**
|
|
138
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Context rows: every line keeps at least one row; wrapping fills the rest of
|
|
92
|
+
* the budget. Folded (`maxRows` below CONTEXT_MAX_ROWS, Task 4) the context
|
|
93
|
+
* takes that many rows at most: more lines than rows run together.
|
|
94
|
+
*/
|
|
95
|
+
function contextRows(context, cells, maxRows = CONTEXT_MAX_ROWS) {
|
|
139
96
|
if (!context)
|
|
140
97
|
return [];
|
|
141
|
-
|
|
142
|
-
|
|
98
|
+
let lines = context.split('\n');
|
|
99
|
+
if (maxRows < CONTEXT_MAX_ROWS && lines.length > maxRows)
|
|
100
|
+
lines = [lines.join(' ')];
|
|
101
|
+
let budget = maxRows < CONTEXT_MAX_ROWS ? maxRows : Math.max(lines.length, CONTEXT_MAX_ROWS);
|
|
143
102
|
const out = [];
|
|
144
103
|
lines.forEach((line, i) => {
|
|
145
104
|
const reserve = lines.length - i - 1; // one row for each line still to come
|
|
146
|
-
const rows =
|
|
105
|
+
const rows = wrapRows(line, cells, Math.max(1, budget - reserve));
|
|
147
106
|
out.push(...rows);
|
|
148
107
|
budget -= rows.length;
|
|
149
108
|
});
|
|
150
109
|
return out;
|
|
151
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Lay out one option at `inner` cells. The label wraps onto LABEL_MAX_ROWS
|
|
113
|
+
* under its own text; the (recommended) / (default) suffix stays whole on
|
|
114
|
+
* the last row (a new row when there is room for one, else the label is cut
|
|
115
|
+
* to make room; with no room at all the label wins and the suffix drops).
|
|
116
|
+
* The description wraps onto DESCRIPTION_MAX_ROWS dim rows under the label.
|
|
117
|
+
*/
|
|
118
|
+
export function optionRows(o, idx, q, inner, layout) {
|
|
119
|
+
const prefixWidth = 2 + `${idx + 1}. `.length + (q.multiSelect ? stringWidth(`${getGlyphs().tickOff} `) : 0);
|
|
120
|
+
const avail = Math.max(1, inner - prefixWidth);
|
|
121
|
+
const wantRec = o.recommended === true && !/\brecommended\b/i.test(o.label);
|
|
122
|
+
const def = q.defaultValue !== undefined && o.value === q.defaultValue;
|
|
123
|
+
const suffix = (wantRec ? REC : '') + (def ? DEFAULT_SUFFIX : '');
|
|
124
|
+
let label = wrapRows(o.label, avail, layout.labelRows);
|
|
125
|
+
let rec = wantRec;
|
|
126
|
+
let showDef = def;
|
|
127
|
+
if (suffix) {
|
|
128
|
+
// The suffix words are wrapped WITH the label; when they land whole at
|
|
129
|
+
// the end, they come off the text again and are drawn dim on that row.
|
|
130
|
+
const joined = wrapRows(`${sanitizeControlChars(o.label)}${suffix}`, avail, layout.labelRows);
|
|
131
|
+
const last = joined[joined.length - 1];
|
|
132
|
+
const bare = suffix.trimStart();
|
|
133
|
+
if (last.endsWith(bare) && !last.endsWith(getGlyphs().more)) {
|
|
134
|
+
const text = last.slice(0, last.length - bare.length).trimEnd();
|
|
135
|
+
label = [...joined.slice(0, -1), text];
|
|
136
|
+
}
|
|
137
|
+
else if (avail - stringWidth(suffix) >= 1) {
|
|
138
|
+
// Too long for the rows: the label is cut so the suffix stays whole.
|
|
139
|
+
const lastLabel = label[label.length - 1];
|
|
140
|
+
label = [...label.slice(0, -1), truncateToWidth(`${lastLabel} `, avail - stringWidth(suffix))];
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
rec = false;
|
|
144
|
+
showDef = false; // no room at all: the label wins
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const description = layout.descriptionRows > 0 && o.description
|
|
148
|
+
? wrapRows(o.description, Math.max(1, inner - DESCRIPTION_INDENT.length), layout.descriptionRows).map((r) => `${DESCRIPTION_INDENT}${r}`)
|
|
149
|
+
: [];
|
|
150
|
+
return { label, rec, def: showDef, description, prefixWidth };
|
|
151
|
+
}
|
|
152
|
+
/** Rows one option takes on the card. */
|
|
153
|
+
const optionHeight = (r) => r.label.length + r.description.length;
|
|
152
154
|
export function toCardQuestions(request) {
|
|
153
155
|
if ('questions' in request) {
|
|
154
156
|
return request.questions.map((q) => {
|
|
@@ -179,51 +181,106 @@ export function toCardQuestions(request) {
|
|
|
179
181
|
keys: request.keys,
|
|
180
182
|
}];
|
|
181
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* The footer. Task 4: the same words as ConfirmCard ("1-N picks · ↑↓ move ·
|
|
186
|
+
* Enter chooses · Esc cancels"), with the real N (rows a digit reaches, at
|
|
187
|
+
* most 9). Multi-select says digits and Space tick; single-key answers add
|
|
188
|
+
* "keys y/n" as ConfirmCard does. Text rows keep "Enter submits".
|
|
189
|
+
*/
|
|
182
190
|
export function askCardHint(kind, opts = {}) {
|
|
183
191
|
const nav = opts.multiQuestion ? ['Tab next question'] : [];
|
|
192
|
+
const digits = `1-${Math.max(1, Math.min(9, opts.count ?? 9))}`;
|
|
193
|
+
const keys = opts.keys && opts.keys.length > 0 ? [`keys ${opts.keys.join('/')}`] : [];
|
|
184
194
|
switch (kind) {
|
|
185
195
|
case 'text': return ['Enter submits', ...nav, 'Esc cancels'].join(' · ');
|
|
186
196
|
case 'other': return ['Enter submits', ...nav, 'Esc goes back to the options'].join(' · ');
|
|
187
197
|
case 'multi':
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
return ['Space or 1-9 ticks a row', 'Enter confirms', ...nav, 'Esc cancels'].join(' · ');
|
|
198
|
+
// The "(choose one or more)" question suffix carries the multi-select cue.
|
|
199
|
+
return [`${digits} or Space ticks`, '↑↓ move', 'Enter chooses', ...nav, 'Esc cancels', ...keys].join(' · ');
|
|
191
200
|
default:
|
|
192
|
-
return ['↑↓ move', '
|
|
201
|
+
return [`${digits} picks`, '↑↓ move', 'Enter chooses', ...nav, 'Esc cancels', ...keys].join(' · ');
|
|
193
202
|
}
|
|
194
203
|
}
|
|
195
|
-
export const REVIEW_HINT = '↑↓ move ·
|
|
204
|
+
export const REVIEW_HINT = '1-2 picks · ↑↓ move · Enter chooses · ← back to edit · Esc cancels';
|
|
196
205
|
const REVIEW_CHOICES = ['Submit answers', 'Cancel'];
|
|
197
206
|
const MULTI_SUFFIX = ' (choose one or more)';
|
|
198
207
|
const REC = ' (recommended)';
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
*/
|
|
204
|
-
|
|
205
|
-
|
|
208
|
+
const DEFAULT_SUFFIX = ' (default)';
|
|
209
|
+
const suffixText = (r) => (r.rec ? REC : '') + (r.def ? DEFAULT_SUFFIX : '');
|
|
210
|
+
/** Content width: the card width less the frame's side and paddingX on both sides (Task 4). */
|
|
211
|
+
const askInner = (width) => Math.max(1, width - 4);
|
|
212
|
+
/** The frame's top and bottom edges. */
|
|
213
|
+
const FRAME_ROWS = 2;
|
|
214
|
+
/** The card's rows at `layout` — worst case over every question screen, plus the Review screen. */
|
|
215
|
+
function askRowsFor(qs, inner, layout) {
|
|
206
216
|
const chipRows = qs.length > 1 ? 2 : 0; // tab row + its blank line
|
|
207
|
-
const inner = Math.max(1, width - 2);
|
|
208
217
|
let worst = 0;
|
|
209
218
|
for (const q of qs) {
|
|
210
|
-
const ctx = contextRows(q.context, inner).length;
|
|
219
|
+
const ctx = contextRows(q.context, inner, layout.contextRows).length;
|
|
211
220
|
const suffix = q.multiSelect && inner - MULTI_SUFFIX.length >= 1 ? MULTI_SUFFIX : '';
|
|
212
|
-
const qRows = questionRows(q.question, suffix, inner).length;
|
|
221
|
+
const qRows = questionRows(q.question, suffix, inner, layout.questionRows).length;
|
|
213
222
|
let body = 2; // margin + the text input row
|
|
214
223
|
if (!q.freeText) {
|
|
215
|
-
|
|
216
|
-
|
|
224
|
+
// Every wrapped label and description row counts; with more options
|
|
225
|
+
// than the window shows, the tallest window of VISIBLE_OPTION_ROWS.
|
|
226
|
+
const heights = q.options.map((o, i) => optionHeight(optionRows(o, i, q, inner, layout)));
|
|
227
|
+
const span = Math.min(heights.length, VISIBLE_OPTION_ROWS);
|
|
228
|
+
let tallest = 0;
|
|
229
|
+
for (let start = 0; start + span <= heights.length; start++) {
|
|
230
|
+
tallest = Math.max(tallest, heights.slice(start, start + span).reduce((x, y) => x + y, 0));
|
|
231
|
+
}
|
|
217
232
|
const arrows = q.options.length > VISIBLE_OPTION_ROWS ? 2 : 0;
|
|
218
|
-
body = 1 +
|
|
233
|
+
body = 1 + tallest + arrows + (q.otherRow ? 1 : 0);
|
|
219
234
|
}
|
|
220
|
-
worst = Math.max(worst, chipRows + qRows + ctx + body +
|
|
235
|
+
worst = Math.max(worst, chipRows + qRows + ctx + body + 1 + FRAME_ROWS); // question + body + hint + frame
|
|
221
236
|
}
|
|
222
237
|
if (qs.length > 1) {
|
|
223
|
-
|
|
238
|
+
// title + one row per answer + margin + choices + hint + frame
|
|
239
|
+
worst = Math.max(worst, chipRows + 1 + qs.length + 1 + REVIEW_CHOICES.length + 1 + FRAME_ROWS);
|
|
224
240
|
}
|
|
225
241
|
return worst;
|
|
226
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* The one place the card's height budget is decided (fix round 1, review
|
|
245
|
+
* I1). Labels and descriptions wrap to their full rows; when the card would
|
|
246
|
+
* be taller than `maxRows` (App: the live area's room for a card), it folds
|
|
247
|
+
* in this order until it fits: labels to 1 row, descriptions to 1 row, no
|
|
248
|
+
* descriptions, then (Task 4) the context to 3 rows and the question to 2.
|
|
249
|
+
* `showDescriptions` false (liveBudget) starts with none. Frame rows count.
|
|
250
|
+
*/
|
|
251
|
+
export function askCardLayout(request, showDescriptions, width = uiWidth(), maxRows) {
|
|
252
|
+
const layout = {
|
|
253
|
+
labelRows: LABEL_MAX_ROWS,
|
|
254
|
+
descriptionRows: showDescriptions ? DESCRIPTION_MAX_ROWS : 0,
|
|
255
|
+
contextRows: CONTEXT_MAX_ROWS,
|
|
256
|
+
questionRows: QUESTION_MAX_ROWS,
|
|
257
|
+
};
|
|
258
|
+
if (maxRows === undefined)
|
|
259
|
+
return layout;
|
|
260
|
+
const qs = toCardQuestions(request);
|
|
261
|
+
const inner = askInner(width);
|
|
262
|
+
const folds = [
|
|
263
|
+
(l) => { l.labelRows = 1; },
|
|
264
|
+
(l) => { l.descriptionRows = Math.min(l.descriptionRows, 1); },
|
|
265
|
+
(l) => { l.descriptionRows = 0; },
|
|
266
|
+
(l) => { l.contextRows = CONTEXT_FOLDED_ROWS; },
|
|
267
|
+
(l) => { l.questionRows = QUESTION_FOLDED_ROWS; },
|
|
268
|
+
];
|
|
269
|
+
for (const fold of folds) {
|
|
270
|
+
if (askRowsFor(qs, inner, layout) <= maxRows)
|
|
271
|
+
break;
|
|
272
|
+
fold(layout);
|
|
273
|
+
}
|
|
274
|
+
return layout;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Upper bound on the rows this card renders — the card's share of the
|
|
278
|
+
* live-area height budget (spec §2; liveBudget decides showDescriptions).
|
|
279
|
+
* With `maxRows`, the height after askCardLayout's folds.
|
|
280
|
+
*/
|
|
281
|
+
export function estimateAskCardRows(request, showDescriptions, width = uiWidth(), maxRows) {
|
|
282
|
+
return askRowsFor(toCardQuestions(request), askInner(width), askCardLayout(request, showDescriptions, width, maxRows));
|
|
283
|
+
}
|
|
227
284
|
/**
|
|
228
285
|
* Cut differently-styled segments of ONE row to `cells` in total: whole
|
|
229
286
|
* segments while they fit, then the one that overflows ends with the `more`
|
|
@@ -250,7 +307,7 @@ function fitSegments(segs, cells) {
|
|
|
250
307
|
}
|
|
251
308
|
return out;
|
|
252
309
|
}
|
|
253
|
-
export function AskCard({ request, width, showDescriptions, onResolveV1, onResolveForm, onInterruptTurn }) {
|
|
310
|
+
export function AskCard({ request, width, showDescriptions, maxRows, onResolveV1, onResolveForm, onInterruptTurn }) {
|
|
254
311
|
useReclaimRawStdin();
|
|
255
312
|
const g = getGlyphs();
|
|
256
313
|
const dim = roleProps('dim');
|
|
@@ -370,6 +427,13 @@ export function AskCard({ request, width, showDescriptions, onResolveV1, onResol
|
|
|
370
427
|
return; // v2: ignored (today)
|
|
371
428
|
}
|
|
372
429
|
// custom marks the free-text ESCAPE on an optioned question only.
|
|
430
|
+
// Task 4 (m3): on a multi-select question the ticked options are kept.
|
|
431
|
+
// The answer is their labels, then the typed words (all typed text).
|
|
432
|
+
if (onOther && q?.multiSelect) {
|
|
433
|
+
const ticked = q.options.filter((o) => ticks[focus].has(o.value)).map((o) => o.label);
|
|
434
|
+
answerAndAdvance(focus, [...ticked, trimmed].join(', '), true);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
373
437
|
answerAndAdvance(focus, trimmed, onOther);
|
|
374
438
|
};
|
|
375
439
|
useInput((input, key) => {
|
|
@@ -382,8 +446,12 @@ export function AskCard({ request, width, showDescriptions, onResolveV1, onResol
|
|
|
382
446
|
if (key.ctrl && input === 'c') {
|
|
383
447
|
const first = !done.current;
|
|
384
448
|
cancel();
|
|
385
|
-
if (first)
|
|
449
|
+
if (first) {
|
|
450
|
+
// Task 4 (m2): a caller asking a series (the spec-gap blockers) stops the rest.
|
|
451
|
+
if (isForm)
|
|
452
|
+
request.onInterrupt?.();
|
|
386
453
|
onInterruptTurn?.();
|
|
454
|
+
}
|
|
387
455
|
return;
|
|
388
456
|
}
|
|
389
457
|
if (key.escape) {
|
|
@@ -472,7 +540,10 @@ export function AskCard({ request, width, showDescriptions, onResolveV1, onResol
|
|
|
472
540
|
pickEnter();
|
|
473
541
|
}, { isActive: true });
|
|
474
542
|
const numbered = (i) => `${i + 1}. `;
|
|
475
|
-
const inner =
|
|
543
|
+
const inner = askInner(width); // the frame's sides and paddingX={1}
|
|
544
|
+
const frameHeader = isForm ? request.frameHeader : undefined;
|
|
545
|
+
// The same fold as estimateAskCardRows, so the drawn card is the estimated card.
|
|
546
|
+
const layout = askCardLayout(request, showDescriptions, width, maxRows);
|
|
476
547
|
const chipRow = n > 1 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { wrap: "truncate-end", children: fitSegments([
|
|
477
548
|
...questions.map((qq, i) => ({
|
|
478
549
|
text: `${i > 0 ? ' ' : ''}${answers[i] !== null ? g.answered : g.pending} ${fit(qq.header, inner)}`,
|
|
@@ -483,7 +554,7 @@ export function AskCard({ request, width, showDescriptions, onResolveV1, onResol
|
|
|
483
554
|
if (!q) {
|
|
484
555
|
// Review step (forms with 2+ questions, all answered).
|
|
485
556
|
const headerCol = Math.min(14, Math.max(...questions.map((qq) => stringWidth(fit(qq.header, inner)))) + 2);
|
|
486
|
-
return (_jsxs(
|
|
557
|
+
return (_jsxs(CardFrame, { tag: "QUESTION", header: frameHeader, width: width, children: [chipRow, _jsx(Text, { bold: true, wrap: "truncate-end", children: fit('Review your answers', inner) }), questions.map((qq, i) => {
|
|
487
558
|
const a = answers[i];
|
|
488
559
|
const head = fit(qq.header, headerCol - 2);
|
|
489
560
|
const cell = ` ${head}${' '.repeat(Math.max(0, headerCol - stringWidth(head)))}`;
|
|
@@ -492,30 +563,35 @@ export function AskCard({ request, width, showDescriptions, onResolveV1, onResol
|
|
|
492
563
|
}), _jsx(Box, { flexDirection: "column", marginTop: 1, children: REVIEW_CHOICES.map((label, i) => {
|
|
493
564
|
const cur = i === reviewCursor;
|
|
494
565
|
return (_jsx(Text, { wrap: "truncate-end", ...(cur ? selectedProps() : {}), children: fit(`${cur ? `${g.cursor} ` : ' '}${numbered(i)}${label}`, inner) }, `rc-${i}`));
|
|
495
|
-
}) }), _jsx(
|
|
566
|
+
}) }), _jsx(Text, { ...dim, wrap: "truncate-end", children: fit(REVIEW_HINT, inner) })] }));
|
|
496
567
|
}
|
|
497
568
|
const winStart = Math.max(0, Math.min(cursor - 3, q.options.length - VISIBLE_OPTION_ROWS));
|
|
498
569
|
const visible = q.options.slice(winStart, winStart + VISIBLE_OPTION_ROWS);
|
|
499
570
|
const below = Math.max(0, q.options.length - (winStart + VISIBLE_OPTION_ROWS));
|
|
500
|
-
const inputWidth = Math.max(8,
|
|
501
|
-
const contextLines = contextRows(q.context, inner);
|
|
571
|
+
const inputWidth = Math.max(8, inner - 2 - `${rowCount}. `.length - 1);
|
|
572
|
+
const contextLines = contextRows(q.context, inner, layout.contextRows);
|
|
502
573
|
const hintKind = q.freeText ? 'text' : onOther ? 'other' : q.multiSelect ? 'multi' : 'pick';
|
|
503
574
|
// The multi-select suffix is the card's only multi cue: it stays whole and
|
|
504
575
|
// the question is cut instead (the same rule as "(recommended)" below).
|
|
505
576
|
const suffix = q.multiSelect && inner - MULTI_SUFFIX.length >= 1 ? MULTI_SUFFIX : '';
|
|
506
|
-
const questionLines = questionRows(q.question, suffix, inner);
|
|
507
|
-
|
|
577
|
+
const questionLines = questionRows(q.question, suffix, inner, layout.questionRows);
|
|
578
|
+
const hint = askCardHint(hintKind, {
|
|
579
|
+
multiQuestion: n > 1,
|
|
580
|
+
count: rowCount,
|
|
581
|
+
keys: Object.keys(q.keys ?? {}).filter((k) => k.length === 1),
|
|
582
|
+
});
|
|
583
|
+
// Task 4 — the same frame as ConfirmCard: QUESTION tag (plus the optional
|
|
584
|
+
// header) on the top edge, the hint right under the options.
|
|
585
|
+
return (_jsxs(CardFrame, { tag: "QUESTION", header: frameHeader, width: width, children: [chipRow, questionLines.map((line, i) => (_jsx(Text, { bold: true, wrap: "truncate-end", children: line }, `q-${i}`))), contextLines.map((line, i) => (_jsx(Text, { ...dim, wrap: "truncate-end", children: line }, `ctx-${i}`))), q.freeText ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { ...roleProps('sap'), children: `${g.cursor} ` }), _jsx(TextInput, { value: drafts[focus], onChange: setDraft, onSubmit: onTextSubmit, maxDisplayWidth: inputWidth })] })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [winStart > 0 && _jsx(Text, { ...dim, wrap: "truncate-end", children: ` ↑ ${winStart} more` }), visible.map((o, vi) => {
|
|
508
586
|
const idx = winStart + vi;
|
|
509
587
|
const cur = !onOther && idx === cursor;
|
|
510
|
-
const rec = o.recommended === true && !/\brecommended\b/i.test(o.label);
|
|
511
588
|
const tick = q.multiSelect ? `${ticks[focus].has(o.value) ? g.tickOn : g.tickOff} ` : '';
|
|
512
589
|
const prefix = `${cur ? `${g.cursor} ` : ' '}${numbered(idx)}${tick}`;
|
|
513
|
-
const
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}), below > 0 && _jsx(Text, { ...dim, wrap: "truncate-end", children: ` ↓ ${below} more` }), q.otherRow ? (_jsxs(Box, { children: [_jsxs(Text, { wrap: "truncate-end", ...(onOther ? selectedProps() : {}), children: [onOther ? `${g.cursor} ` : ' ', numbered(q.options.length)] }), onOther ? (_jsx(TextInput, { value: drafts[focus], onChange: setDraft, onSubmit: onTextSubmit, placeholder: `Type something else${g.more}`, maxDisplayWidth: inputWidth })) : (_jsx(Text, { ...dim, wrap: "truncate-end", children: `Type something else${g.more}` }))] })) : null] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { ...dim, wrap: "truncate-end", children: fit(askCardHint(hintKind, { multiQuestion: n > 1 }), inner) }) })] }));
|
|
590
|
+
const r = optionRows(o, idx, q, inner, layout);
|
|
591
|
+
const pad = ' '.repeat(r.prefixWidth);
|
|
592
|
+
const lastRow = r.label.length - 1;
|
|
593
|
+
return (_jsxs(Box, { flexDirection: "column", children: [r.label.map((row, j) => (_jsxs(Text, { wrap: "truncate-end", ...(cur ? selectedProps() : {}), children: [j === 0 ? prefix : pad, row, j === lastRow && (r.rec || r.def) ? (
|
|
594
|
+
// A suffix that wrapped onto a row of its own sits under the label text, no leading blank.
|
|
595
|
+
_jsx(Text, { ...dim, children: row === '' ? suffixText(r).trimStart() : suffixText(r) })) : null] }, `l-${j}`))), r.description.map((row, j) => (_jsx(Text, { ...dim, wrap: "truncate-end", children: row }, `d-${j}`)))] }, `opt-${idx}`));
|
|
596
|
+
}), below > 0 && _jsx(Text, { ...dim, wrap: "truncate-end", children: ` ↓ ${below} more` }), q.otherRow ? (_jsxs(Box, { children: [_jsxs(Text, { wrap: "truncate-end", ...(onOther ? selectedProps() : {}), children: [onOther ? `${g.cursor} ` : ' ', numbered(q.options.length)] }), onOther ? (_jsx(TextInput, { value: drafts[focus], onChange: setDraft, onSubmit: onTextSubmit, placeholder: `Type something else${g.more}`, maxDisplayWidth: inputWidth })) : (_jsx(Text, { ...dim, wrap: "truncate-end", children: `Type something else${g.more}` }))] })) : null] })), _jsx(Text, { ...dim, wrap: "truncate-end", children: fit(hint, inner) })] }));
|
|
521
597
|
}
|
|
@@ -7,7 +7,26 @@ import { getGlyphs } from '../../renderer/glyphs.js';
|
|
|
7
7
|
import { roleProps, selectedProps } from '../role-props.js';
|
|
8
8
|
import { truncateToWidth, uiWidth } from '../../renderer/ui-width.js';
|
|
9
9
|
import { sanitizeControlChars } from '../../renderer/sanitize.js';
|
|
10
|
+
import { wrapRows } from '../../renderer/wrap-rows.js';
|
|
10
11
|
export const COACHING_HINT = '↑↓ move · 1-9/Enter picks · Enter with nothing selected cancels · Esc cancels';
|
|
12
|
+
/** Fix batch 2026-10-12 — "Pick if:" tells the skills apart, so it wraps onto this many rows. */
|
|
13
|
+
export const PICK_IF_MAX_ROWS = 2;
|
|
14
|
+
const PICK_IF_INDENT = ' ';
|
|
15
|
+
/** The highlighted option's "Pick if:" rows at content width `inner`, each carrying its indent. */
|
|
16
|
+
export function pickIfRows(whenToPick, inner) {
|
|
17
|
+
return wrapRows(`Pick if: ${whenToPick}`, Math.max(1, inner - PICK_IF_INDENT.length), PICK_IF_MAX_ROWS)
|
|
18
|
+
.map((r) => `${PICK_IF_INDENT}${r}`);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The card's rendered height (live-area budget): title + gap + one row per
|
|
22
|
+
* option + the tallest "Pick if:" (only the highlighted option shows one) +
|
|
23
|
+
* gap + hint.
|
|
24
|
+
*/
|
|
25
|
+
export function estimateCoachingPickerRows(params, width = uiWidth()) {
|
|
26
|
+
const inner = Math.max(1, width - 2);
|
|
27
|
+
const pickIf = Math.max(0, ...params.options.map((o) => (o.whenToPick ? pickIfRows(o.whenToPick, inner).length : 0)));
|
|
28
|
+
return 4 + params.options.length + pickIf;
|
|
29
|
+
}
|
|
11
30
|
/**
|
|
12
31
|
* Sanitize, then cut to `cells` terminal cells (same rule as the ask card):
|
|
13
32
|
* the prompt is user text and must not drive the terminal; rows are pre-cut
|
|
@@ -78,6 +97,8 @@ export function CoachingPicker({ params, onResolve, width }) {
|
|
|
78
97
|
return (_jsxs(Box, { flexDirection: "column", width: w, paddingX: 1, flexShrink: 0, children: [_jsx(Text, { bold: true, wrap: "truncate-end", children: fit(`Pick a skill for "${params.prompt}"`, inner) }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: params.options.map((opt, i) => {
|
|
79
98
|
const cur = i === cursor;
|
|
80
99
|
const head = fit(`${cur ? `${g.cursor} ` : ' '}${i + 1}. ${`/${opt.skill}`.padEnd(nameWidth, ' ')}`, inner);
|
|
81
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", ...(cur ? selectedProps() : {}), children: [head, _jsx(Text, { ...(cur ? {} : dim), children: fit(opt.whatItDoes, inner - stringWidth(head)) })] }), cur && opt.whenToPick
|
|
100
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", ...(cur ? selectedProps() : {}), children: [head, _jsx(Text, { ...(cur ? {} : dim), children: fit(opt.whatItDoes, inner - stringWidth(head)) })] }), cur && opt.whenToPick
|
|
101
|
+
? pickIfRows(opt.whenToPick, inner).map((row, j) => (_jsx(Text, { ...dim, wrap: "truncate-end", children: row }, `pick-if-${j}`)))
|
|
102
|
+
: null] }, opt.skill));
|
|
82
103
|
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { ...dim, wrap: "truncate-end", children: fit(COACHING_HINT, inner) }) })] }));
|
|
83
104
|
}
|