@cspeach/cli 1.1.18 → 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 +58 -21
- 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/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
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* AskCard — the one question card (piece 2, spec §3). Replaces the
|
|
4
|
+
* retired v1 modal and v2 form widgets (both deleted in Q2).
|
|
5
|
+
*
|
|
6
|
+
* NO NEW VALUES (spec invariant) — every resolution is one today's widgets
|
|
7
|
+
* could return:
|
|
8
|
+
* v1 choice → { answer: value } | { answer: null, cancelled: true }
|
|
9
|
+
* v1 multi → { answer: 'v1,v2' } (Enter with nothing ticked → the highlighted row)
|
|
10
|
+
* v1 text → { answer: trimmed } (Enter on empty → cancelled, as today)
|
|
11
|
+
* v2 form → { formId, answers: [{ id, answer, custom? }] } | { formId, answers: [], cancelled: true }
|
|
12
|
+
* v1 choice/multi get NO free-text row. v2 optioned questions keep one —
|
|
13
|
+
* "N. Type something else…", today's Other escape (custom: true).
|
|
14
|
+
*
|
|
15
|
+
* Forms with 2+ questions: a tab row (pending/answered chip per header,
|
|
16
|
+
* then a Submit chip) and, after the last answer, a Review step
|
|
17
|
+
* ("1. Submit answers" / "2. Cancel"). Cancel returns
|
|
18
|
+
* { formId, answers: [], cancelled: true } — exactly today's Esc.
|
|
19
|
+
*
|
|
20
|
+
* Keys: ↑↓ move · 1-9 pick (single) or tick (multi) · Space ticks · Enter
|
|
21
|
+
* chooses · Tab/→ next and Shift+Tab/← previous question (←/→ only while
|
|
22
|
+
* the text input is not focused) · Esc cancels; on the free-text row Esc
|
|
23
|
+
* first returns to the options.
|
|
24
|
+
*/
|
|
25
|
+
import { useMemo, useRef, useState } from 'react';
|
|
26
|
+
import { Box, Text, useInput } from 'ink';
|
|
27
|
+
import TextInput from '../text-input.js';
|
|
28
|
+
import { useReclaimRawStdin } from './reclaim-raw-stdin.js';
|
|
29
|
+
import stringWidth from 'string-width';
|
|
30
|
+
import { getGlyphs } from '../../renderer/glyphs.js';
|
|
31
|
+
import { truncateToWidth, uiWidth } from '../../renderer/ui-width.js';
|
|
32
|
+
import { roleProps, selectedProps } from '../role-props.js';
|
|
33
|
+
import { sanitizeControlChars } from '../../renderer/sanitize.js';
|
|
34
|
+
import { answerDisplayText } from '../ask-answer-text.js';
|
|
35
|
+
/** Option rows visible at once — the rest scroll. */
|
|
36
|
+
export const VISIBLE_OPTION_ROWS = 5;
|
|
37
|
+
/**
|
|
38
|
+
* Single-key cards (the Yes/No agent confirm) drop keys received in the first
|
|
39
|
+
* GUARD_MS after mount, so a key typed ahead while output streamed cannot
|
|
40
|
+
* answer the card.
|
|
41
|
+
*/
|
|
42
|
+
const GUARD_MS = 150;
|
|
43
|
+
/**
|
|
44
|
+
* Card text is model- or tool-supplied. Tabs and newlines are flattened by
|
|
45
|
+
* truncateToWidth; every OTHER control character is neutralised by the
|
|
46
|
+
* shared sanitizer so it cannot drive the terminal inside the live frame.
|
|
47
|
+
*/
|
|
48
|
+
const sanitize = sanitizeControlChars;
|
|
49
|
+
/**
|
|
50
|
+
* Sanitize, then cut to `cells` terminal cells with string-width. Ink's own
|
|
51
|
+
* truncate-end counts some emoji as one cell, so rows are pre-cut here and
|
|
52
|
+
* Ink's truncation is only the safety net.
|
|
53
|
+
*/
|
|
54
|
+
function fit(text, cells) {
|
|
55
|
+
return truncateToWidth(sanitize(text), cells);
|
|
56
|
+
}
|
|
57
|
+
/** Round 4 item 8b — the question wraps up to this many rows, then is cut. */
|
|
58
|
+
export const QUESTION_MAX_ROWS = 3;
|
|
59
|
+
/** 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
|
+
export const CONTEXT_MAX_ROWS = 6;
|
|
61
|
+
const GRAPHEMES = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
62
|
+
/** The longest grapheme-whole prefix of `word` that fits `cells`. */
|
|
63
|
+
function takeCells(word, cells) {
|
|
64
|
+
let out = '';
|
|
65
|
+
let used = 0;
|
|
66
|
+
for (const { segment } of GRAPHEMES.segment(word)) {
|
|
67
|
+
const w = stringWidth(segment);
|
|
68
|
+
if (used + w > cells && out.length > 0)
|
|
69
|
+
break;
|
|
70
|
+
out += segment;
|
|
71
|
+
used += w;
|
|
72
|
+
if (used >= cells)
|
|
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) {
|
|
122
|
+
if (!suffix)
|
|
123
|
+
return wrapCardText(question, cells, QUESTION_MAX_ROWS);
|
|
124
|
+
const rows = wrapCardText(question, cells, QUESTION_MAX_ROWS);
|
|
125
|
+
const last = rows[rows.length - 1];
|
|
126
|
+
if (stringWidth(last) + stringWidth(suffix) <= cells) {
|
|
127
|
+
rows[rows.length - 1] = last + suffix;
|
|
128
|
+
}
|
|
129
|
+
else if (rows.length < QUESTION_MAX_ROWS) {
|
|
130
|
+
rows.push(suffix.trimStart());
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
rows[rows.length - 1] = truncateToWidth(`${last} `, Math.max(1, cells - stringWidth(suffix))) + suffix;
|
|
134
|
+
}
|
|
135
|
+
return rows;
|
|
136
|
+
}
|
|
137
|
+
/** Context rows: every line keeps at least one row; wrapping fills the rest of the budget. */
|
|
138
|
+
function contextRows(context, cells) {
|
|
139
|
+
if (!context)
|
|
140
|
+
return [];
|
|
141
|
+
const lines = context.split('\n');
|
|
142
|
+
let budget = Math.max(lines.length, CONTEXT_MAX_ROWS);
|
|
143
|
+
const out = [];
|
|
144
|
+
lines.forEach((line, i) => {
|
|
145
|
+
const reserve = lines.length - i - 1; // one row for each line still to come
|
|
146
|
+
const rows = wrapCardText(line, cells, Math.max(1, budget - reserve));
|
|
147
|
+
out.push(...rows);
|
|
148
|
+
budget -= rows.length;
|
|
149
|
+
});
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
export function toCardQuestions(request) {
|
|
153
|
+
if ('questions' in request) {
|
|
154
|
+
return request.questions.map((q) => {
|
|
155
|
+
const freeText = q.freeText || q.options.length === 0;
|
|
156
|
+
return {
|
|
157
|
+
id: q.id,
|
|
158
|
+
question: q.question,
|
|
159
|
+
context: q.context,
|
|
160
|
+
header: q.header ?? q.id,
|
|
161
|
+
multiSelect: !freeText && q.multiSelect,
|
|
162
|
+
freeText,
|
|
163
|
+
options: freeText ? [] : q.options,
|
|
164
|
+
otherRow: !freeText,
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const freeText = request.kind === 'text' || request.choices.length === 0;
|
|
169
|
+
return [{
|
|
170
|
+
id: request.id,
|
|
171
|
+
question: request.question,
|
|
172
|
+
context: request.context,
|
|
173
|
+
header: request.id,
|
|
174
|
+
multiSelect: !freeText && request.kind === 'multi',
|
|
175
|
+
freeText,
|
|
176
|
+
options: freeText ? [] : request.choices,
|
|
177
|
+
otherRow: false,
|
|
178
|
+
defaultValue: request.defaultValue,
|
|
179
|
+
keys: request.keys,
|
|
180
|
+
}];
|
|
181
|
+
}
|
|
182
|
+
export function askCardHint(kind, opts = {}) {
|
|
183
|
+
const nav = opts.multiQuestion ? ['Tab next question'] : [];
|
|
184
|
+
switch (kind) {
|
|
185
|
+
case 'text': return ['Enter submits', ...nav, 'Esc cancels'].join(' · ');
|
|
186
|
+
case 'other': return ['Enter submits', ...nav, 'Esc goes back to the options'].join(' · ');
|
|
187
|
+
case 'multi':
|
|
188
|
+
// Kept under 78 cells (an 80-column card) so it never truncates; the
|
|
189
|
+
// "(choose one or more)" question suffix carries the multi-select cue.
|
|
190
|
+
return ['Space or 1-9 ticks a row', 'Enter confirms', ...nav, 'Esc cancels'].join(' · ');
|
|
191
|
+
default:
|
|
192
|
+
return ['↑↓ move', '1-9 or Enter picks', ...nav, 'Esc cancels'].join(' · ');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export const REVIEW_HINT = '↑↓ move · 1-2 or Enter chooses · ← back to edit · Esc cancels';
|
|
196
|
+
const REVIEW_CHOICES = ['Submit answers', 'Cancel'];
|
|
197
|
+
const MULTI_SUFFIX = ' (choose one or more)';
|
|
198
|
+
const REC = ' (recommended)';
|
|
199
|
+
/**
|
|
200
|
+
* Upper bound on the rows this card renders — the card's share of the
|
|
201
|
+
* live-area height budget (spec §2; liveBudget decides showDescriptions).
|
|
202
|
+
* Worst case over every question screen, plus the Review screen.
|
|
203
|
+
*/
|
|
204
|
+
export function estimateAskCardRows(request, showDescriptions, width = uiWidth()) {
|
|
205
|
+
const qs = toCardQuestions(request);
|
|
206
|
+
const chipRows = qs.length > 1 ? 2 : 0; // tab row + its blank line
|
|
207
|
+
const inner = Math.max(1, width - 2);
|
|
208
|
+
let worst = 0;
|
|
209
|
+
for (const q of qs) {
|
|
210
|
+
const ctx = contextRows(q.context, inner).length;
|
|
211
|
+
const suffix = q.multiSelect && inner - MULTI_SUFFIX.length >= 1 ? MULTI_SUFFIX : '';
|
|
212
|
+
const qRows = questionRows(q.question, suffix, inner).length;
|
|
213
|
+
let body = 2; // margin + the text input row
|
|
214
|
+
if (!q.freeText) {
|
|
215
|
+
const shown = Math.min(q.options.length, VISIBLE_OPTION_ROWS);
|
|
216
|
+
const described = showDescriptions ? Math.min(shown, q.options.filter((o) => o.description).length) : 0;
|
|
217
|
+
const arrows = q.options.length > VISIBLE_OPTION_ROWS ? 2 : 0;
|
|
218
|
+
body = 1 + shown + described + arrows + (q.otherRow ? 1 : 0);
|
|
219
|
+
}
|
|
220
|
+
worst = Math.max(worst, chipRows + qRows + ctx + body + 2); // question + body + margin + hint
|
|
221
|
+
}
|
|
222
|
+
if (qs.length > 1) {
|
|
223
|
+
worst = Math.max(worst, chipRows + 1 + qs.length + 1 + REVIEW_CHOICES.length + 2);
|
|
224
|
+
}
|
|
225
|
+
return worst;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Cut differently-styled segments of ONE row to `cells` in total: whole
|
|
229
|
+
* segments while they fit, then the one that overflows ends with the `more`
|
|
230
|
+
* glyph and the rest are dropped.
|
|
231
|
+
*/
|
|
232
|
+
function fitSegments(segs, cells) {
|
|
233
|
+
if (segs.reduce((w, sg) => w + stringWidth(sg.text), 0) <= cells)
|
|
234
|
+
return segs;
|
|
235
|
+
const out = [];
|
|
236
|
+
let used = 0;
|
|
237
|
+
for (const seg of segs) {
|
|
238
|
+
const room = cells - used;
|
|
239
|
+
const w = stringWidth(seg.text);
|
|
240
|
+
if (w < room) {
|
|
241
|
+
out.push(seg);
|
|
242
|
+
used += w;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// The row overflows, so the segment that reaches the edge must show the
|
|
246
|
+
// `more` glyph — the trailing space forces a cut even at an exact fit.
|
|
247
|
+
if (room > 0)
|
|
248
|
+
out.push({ ...seg, text: truncateToWidth(`${seg.text} `, room) });
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
export function AskCard({ request, width, showDescriptions, onResolveV1, onResolveForm, onInterruptTurn }) {
|
|
254
|
+
useReclaimRawStdin();
|
|
255
|
+
const g = getGlyphs();
|
|
256
|
+
const dim = roleProps('dim');
|
|
257
|
+
const isForm = 'questions' in request;
|
|
258
|
+
const formId = isForm ? request.formId : '';
|
|
259
|
+
const questions = useMemo(() => toCardQuestions(request), [request]);
|
|
260
|
+
const n = questions.length;
|
|
261
|
+
// Single-key answers (Q3 Yes/No) get a pre-open drop: a `y` typed ahead
|
|
262
|
+
// while output streamed must not answer the card.
|
|
263
|
+
const mountGuard = questions.some((qq) => qq.keys !== undefined);
|
|
264
|
+
const mountedAt = useRef(Date.now());
|
|
265
|
+
const done = useRef(false);
|
|
266
|
+
// Focus slots: 0..n-1 = questions; n = the Review step (2+ questions,
|
|
267
|
+
// reachable only once every question has an answer).
|
|
268
|
+
const [focus, setFocus] = useState(0);
|
|
269
|
+
const [cursors, setCursors] = useState(() => questions.map((qq) => Math.max(0, qq.options.findIndex((o) => o.value === qq.defaultValue))));
|
|
270
|
+
const [ticks, setTicks] = useState(() => questions.map(() => new Set()));
|
|
271
|
+
const [drafts, setDrafts] = useState(() => questions.map(() => ''));
|
|
272
|
+
const [answers, setAnswers] = useState(() => questions.map(() => null));
|
|
273
|
+
const [reviewCursor, setReviewCursor] = useState(0);
|
|
274
|
+
const isReview = n > 1 && focus === n;
|
|
275
|
+
const q = isReview ? undefined : questions[focus];
|
|
276
|
+
const cursor = q ? cursors[focus] : 0;
|
|
277
|
+
const rowCount = q ? q.options.length + (q.otherRow ? 1 : 0) : 0;
|
|
278
|
+
const onOther = q !== undefined && q.otherRow && cursor === q.options.length;
|
|
279
|
+
const inputFocused = q !== undefined && (q.freeText || onOther);
|
|
280
|
+
const allAnswered = answers.every((a) => a !== null);
|
|
281
|
+
const setCursor = (i) => setCursors((prev) => prev.map((c, j) => (j === focus ? i : c)));
|
|
282
|
+
const setDraft = (v) => setDrafts((prev) => prev.map((d, j) => (j === focus ? v : d)));
|
|
283
|
+
const toggle = (value) => setTicks((prev) => prev.map((s, j) => {
|
|
284
|
+
if (j !== focus)
|
|
285
|
+
return s;
|
|
286
|
+
const next = new Set(s);
|
|
287
|
+
if (next.has(value))
|
|
288
|
+
next.delete(value);
|
|
289
|
+
else
|
|
290
|
+
next.add(value);
|
|
291
|
+
return next;
|
|
292
|
+
}));
|
|
293
|
+
const cancel = () => {
|
|
294
|
+
if (done.current)
|
|
295
|
+
return;
|
|
296
|
+
done.current = true;
|
|
297
|
+
if (isForm)
|
|
298
|
+
onResolveForm({ formId, answers: [], cancelled: true });
|
|
299
|
+
else
|
|
300
|
+
onResolveV1({ answer: null, cancelled: true });
|
|
301
|
+
};
|
|
302
|
+
const submit = (final) => {
|
|
303
|
+
if (done.current)
|
|
304
|
+
return;
|
|
305
|
+
done.current = true;
|
|
306
|
+
if (!isForm) {
|
|
307
|
+
const a = final[0];
|
|
308
|
+
onResolveV1(a ? { answer: a.answer } : { answer: null, cancelled: true });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
onResolveForm({
|
|
312
|
+
formId,
|
|
313
|
+
// Question order, always — regardless of the order answered in.
|
|
314
|
+
answers: questions.map((qq, i) => ({
|
|
315
|
+
id: qq.id,
|
|
316
|
+
answer: final[i]?.answer ?? null,
|
|
317
|
+
...(final[i]?.custom ? { custom: true } : {}),
|
|
318
|
+
})),
|
|
319
|
+
});
|
|
320
|
+
};
|
|
321
|
+
/**
|
|
322
|
+
* Commit, then move to the next UNANSWERED question (wrapping). None left:
|
|
323
|
+
* one-question cards resolve at once; forms open the Review step.
|
|
324
|
+
*/
|
|
325
|
+
const answerAndAdvance = (i, value, custom) => {
|
|
326
|
+
const next = answers.map((a, j) => j === i ? { answer: value, ...(custom ? { custom: true } : {}) } : a);
|
|
327
|
+
// Fix round 3 (item 9c) — "Stop here" / Cancel ends the form now; the
|
|
328
|
+
// later questions come back null instead of being asked anyway.
|
|
329
|
+
const picked = custom ? undefined : questions[i]?.options.find((o) => o.value === value);
|
|
330
|
+
if (picked?.endsForm) {
|
|
331
|
+
submit(next);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
for (let step = 1; step <= n; step++) {
|
|
335
|
+
const j = (i + step) % n;
|
|
336
|
+
if (next[j] === null) {
|
|
337
|
+
setAnswers(next);
|
|
338
|
+
setFocus(j);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (n === 1) {
|
|
343
|
+
submit(next);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
setAnswers(next);
|
|
347
|
+
setReviewCursor(0);
|
|
348
|
+
setFocus(n);
|
|
349
|
+
};
|
|
350
|
+
const pickEnter = () => {
|
|
351
|
+
if (!q)
|
|
352
|
+
return;
|
|
353
|
+
const opt = q.options[cursor];
|
|
354
|
+
if (q.multiSelect) {
|
|
355
|
+
const values = q.options.filter((o) => ticks[focus].has(o.value)).map((o) => o.value);
|
|
356
|
+
if (values.length > 0)
|
|
357
|
+
answerAndAdvance(focus, values.join(','));
|
|
358
|
+
else if (opt)
|
|
359
|
+
answerAndAdvance(focus, opt.value); // nothing ticked → the highlighted row
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (opt)
|
|
363
|
+
answerAndAdvance(focus, opt.value);
|
|
364
|
+
};
|
|
365
|
+
const onTextSubmit = (v) => {
|
|
366
|
+
const trimmed = v.trim();
|
|
367
|
+
if (trimmed.length === 0) {
|
|
368
|
+
if (!isForm)
|
|
369
|
+
cancel(); // v1 text: Enter on empty cancels (today)
|
|
370
|
+
return; // v2: ignored (today)
|
|
371
|
+
}
|
|
372
|
+
// custom marks the free-text ESCAPE on an optioned question only.
|
|
373
|
+
answerAndAdvance(focus, trimmed, onOther);
|
|
374
|
+
};
|
|
375
|
+
useInput((input, key) => {
|
|
376
|
+
if (mountGuard && Date.now() - mountedAt.current < GUARD_MS)
|
|
377
|
+
return;
|
|
378
|
+
// Ctrl+C cancels outright, the result Esc gives when it cancels (same as
|
|
379
|
+
// ConfirmCard / FilesCard) — even from the text row, where Esc steps back.
|
|
380
|
+
// Round 7 item 4 — and then stops the turn (the model must not go on
|
|
381
|
+
// spending after the user pressed Ctrl+C).
|
|
382
|
+
if (key.ctrl && input === 'c') {
|
|
383
|
+
const first = !done.current;
|
|
384
|
+
cancel();
|
|
385
|
+
if (first)
|
|
386
|
+
onInterruptTurn?.();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (key.escape) {
|
|
390
|
+
if (q && onOther) {
|
|
391
|
+
setCursor(Math.max(0, q.options.length - 1));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
cancel();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (n > 1) {
|
|
398
|
+
const maxSlot = allAnswered ? n : n - 1;
|
|
399
|
+
if ((key.tab && !key.shift) || (!inputFocused && key.rightArrow)) {
|
|
400
|
+
setFocus((f) => (f >= maxSlot ? 0 : f + 1));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if ((key.tab && key.shift) || (!inputFocused && key.leftArrow)) {
|
|
404
|
+
setFocus((f) => (f <= 0 ? maxSlot : f - 1));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (isReview) {
|
|
409
|
+
if (key.upArrow) {
|
|
410
|
+
setReviewCursor(0);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (key.downArrow) {
|
|
414
|
+
setReviewCursor(1);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (input === '1' || (key.return && reviewCursor === 0)) {
|
|
418
|
+
submit(answers);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (input === '2' || (key.return && reviewCursor === 1)) {
|
|
422
|
+
cancel();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (!q)
|
|
428
|
+
return;
|
|
429
|
+
if (!q.freeText) {
|
|
430
|
+
if (key.upArrow) {
|
|
431
|
+
setCursor(Math.max(0, cursor - 1));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (key.downArrow) {
|
|
435
|
+
setCursor(Math.min(rowCount - 1, cursor + 1));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// The text input owns every other key while it has focus.
|
|
440
|
+
if (inputFocused)
|
|
441
|
+
return;
|
|
442
|
+
// Ctrl+Y / Meta+Y are not "y": only a bare key answers.
|
|
443
|
+
const alias = key.ctrl || key.meta ? undefined : q.keys?.[input.toLowerCase()];
|
|
444
|
+
if (alias !== undefined && q.options.some((o) => o.value === alias)) {
|
|
445
|
+
answerAndAdvance(focus, alias);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const digit = /^[1-9]$/.test(input) ? Number(input) - 1 : -1;
|
|
449
|
+
if (digit >= 0) {
|
|
450
|
+
if (digit >= rowCount)
|
|
451
|
+
return;
|
|
452
|
+
if (q.otherRow && digit === q.options.length) {
|
|
453
|
+
setCursor(digit);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const opt = q.options[digit];
|
|
457
|
+
if (q.multiSelect) {
|
|
458
|
+
toggle(opt.value);
|
|
459
|
+
setCursor(digit);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
answerAndAdvance(focus, opt.value);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (q.multiSelect && input === ' ') {
|
|
466
|
+
const opt = q.options[cursor];
|
|
467
|
+
if (opt)
|
|
468
|
+
toggle(opt.value);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (key.return)
|
|
472
|
+
pickEnter();
|
|
473
|
+
}, { isActive: true });
|
|
474
|
+
const numbered = (i) => `${i + 1}. `;
|
|
475
|
+
const inner = Math.max(1, width - 2); // paddingX={1} on both sides
|
|
476
|
+
const chipRow = n > 1 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { wrap: "truncate-end", children: fitSegments([
|
|
477
|
+
...questions.map((qq, i) => ({
|
|
478
|
+
text: `${i > 0 ? ' ' : ''}${answers[i] !== null ? g.answered : g.pending} ${fit(qq.header, inner)}`,
|
|
479
|
+
props: i === focus ? selectedProps() : dim,
|
|
480
|
+
})),
|
|
481
|
+
{ text: ` ${g.done} Submit`, props: isReview ? selectedProps() : dim },
|
|
482
|
+
], inner).map((sg, i) => _jsx(Text, { ...sg.props, children: sg.text }, `chip-${i}`)) }) })) : null;
|
|
483
|
+
if (!q) {
|
|
484
|
+
// Review step (forms with 2+ questions, all answered).
|
|
485
|
+
const headerCol = Math.min(14, Math.max(...questions.map((qq) => stringWidth(fit(qq.header, inner)))) + 2);
|
|
486
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, paddingX: 1, flexShrink: 0, children: [chipRow, _jsx(Text, { bold: true, wrap: "truncate-end", children: fit('Review your answers', inner) }), questions.map((qq, i) => {
|
|
487
|
+
const a = answers[i];
|
|
488
|
+
const head = fit(qq.header, headerCol - 2);
|
|
489
|
+
const cell = ` ${head}${' '.repeat(Math.max(0, headerCol - stringWidth(head)))}`;
|
|
490
|
+
const shown = a ? answerDisplayText(a.answer, qq.options, a.custom === true) : '';
|
|
491
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...dim, children: fit(cell, inner) }), fit(shown, Math.max(0, inner - stringWidth(cell)))] }, `rev-${i}`));
|
|
492
|
+
}), _jsx(Box, { flexDirection: "column", marginTop: 1, children: REVIEW_CHOICES.map((label, i) => {
|
|
493
|
+
const cur = i === reviewCursor;
|
|
494
|
+
return (_jsx(Text, { wrap: "truncate-end", ...(cur ? selectedProps() : {}), children: fit(`${cur ? `${g.cursor} ` : ' '}${numbered(i)}${label}`, inner) }, `rc-${i}`));
|
|
495
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { ...dim, wrap: "truncate-end", children: fit(REVIEW_HINT, inner) }) })] }));
|
|
496
|
+
}
|
|
497
|
+
const winStart = Math.max(0, Math.min(cursor - 3, q.options.length - VISIBLE_OPTION_ROWS));
|
|
498
|
+
const visible = q.options.slice(winStart, winStart + VISIBLE_OPTION_ROWS);
|
|
499
|
+
const below = Math.max(0, q.options.length - (winStart + VISIBLE_OPTION_ROWS));
|
|
500
|
+
const inputWidth = Math.max(8, width - 2 - 2 - `${rowCount}. `.length - 1);
|
|
501
|
+
const contextLines = contextRows(q.context, inner);
|
|
502
|
+
const hintKind = q.freeText ? 'text' : onOther ? 'other' : q.multiSelect ? 'multi' : 'pick';
|
|
503
|
+
// The multi-select suffix is the card's only multi cue: it stays whole and
|
|
504
|
+
// the question is cut instead (the same rule as "(recommended)" below).
|
|
505
|
+
const suffix = q.multiSelect && inner - MULTI_SUFFIX.length >= 1 ? MULTI_SUFFIX : '';
|
|
506
|
+
const questionLines = questionRows(q.question, suffix, inner);
|
|
507
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, paddingX: 1, flexShrink: 0, 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
|
+
const idx = winStart + vi;
|
|
509
|
+
const cur = !onOther && idx === cursor;
|
|
510
|
+
const rec = o.recommended === true && !/\brecommended\b/i.test(o.label);
|
|
511
|
+
const tick = q.multiSelect ? `${ticks[focus].has(o.value) ? g.tickOn : g.tickOff} ` : '';
|
|
512
|
+
const prefix = `${cur ? `${g.cursor} ` : ' '}${numbered(idx)}${tick}`;
|
|
513
|
+
const avail = inner - stringWidth(prefix);
|
|
514
|
+
const label = sanitize(o.label);
|
|
515
|
+
// (recommended) stays whole: the label is cut first; with no room
|
|
516
|
+
// for both, the label wins and the dim suffix is dropped.
|
|
517
|
+
const showRec = rec && avail - REC.length >= 1;
|
|
518
|
+
const shownLabel = truncateToWidth(label, showRec ? avail - REC.length : avail);
|
|
519
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", ...(cur ? selectedProps() : {}), children: [prefix, shownLabel, showRec ? _jsx(Text, { ...dim, children: REC }) : null, q.defaultValue !== undefined && o.value === q.defaultValue ? _jsx(Text, { ...dim, children: " (default)" }) : null] }), showDescriptions && o.description ? (_jsx(Text, { ...dim, wrap: "truncate-end", children: fit(` ${o.description}`, inner) })) : null] }, `opt-${idx}`));
|
|
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) }) })] }));
|
|
521
|
+
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { roleProps } from '../role-props.js';
|
|
4
|
+
// The model names a colour; the bar takes the matching theme role, so it follows
|
|
5
|
+
// NO_COLOR and the light palette. White / unknown → the terminal's own text colour.
|
|
6
|
+
const COLOR_ROLE = {
|
|
7
|
+
red: 'bad', yellow: 'warn', green: 'ok', cyan: 'sap', blue: 'sap', magenta: 'brand', gray: 'dim',
|
|
8
|
+
};
|
|
9
|
+
function barProps(c) {
|
|
10
|
+
const role = c ? COLOR_ROLE[c] : undefined;
|
|
11
|
+
return role ? roleProps(role) : {};
|
|
6
12
|
}
|
|
7
13
|
export function BarChart({ data }) {
|
|
8
14
|
const max = Math.max(...data.map((d) => d.value), 1);
|
|
9
15
|
const barWidth = 30;
|
|
10
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor:
|
|
16
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: roleProps('frame').color, paddingX: 1, children: [_jsx(Text, { ...roleProps('sap', { bold: true }), children: "Distribution" }), data.map((item) => {
|
|
11
17
|
const filled = Math.round((item.value / max) * barWidth);
|
|
12
18
|
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
13
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { children: [item.label.padEnd(28), " "] }), _jsx(Text, {
|
|
19
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { children: [item.label.padEnd(28), " "] }), _jsx(Text, { ...barProps(item.color), children: bar }), _jsxs(Text, { children: [" ", item.value] })] }, item.label));
|
|
14
20
|
})] }));
|
|
15
21
|
}
|