@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,45 +1,51 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import stringWidth from 'string-width';
|
|
4
5
|
import { PICK_CANCELLED } from '../skill-picker.js';
|
|
5
|
-
|
|
6
|
+
import { getGlyphs } from '../../renderer/glyphs.js';
|
|
7
|
+
import { roleProps, selectedProps } from '../role-props.js';
|
|
8
|
+
import { truncateToWidth, uiWidth } from '../../renderer/ui-width.js';
|
|
9
|
+
import { sanitizeControlChars } from '../../renderer/sanitize.js';
|
|
10
|
+
export const COACHING_HINT = '↑↓ move · 1-9/Enter picks · Enter with nothing selected cancels · Esc cancels';
|
|
6
11
|
/**
|
|
7
|
-
*
|
|
12
|
+
* Sanitize, then cut to `cells` terminal cells (same rule as the ask card):
|
|
13
|
+
* the prompt is user text and must not drive the terminal; rows are pre-cut
|
|
14
|
+
* with string-width and Ink's truncate-end is only the safety net.
|
|
15
|
+
*/
|
|
16
|
+
function fit(text, cells) {
|
|
17
|
+
return truncateToWidth(sanitizeControlChars(text), Math.max(0, cells));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Ink-mode skill picker on the AskCard look (piece 2 / Q4, spec §3).
|
|
21
|
+
*
|
|
22
|
+
* Pick a skill for "What is the best part…?"
|
|
8
23
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* once mounted inline between Body and Footer, and Ink overflow-
|
|
13
|
-
* clipped only the BOTTOM line of each option leaving a garbled
|
|
14
|
-
* picker (screenshot 71). This rewrite uses one line per option +
|
|
15
|
-
* a "details" line only for the highlighted entry, total height
|
|
16
|
-
* bounded to ~8 lines regardless of option count.
|
|
24
|
+
* ❯ 1. /abap-explain Plain-language code explainer
|
|
25
|
+
* Pick if: pick this to teach code to a human
|
|
26
|
+
* 2. /abap-review Code review vs quality criteria
|
|
17
27
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* │ ▸ /abap-explain — Pick if: pick this to teach code to a human │
|
|
25
|
-
* │ ↑/↓ navigate · Enter select · Esc cancel │
|
|
26
|
-
* ╰──────────────────────────────────────────────────────────╯
|
|
28
|
+
* ↑↓ move · 1-9/Enter picks · Enter with nothing selected cancels · Esc cancels
|
|
29
|
+
*
|
|
30
|
+
* INVARIANT (spec, pinned): with nothing highlighted — preSelect false, or
|
|
31
|
+
* ↑ past the first row — Enter resolves PICK_CANCELLED. Starting cursor:
|
|
32
|
+
* preferSkill (if listed) → first row when preSelect → nothing. The
|
|
33
|
+
* confidence percentage is gone; results are unchanged (skill | PICK_CANCELLED).
|
|
27
34
|
*/
|
|
28
|
-
export function CoachingPicker({ params, onResolve }) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
35
|
+
export function CoachingPicker({ params, onResolve, width }) {
|
|
36
|
+
const g = getGlyphs();
|
|
37
|
+
const dim = roleProps('dim');
|
|
38
|
+
const w = width ?? uiWidth();
|
|
39
|
+
// Content width: card width minus paddingX={1} on both sides.
|
|
40
|
+
const inner = Math.max(1, w - 2);
|
|
34
41
|
const preferIdx = params.preferSkill
|
|
35
42
|
? params.options.findIndex((o) => o.skill === params.preferSkill)
|
|
36
43
|
: -1;
|
|
37
|
-
const initialCursor = preferIdx >= 0
|
|
38
|
-
? preferIdx
|
|
39
|
-
: params.preSelect ? 0 : -1;
|
|
44
|
+
const initialCursor = preferIdx >= 0 ? preferIdx : params.preSelect ? 0 : -1;
|
|
40
45
|
const [cursor, setCursor] = useState(initialCursor);
|
|
41
|
-
useInput((
|
|
42
|
-
|
|
46
|
+
useInput((input, key) => {
|
|
47
|
+
// Ctrl+C cancels like Esc (same as ConfirmCard / FilesCard).
|
|
48
|
+
if (key.escape || (key.ctrl && input === 'c')) {
|
|
43
49
|
onResolve(PICK_CANCELLED);
|
|
44
50
|
return;
|
|
45
51
|
}
|
|
@@ -51,6 +57,13 @@ export function CoachingPicker({ params, onResolve }) {
|
|
|
51
57
|
setCursor((c) => Math.min(params.options.length - 1, c + 1));
|
|
52
58
|
return;
|
|
53
59
|
}
|
|
60
|
+
const digit = /^[1-9]$/.test(input) ? Number(input) - 1 : -1;
|
|
61
|
+
if (digit >= 0) {
|
|
62
|
+
const picked = params.options[digit];
|
|
63
|
+
if (picked)
|
|
64
|
+
onResolve(picked.skill);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
54
67
|
if (key.return) {
|
|
55
68
|
if (cursor < 0) {
|
|
56
69
|
onResolve(PICK_CANCELLED);
|
|
@@ -61,28 +74,10 @@ export function CoachingPicker({ params, onResolve }) {
|
|
|
61
74
|
onResolve(chosen.skill);
|
|
62
75
|
}
|
|
63
76
|
}, { isActive: true });
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// flexShrink: 1 lets the picker get squashed when vertical space is
|
|
71
|
-
// tight, and the TOP options clip off (only the bottom one remained
|
|
72
|
-
// visible — see screenshot 74). flexShrink={0} guarantees the picker
|
|
73
|
-
// claims its natural height; Body shrinks to make room (and Body
|
|
74
|
-
// content stays recoverable via PgUp/Ctrl+B scroll).
|
|
75
|
-
_jsxs(Box, { borderStyle: "round", borderColor: PEACH, flexDirection: "column", paddingX: 1, flexShrink: 0, children: [_jsxs(Text, { bold: true, children: ["Pick a skill for: ", _jsxs(Text, { color: "cyan", children: ["\"", promptPreview, "\""] })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: params.options.map((opt, i) => {
|
|
76
|
-
const isCursor = i === cursor;
|
|
77
|
-
const arrow = isCursor ? '▸ ' : ' ';
|
|
78
|
-
// 2026-05-17 — hide the "0%" cruft. Confidence is 0 for any
|
|
79
|
-
// alternate / fallback option (we never had a real score for
|
|
80
|
-
// them), AND for top-1 picks where the classifier itself
|
|
81
|
-
// returned 0. In both cases displaying "0%" looks broken.
|
|
82
|
-
// Show "%" only when there's a meaningful number.
|
|
83
|
-
const pctNum = Math.round(opt.confidence * 100);
|
|
84
|
-
const pct = pctNum > 0 ? `${pctNum.toString().padStart(3, ' ')}%` : ' ';
|
|
85
|
-
const name = `/${opt.skill}`.padEnd(20, ' ');
|
|
86
|
-
return (_jsxs(Text, { color: isCursor ? PEACH : undefined, bold: isCursor, children: [arrow, name, " ", pct, " ", opt.whatItDoes.slice(0, 60)] }, opt.skill));
|
|
87
|
-
}) }), highlighted && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Pick if: ", highlighted.whenToPick] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel" }) })] }));
|
|
77
|
+
const nameWidth = Math.max(...params.options.map((o) => o.skill.length + 1)) + 2;
|
|
78
|
+
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
|
+
const cur = i === cursor;
|
|
80
|
+
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 ? (_jsx(Text, { ...dim, wrap: "truncate-end", children: fit(` Pick if: ${opt.whenToPick}`, inner) })) : null] }, opt.skill));
|
|
82
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { ...dim, wrap: "truncate-end", children: fit(COACHING_HINT, inner) }) })] }));
|
|
88
83
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// src/ui/widgets/confirm-card.tsx
|
|
3
|
+
//
|
|
4
|
+
// Piece 2 §5 — the one confirm card (APPROVAL / SAFETY CHECK / CANNOT BE
|
|
5
|
+
// UNDONE, plus the ADVISORY and QUESTION confirms). Tag chip on the top edge,
|
|
6
|
+
// bold title, dim facts, numbered choices, dim footnote on the bottom edge.
|
|
7
|
+
//
|
|
8
|
+
// Keys (deliberately NOT the question-card keys):
|
|
9
|
+
// ↑/↓ move the cursor. Digits 1–9 pick on APPROVAL / QUESTION / ADVISORY
|
|
10
|
+
// cards; on SAFETY CHECK and CANNOT BE UNDONE cards they only move it.
|
|
11
|
+
// Enter confirms the highlighted choice.
|
|
12
|
+
// Esc / Ctrl+C → { value: null, cancelled: true }.
|
|
13
|
+
// request.aliases (e.g. y/n/d, or typed words) resolve immediately.
|
|
14
|
+
// Anything received in the first CONFIRM_INPUT_GUARD_MS after mount is
|
|
15
|
+
// dropped: a `1` or Enter typed before the card opened can never apply.
|
|
16
|
+
import { useRef, useState } from 'react';
|
|
17
|
+
import { Box, Text, useInput } from 'ink';
|
|
18
|
+
import stringWidth from 'string-width';
|
|
19
|
+
import { roleProps, selectedProps } from '../role-props.js';
|
|
20
|
+
import { getBoxChars } from '../ascii-chars.js';
|
|
21
|
+
import { getGlyphs } from '../../renderer/glyphs.js';
|
|
22
|
+
import { truncateToWidth } from '../../renderer/ui-width.js';
|
|
23
|
+
import { sanitizeControlChars } from '../../renderer/sanitize.js';
|
|
24
|
+
// Q1b's shared hook — the one raw-stdin reclaim for every card (no second copy).
|
|
25
|
+
import { useReclaimRawStdin } from './reclaim-raw-stdin.js';
|
|
26
|
+
export const CONFIRM_INPUT_GUARD_MS = 150;
|
|
27
|
+
/**
|
|
28
|
+
* The card's full rendered height for the live-area budget (F3 liveBudget
|
|
29
|
+
* `cardRows`): top edge + title + one row per fact + the gap above the
|
|
30
|
+
* choices + one row per choice + the hint + bottom edge. Every row is
|
|
31
|
+
* truncate-end, so nothing wraps into extra rows.
|
|
32
|
+
*/
|
|
33
|
+
export function estimateConfirmCardRows(req) {
|
|
34
|
+
return 5 + (req.facts?.length ?? 0) + req.choices.length;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Owner decision (fix-round-3 deferred M1): a digit PICKS only on ordinary
|
|
38
|
+
* cards (APPROVAL, QUESTION, ADVISORY). A SAFETY CHECK or CANNOT BE UNDONE
|
|
39
|
+
* card needs move-then-Enter — a digit only moves the cursor there — so one
|
|
40
|
+
* stray key can never pass a safety gate or release a transport. An
|
|
41
|
+
* allow-list, so any tag added later fails safe (move + Enter) until it is
|
|
42
|
+
* deliberately listed here.
|
|
43
|
+
*/
|
|
44
|
+
const DIGIT_PICK_TAGS = new Set(['APPROVAL', 'QUESTION', 'ADVISORY']);
|
|
45
|
+
export function digitsPickOn(tag) {
|
|
46
|
+
return DIGIT_PICK_TAGS.has(tag);
|
|
47
|
+
}
|
|
48
|
+
export const TAG_ROLE = {
|
|
49
|
+
APPROVAL: 'sap',
|
|
50
|
+
'SAFETY CHECK': 'warn',
|
|
51
|
+
'CANNOT BE UNDONE': 'bad',
|
|
52
|
+
ADVISORY: 'warn',
|
|
53
|
+
QUESTION: 'sap',
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* One edge line in three segments so the label can take its own colour.
|
|
57
|
+
* lead + label + tail is exactly `width` cells whenever width ≥ 7 (the
|
|
58
|
+
* label is cut to width − 6 cells with …; wide characters count two).
|
|
59
|
+
*/
|
|
60
|
+
export function edgeSegments(corners, h, label, width) {
|
|
61
|
+
const [left, right] = corners;
|
|
62
|
+
if (!label)
|
|
63
|
+
return { lead: left + h.repeat(Math.max(0, width - 2)), label: '', tail: right };
|
|
64
|
+
const shown = truncateToWidth(label, Math.max(1, width - 6));
|
|
65
|
+
// Cells, not code points: a wide-character label must not widen the edge.
|
|
66
|
+
const fill = Math.max(1, width - 5 - stringWidth(shown));
|
|
67
|
+
return { lead: `${left}${h} `, label: shown, tail: ` ${h.repeat(fill)}${right}` };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Alias matching: a single key resolves at once; a word (e.g. 'abort') is
|
|
71
|
+
* collected letter by letter and resolves on its last letter; a pasted word
|
|
72
|
+
* resolves whole. Digits never reach here (they move the cursor).
|
|
73
|
+
*/
|
|
74
|
+
export function matchAlias(aliases, input, word) {
|
|
75
|
+
if (!aliases || input.length === 0)
|
|
76
|
+
return undefined;
|
|
77
|
+
const k = input.toLowerCase();
|
|
78
|
+
if (aliases[k] !== undefined) {
|
|
79
|
+
word.current = '';
|
|
80
|
+
return aliases[k];
|
|
81
|
+
}
|
|
82
|
+
const next = word.current + k;
|
|
83
|
+
if (aliases[next] !== undefined) {
|
|
84
|
+
word.current = '';
|
|
85
|
+
return aliases[next];
|
|
86
|
+
}
|
|
87
|
+
word.current = Object.keys(aliases).some((a) => a.startsWith(next)) ? next : '';
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Card text can carry model-supplied words (the plan summary). Neutralise
|
|
92
|
+
* control characters, then pre-cut to `cells` with string-width: Ink's own
|
|
93
|
+
* truncate-end miscounts some emoji, so it is only the safety net here.
|
|
94
|
+
*/
|
|
95
|
+
function fit(text, cells) {
|
|
96
|
+
return truncateToWidth(sanitizeControlChars(text), cells);
|
|
97
|
+
}
|
|
98
|
+
const inkBorder = (b) => ({
|
|
99
|
+
topLeft: b.tl, top: b.h, topRight: b.tr, right: b.v,
|
|
100
|
+
bottomRight: b.br, bottom: b.h, bottomLeft: b.bl, left: b.v,
|
|
101
|
+
});
|
|
102
|
+
/** The frame every card in the slot shares: tag chip top edge, footnote bottom edge. */
|
|
103
|
+
export function CardFrame({ tag, footnote, width, children }) {
|
|
104
|
+
const box = getBoxChars();
|
|
105
|
+
const frame = roleProps('frame');
|
|
106
|
+
const top = edgeSegments([box.tl, box.tr], box.h, tag, width);
|
|
107
|
+
const bottom = edgeSegments([box.bl, box.br], box.h, footnote, width);
|
|
108
|
+
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: top.label }), _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 })] })] }));
|
|
109
|
+
}
|
|
110
|
+
export function ConfirmCard({ request, width, onResolve, onInterruptTurn }) {
|
|
111
|
+
// Re-assert raw stdin on mount: an earlier inquirer prompt can leave Ink's
|
|
112
|
+
// raw-mode bookkeeping stale, and the card would then get no keys.
|
|
113
|
+
useReclaimRawStdin();
|
|
114
|
+
const g = getGlyphs();
|
|
115
|
+
const last = Math.max(0, request.choices.length - 1);
|
|
116
|
+
const [cursor, setCursor] = useState(() => Math.min(Math.max(request.initial ?? 0, 0), last));
|
|
117
|
+
const mountedAt = useRef(Date.now());
|
|
118
|
+
const done = useRef(false);
|
|
119
|
+
const word = useRef('');
|
|
120
|
+
const digitsPick = digitsPickOn(request.tag);
|
|
121
|
+
const finish = (r) => {
|
|
122
|
+
if (done.current)
|
|
123
|
+
return;
|
|
124
|
+
done.current = true;
|
|
125
|
+
onResolve(r);
|
|
126
|
+
};
|
|
127
|
+
useInput((input, key) => {
|
|
128
|
+
if (done.current)
|
|
129
|
+
return;
|
|
130
|
+
// Round 8 item 1 — Ctrl+C is never an answer, so the mount guard does not
|
|
131
|
+
// swallow it: it cancels the card and stops the turn.
|
|
132
|
+
if (key.ctrl && input === 'c') {
|
|
133
|
+
finish({ value: null, cancelled: true });
|
|
134
|
+
onInterruptTurn?.();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (Date.now() - mountedAt.current < CONFIRM_INPUT_GUARD_MS)
|
|
138
|
+
return;
|
|
139
|
+
if (key.escape) {
|
|
140
|
+
finish({ value: null, cancelled: true });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (key.upArrow) {
|
|
144
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (key.downArrow) {
|
|
148
|
+
setCursor((c) => Math.min(last, c + 1));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (key.return) {
|
|
152
|
+
const chosen = request.choices[cursor];
|
|
153
|
+
if (chosen)
|
|
154
|
+
finish({ value: chosen.value });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (/^[1-9]$/.test(input)) {
|
|
158
|
+
const i = Number(input) - 1;
|
|
159
|
+
if (i > last)
|
|
160
|
+
return;
|
|
161
|
+
// Fix round 3 (item 9b): a digit PICKS, the same as on question cards
|
|
162
|
+
// ("1" sat unanswered for 7 minutes live). SAFETY CHECK and CANNOT BE
|
|
163
|
+
// UNDONE cards keep move-then-Enter (see digitsPickOn).
|
|
164
|
+
if (digitsPick)
|
|
165
|
+
finish({ value: request.choices[i].value });
|
|
166
|
+
else
|
|
167
|
+
setCursor(i);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (key.ctrl || key.meta)
|
|
171
|
+
return;
|
|
172
|
+
const hit = matchAlias(request.aliases, input, word);
|
|
173
|
+
// No new values: an alias only answers with a choice the card offers.
|
|
174
|
+
if (hit !== undefined && request.choices.some((c) => c.value === hit))
|
|
175
|
+
finish({ value: hit });
|
|
176
|
+
});
|
|
177
|
+
const inner = Math.max(1, width - 4);
|
|
178
|
+
const dim = roleProps('dim');
|
|
179
|
+
const titleProps = request.tag === 'CANNOT BE UNDONE' ? roleProps('bad', { bold: true }) : { bold: true };
|
|
180
|
+
const letters = Object.keys(request.aliases ?? {}).filter((k) => k.length === 1);
|
|
181
|
+
const digits = Math.min(9, request.choices.length);
|
|
182
|
+
const hint = (digitsPick
|
|
183
|
+
? `1-${digits} picks · ↑↓ move · Enter chooses · Esc cancels`
|
|
184
|
+
: `Enter chooses · Esc cancels · ↑↓ or 1-${digits} move`)
|
|
185
|
+
+ (letters.length > 0 ? ` · keys ${letters.join('/')}` : '');
|
|
186
|
+
return (_jsxs(CardFrame, { tag: request.tag, footnote: request.footnote, width: width, children: [_jsx(Text, { wrap: "truncate-end", ...titleProps, children: fit(request.title, inner) }), (request.facts ?? []).map((f, i) => (_jsx(Text, { wrap: "truncate-end", ...dim, children: fit(f, inner) }, `fact-${i}`))), _jsx(Box, { flexDirection: "column", marginTop: 1, children: request.choices.map((c, i) => {
|
|
187
|
+
const on = i === cursor;
|
|
188
|
+
const main = fit(`${on ? g.cursor : ' '} ${i + 1}. ${c.label}`, inner);
|
|
189
|
+
const room = inner - stringWidth(main);
|
|
190
|
+
const hintText = c.hint && room > 2 ? fit(` ${c.hint}`, room) : '';
|
|
191
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...(on ? selectedProps() : {}), children: main }), hintText ? _jsx(Text, { ...dim, children: hintText }) : null] }, c.value));
|
|
192
|
+
}) }), _jsx(Text, { wrap: "truncate-end", ...dim, children: fit(hint, inner) })] }));
|
|
193
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { roleProps } from '../role-props.js';
|
|
3
4
|
export function DepGraph({ data }) {
|
|
4
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor:
|
|
5
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: roleProps('frame').color, paddingX: 1, children: [_jsxs(Text, { ...roleProps('sap', { bold: true }), children: ["Dependencies \u2014 ", data.root] }), data.children.map((c, i) => {
|
|
5
6
|
const isLast = i === data.children.length - 1;
|
|
6
7
|
const prefix = isLast ? '└─' : '├─';
|
|
7
|
-
return (_jsxs(Box, { children: [_jsxs(Text, {
|
|
8
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { ...roleProps('dim'), children: [prefix, " "] }), _jsx(Text, { ...roleProps('warn'), children: c.name }), _jsxs(Text, { ...roleProps('dim'), children: [" (", c.type, ") "] }), _jsx(Text, { dimColor: true, children: c.relation })] }, c.name));
|
|
8
9
|
})] }));
|
|
9
10
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { roleProps } from '../role-props.js';
|
|
3
4
|
export function DiffViewer({ data }) {
|
|
4
5
|
const lines = data.unified.split('\n');
|
|
5
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor:
|
|
6
|
-
const
|
|
7
|
-
line.startsWith('-') ? '
|
|
8
|
-
line.startsWith('@@') ? '
|
|
9
|
-
return (_jsx(Text, {
|
|
6
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: roleProps('frame').color, paddingX: 1, children: [_jsx(Text, { ...roleProps('dim', { bold: true }), children: "Diff" }), lines.map((line, idx) => {
|
|
7
|
+
const role = line.startsWith('+') ? 'ok' :
|
|
8
|
+
line.startsWith('-') ? 'bad' :
|
|
9
|
+
line.startsWith('@@') ? 'sap' : 'dim';
|
|
10
|
+
return (_jsx(Text, { ...roleProps(role, { bold: line.startsWith('@@') }), children: line }, idx));
|
|
10
11
|
})] }));
|
|
11
12
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// src/ui/widgets/files-card.tsx
|
|
3
|
+
//
|
|
4
|
+
// Piece 2 §6.4 — the `/files` picker as a card-slot client under Ink (was an
|
|
5
|
+
// inquirer `search` drawn over the Ink frame). Type to filter by file name
|
|
6
|
+
// (same match as today), ↑/↓ move, Enter picks, Esc/Ctrl+C cancel. Typed-ahead
|
|
7
|
+
// input in the first CONFIRM_INPUT_GUARD_MS is dropped, like every card.
|
|
8
|
+
// The list is bounded to FILES_CARD_ROWS rows (spec §2 height cap).
|
|
9
|
+
import { useRef, useState } from 'react';
|
|
10
|
+
import { Text, useInput } from 'ink';
|
|
11
|
+
import stringWidth from 'string-width';
|
|
12
|
+
import { CardFrame, CONFIRM_INPUT_GUARD_MS } from './confirm-card.js';
|
|
13
|
+
import { roleProps, selectedProps } from '../role-props.js';
|
|
14
|
+
import { getGlyphs } from '../../renderer/glyphs.js';
|
|
15
|
+
import { truncateToWidth } from '../../renderer/ui-width.js';
|
|
16
|
+
import { sanitizeControlChars } from '../../renderer/sanitize.js';
|
|
17
|
+
import { useReclaimRawStdin } from './reclaim-raw-stdin.js';
|
|
18
|
+
export const FILES_CARD_ROWS = 8;
|
|
19
|
+
export function filterFileItems(items, query) {
|
|
20
|
+
const q = query.toLowerCase();
|
|
21
|
+
return q.length === 0 ? items : items.filter((i) => i.value.toLowerCase().includes(q));
|
|
22
|
+
}
|
|
23
|
+
/** File names come from disk: neutralise control characters, pre-cut by cells. */
|
|
24
|
+
function fit(text, cells) {
|
|
25
|
+
return truncateToWidth(sanitizeControlChars(text), Math.max(1, cells));
|
|
26
|
+
}
|
|
27
|
+
export function FilesCard({ request, width, onResolve }) {
|
|
28
|
+
// Same raw-stdin reclaim as every card (an earlier inquirer prompt can leave
|
|
29
|
+
// Ink's raw-mode bookkeeping stale).
|
|
30
|
+
useReclaimRawStdin();
|
|
31
|
+
const g = getGlyphs();
|
|
32
|
+
const [query, setQuery] = useState('');
|
|
33
|
+
const [cursor, setCursor] = useState(0);
|
|
34
|
+
const mountedAt = useRef(Date.now());
|
|
35
|
+
const done = useRef(false);
|
|
36
|
+
const shown = filterFileItems(request.items, query);
|
|
37
|
+
const finish = (r) => {
|
|
38
|
+
if (done.current)
|
|
39
|
+
return;
|
|
40
|
+
done.current = true;
|
|
41
|
+
onResolve(r);
|
|
42
|
+
};
|
|
43
|
+
useInput((input, key) => {
|
|
44
|
+
if (done.current)
|
|
45
|
+
return;
|
|
46
|
+
if (Date.now() - mountedAt.current < CONFIRM_INPUT_GUARD_MS)
|
|
47
|
+
return;
|
|
48
|
+
if (key.escape || (key.ctrl && input === 'c')) {
|
|
49
|
+
finish({ value: null, cancelled: true });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (key.upArrow) {
|
|
53
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (key.downArrow) {
|
|
57
|
+
setCursor((c) => Math.min(Math.max(0, shown.length - 1), c + 1));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (key.return) {
|
|
61
|
+
const it = shown[cursor];
|
|
62
|
+
if (it)
|
|
63
|
+
finish({ value: it.value });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (key.backspace || key.delete) {
|
|
67
|
+
setQuery((q) => q.slice(0, -1));
|
|
68
|
+
setCursor(0);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (key.ctrl || key.meta || key.tab)
|
|
72
|
+
return;
|
|
73
|
+
// eslint-disable-next-line no-control-regex
|
|
74
|
+
const printable = input.replace(/[\u0000-\u001f\u007f]/g, '');
|
|
75
|
+
if (printable.length > 0) {
|
|
76
|
+
setQuery((q) => q + printable);
|
|
77
|
+
setCursor(0);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
const start = cursor < FILES_CARD_ROWS ? 0 : cursor - FILES_CARD_ROWS + 1;
|
|
81
|
+
const visible = shown.slice(start, start + FILES_CARD_ROWS);
|
|
82
|
+
const hidden = shown.length - start - visible.length;
|
|
83
|
+
const inner = Math.max(1, width - 4);
|
|
84
|
+
const dim = roleProps('dim');
|
|
85
|
+
const searchLabel = 'Search ';
|
|
86
|
+
// A long query is cut by cells (with …) so the row never wraps.
|
|
87
|
+
const queryShown = fit(query, inner - stringWidth(searchLabel) - 1);
|
|
88
|
+
return (_jsxs(CardFrame, { tag: "QUESTION", footnote: "/files", width: width, children: [_jsx(Text, { bold: true, wrap: "truncate-end", children: fit(request.title, inner) }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...dim, children: searchLabel }), _jsx(Text, { children: queryShown }), _jsx(Text, { ...roleProps('sap'), children: "_" })] }), visible.length === 0
|
|
89
|
+
? _jsx(Text, { ...dim, children: "No file matches" })
|
|
90
|
+
: visible.map((it, i) => {
|
|
91
|
+
const on = start + i === cursor;
|
|
92
|
+
const main = fit(`${on ? g.cursor : ' '} ${it.label}`, inner);
|
|
93
|
+
const room = inner - stringWidth(main);
|
|
94
|
+
const detail = it.detail && room > 2 ? fit(` ${g.info} ${it.detail}`, room) : '';
|
|
95
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...(on ? selectedProps() : {}), children: main }), detail ? _jsx(Text, { ...dim, children: detail }) : null] }, it.value));
|
|
96
|
+
}), hidden > 0 ? _jsx(Text, { ...dim, children: `${g.more} ${hidden} more` }) : null, _jsx(Text, { wrap: "truncate-end", ...dim, children: fit('Type to search · ↑↓ move · Enter picks · Esc cancels', inner) })] }));
|
|
97
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { roleProps } from '../role-props.js';
|
|
3
4
|
/**
|
|
4
5
|
* Ink-mode rendering of cspeach-widget:question. Uses a double-border box
|
|
5
6
|
* so it's visually distinct from the single-border tool-widget. No
|
|
@@ -7,5 +8,5 @@ import { Box, Text } from 'ink';
|
|
|
7
8
|
* and types their answer as the next REPL prompt.
|
|
8
9
|
*/
|
|
9
10
|
export function QuestionCard({ data }) {
|
|
10
|
-
return (_jsxs(Box, { borderStyle: "double", borderColor:
|
|
11
|
+
return (_jsxs(Box, { borderStyle: "double", borderColor: roleProps('sap').color, flexDirection: "column", padding: 1, marginTop: 1, children: [_jsxs(Text, { bold: true, children: ["Question: ", data.id] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: data.question }) }), data.context ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Context: ", data.context] }) })) : null, (data.kind === 'choice' || data.kind === 'multi') && data.choices ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: data.kind === 'choice' ? 'Pick one:' : 'Pick any (comma-separate):' }), data.choices.map((c, i) => (_jsxs(Text, { children: ["[", i + 1, "] ", c.label] }, c.value)))] })) : null] }));
|
|
11
12
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { resetTtyStdin } from '../../repl/reset-tty-stdin.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reclaim raw-mode stdin when an Ink modal mounts (2026-06-08).
|
|
5
|
+
*
|
|
6
|
+
* `@inquirer/prompts` toggles the terminal's raw mode (and pauses stdin)
|
|
7
|
+
* out from under Ink while it runs. When it releases, Ink's INTERNAL
|
|
8
|
+
* bookkeeping still thinks raw mode is on, so it does not re-assert it — a
|
|
9
|
+
* card mounted afterwards receives no keypresses. Re-syncing the OS state
|
|
10
|
+
* to what Ink expects on mount fixes it. Direct `process.stdin` calls (not
|
|
11
|
+
* Ink's ref-counted setRawMode) because Ink's count is the stale part.
|
|
12
|
+
*
|
|
13
|
+
* Moved here from the retired v1 question modal (piece 2 / Q1b) so AskCard
|
|
14
|
+
* and ApprovalModal share it; the modal file was deleted in Q2.
|
|
15
|
+
*
|
|
16
|
+
* Review I2 (2026-09-27): `setRawMode(true)` + `resume()` alone is the pair
|
|
17
|
+
* the live-fix pty evidence showed does NOT revive Windows console stdin
|
|
18
|
+
* after an inquirer prompt. Use the full reset cycle (no-op off a TTY).
|
|
19
|
+
*/
|
|
20
|
+
export function useReclaimRawStdin() {
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
resetTtyStdin(process.stdin);
|
|
23
|
+
}, []);
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { roleProps } from '../role-props.js';
|
|
4
|
+
import { shouldUseUnicodeBoxChars } from '../../renderer/tty.js';
|
|
5
|
+
import { truncateToWidth } from '../../renderer/ui-width.js';
|
|
6
|
+
/** Spec §8 table, in order. */
|
|
7
|
+
export const SHORTCUT_ROWS = [
|
|
8
|
+
['/', 'commands'],
|
|
9
|
+
['@', 'files'],
|
|
10
|
+
['Ctrl+R', 'history'],
|
|
11
|
+
['Ctrl+T', 'plan'],
|
|
12
|
+
['Shift+Tab', 'mode'],
|
|
13
|
+
['Esc Esc', 'rewind'],
|
|
14
|
+
['Ctrl+V', 'image'],
|
|
15
|
+
['\\', 'new line'],
|
|
16
|
+
];
|
|
17
|
+
export const SHORTCUTS_NOTE = "A message can't start with ? — type it after any other character.";
|
|
18
|
+
const KEY_COL = 11;
|
|
19
|
+
/** The `?` sheet (card-slot occupant 'shortcuts'). Esc closes. */
|
|
20
|
+
export function ShortcutsSheet({ width, onClose }) {
|
|
21
|
+
useInput((input, key) => { if (key.escape || (key.ctrl && input === 'c'))
|
|
22
|
+
onClose(); });
|
|
23
|
+
const inner = Math.max(8, width - 4);
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: shouldUseUnicodeBoxChars() ? 'round' : 'classic', borderColor: roleProps('frame').color, paddingX: 1, width: width, children: [_jsx(Text, { bold: true, children: "Shortcuts" }), SHORTCUT_ROWS.map(([key, action]) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...roleProps('sap'), children: key.padEnd(KEY_COL) }), truncateToWidth(action, Math.max(1, inner - KEY_COL))] }, key))), _jsx(Text, { ...roleProps('dim'), wrap: "truncate-end", children: truncateToWidth(SHORTCUTS_NOTE, inner) }), _jsx(Text, { ...roleProps('dim'), children: "Esc close" })] }));
|
|
25
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { roleProps } from '../role-props.js';
|
|
3
4
|
export function StackFrames({ data }) {
|
|
4
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor:
|
|
5
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: roleProps('bad').color, paddingX: 1, children: [_jsx(Text, { ...roleProps('bad', { bold: true }), children: "Stack Frames" }), data.map((f) => (_jsxs(Box, { flexDirection: "column", marginTop: f.frame === 0 ? 0 : 1, children: [_jsx(Box, { children: _jsxs(Text, { ...roleProps(f.type === 'error' ? 'bad' : 'dim'), children: ['#' + String(f.frame).padStart(2, '0'), " ", f.program, ":", f.line] }) }), _jsx(Box, { marginLeft: 5, children: _jsx(Text, { ...(f.type === 'error' ? {} : roleProps('dim')), dimColor: f.type === 'context', children: f.code }) })] }, f.frame)))] }));
|
|
5
6
|
}
|
package/dist/upgrade-check.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { formatAttention } from './renderer/notice-shape.js';
|
|
2
2
|
import { loadConfig } from './config/loader.js';
|
|
3
3
|
import { CLI_VERSION } from './lib/version.js';
|
|
4
|
+
import { getStore } from './auth/api-key.js';
|
|
5
|
+
import { brandSettled } from './renderer/brand-settled.js';
|
|
4
6
|
function compareVersions(a, b) {
|
|
5
7
|
const pa = a.split('.').map(Number);
|
|
6
8
|
const pb = b.split('.').map(Number);
|
|
@@ -12,18 +14,47 @@ function compareVersions(a, b) {
|
|
|
12
14
|
}
|
|
13
15
|
return 0;
|
|
14
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Minimum-version check (fix round 3, item 11c). The proxy has no
|
|
19
|
+
* unauthenticated endpoint that carries the minimum CLI version (/health
|
|
20
|
+
* reports the proxy's own version only), and it never sets an
|
|
21
|
+
* `x-skill-min-cli-version` header: `min_cli_version` is a field of the
|
|
22
|
+
* AUTHENTICATED `GET /v1/skills/:name` JSON body. The old unauthenticated
|
|
23
|
+
* HEAD always got a 401, so the warning could never fire. Now: signed in →
|
|
24
|
+
* GET with the bearer and read the body field (the header is still honoured
|
|
25
|
+
* should the proxy add it); not signed in → skip (a 401 tells us nothing).
|
|
26
|
+
* The warning goes to stderr so one-shot stdout stays the answer only.
|
|
27
|
+
*/
|
|
15
28
|
export async function checkForUpgrade() {
|
|
16
|
-
const cfg = await loadConfig();
|
|
17
29
|
try {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
30
|
+
const cfg = await loadConfig();
|
|
31
|
+
const key = await (await getStore()).get();
|
|
32
|
+
if (!key)
|
|
33
|
+
return;
|
|
34
|
+
const r = await fetch(`${cfg.proxy_url}/v1/skills/abap-radar`, {
|
|
35
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
36
|
+
signal: AbortSignal.timeout(3000),
|
|
37
|
+
});
|
|
38
|
+
if (!r.ok)
|
|
39
|
+
return;
|
|
40
|
+
let minCli = r.headers.get('x-skill-min-cli-version');
|
|
41
|
+
if (!minCli) {
|
|
42
|
+
const body = (await r.json().catch(() => null));
|
|
43
|
+
minCli = typeof body?.min_cli_version === 'string' ? body.min_cli_version : null;
|
|
44
|
+
}
|
|
20
45
|
const current = CLI_VERSION;
|
|
21
|
-
if (minCli && compareVersions(current, minCli) < 0) {
|
|
22
|
-
|
|
23
|
-
|
|
46
|
+
if (minCli && /^\d+(\.\d+){0,2}$/.test(minCli) && compareVersions(current, minCli) < 0) {
|
|
47
|
+
// Piece 2 §10: one "!" line + Next: (minCli is a server value — formatAttention sanitises it).
|
|
48
|
+
// Never mid-line: wait for the early panels brand line to end (resolved
|
|
49
|
+
// at once when none is open — legacy, one-shot, piped).
|
|
50
|
+
await brandSettled();
|
|
51
|
+
console.error('\n' + formatAttention({
|
|
52
|
+
text: `This CSPeach (${current}) is older than the minimum (${minCli})`,
|
|
53
|
+
next: 'cspeach --upgrade',
|
|
54
|
+
}).trimEnd());
|
|
24
55
|
}
|
|
25
56
|
}
|
|
26
57
|
catch {
|
|
27
|
-
// Offline or proxy down —
|
|
58
|
+
// Offline, keychain unavailable or proxy down — never block startup.
|
|
28
59
|
}
|
|
29
60
|
}
|