auq-mcp-server 2.7.1 → 2.7.2
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/package.json +3 -3
- package/dist/src/i18n/types.js +0 -1
- package/dist/src/tui/shared/session-events.js +0 -1
- package/dist/src/tui/shared/themes/types.js +0 -1
- package/dist/src/tui/shared/types.js +0 -1
- package/dist/src/tui-opentui/ConfigContext.js +10 -0
- package/dist/src/tui-opentui/ThemeProvider.js +73 -0
- package/dist/src/tui-opentui/app.js +536 -0
- package/dist/src/tui-opentui/components/AnimatedGradient.js +56 -0
- package/dist/src/tui-opentui/components/ConfirmationDialog.js +89 -0
- package/dist/src/tui-opentui/components/CustomInput.js +25 -0
- package/dist/src/tui-opentui/components/ErrorBoundary.js +26 -0
- package/dist/src/tui-opentui/components/Footer.js +92 -0
- package/dist/src/tui-opentui/components/Header.js +46 -0
- package/dist/src/tui-opentui/components/MarkdownPrompt.js +13 -0
- package/dist/src/tui-opentui/components/OptionsList.js +258 -0
- package/dist/src/tui-opentui/components/QuestionDisplay.js +23 -0
- package/dist/src/tui-opentui/components/ReviewScreen.js +81 -0
- package/dist/src/tui-opentui/components/SessionDots.js +86 -0
- package/dist/src/tui-opentui/components/SessionPicker.js +162 -0
- package/dist/src/tui-opentui/components/SingleLineTextInput.js +9 -0
- package/dist/src/tui-opentui/components/StepperView.js +493 -0
- package/dist/src/tui-opentui/components/TabBar.js +79 -0
- package/dist/src/tui-opentui/components/ThemeIndicator.js +35 -0
- package/dist/src/tui-opentui/components/Toast.js +44 -0
- package/dist/src/tui-opentui/components/UpdateBadge.js +24 -0
- package/dist/src/tui-opentui/components/UpdateOverlay.js +162 -0
- package/dist/src/tui-opentui/components/WaitingScreen.js +44 -0
- package/dist/src/tui-opentui/hooks/useSessionWatcher.js +69 -0
- package/dist/src/tui-opentui/hooks/useTerminalDimensions.js +8 -0
- package/dist/src/tui-opentui/utils/syntaxStyle.js +64 -0
- package/dist/src/update/types.js +0 -1
- package/package.json +3 -3
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { useTheme } from "../ThemeProvider.js";
|
|
4
|
+
/** Hex string → [r, g, b] tuple */
|
|
5
|
+
function hexToRgb(hex) {
|
|
6
|
+
const h = hex.replace("#", "");
|
|
7
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
8
|
+
}
|
|
9
|
+
/** [r, g, b] tuple → hex string */
|
|
10
|
+
function rgbToHex(r, g, b) {
|
|
11
|
+
return ("#" +
|
|
12
|
+
[r, g, b]
|
|
13
|
+
.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0"))
|
|
14
|
+
.join(""));
|
|
15
|
+
}
|
|
16
|
+
/** Linear interpolation between two hex colours */
|
|
17
|
+
function lerpColor(a, b, t) {
|
|
18
|
+
const [ar, ag, ab] = hexToRgb(a);
|
|
19
|
+
const [br, bg, bb] = hexToRgb(b);
|
|
20
|
+
return rgbToHex(ar + (br - ar) * t, ag + (bg - ag) * t, ab + (bb - ab) * t);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* AnimatedGradient – flowing gradient text for OpenTUI.
|
|
24
|
+
*
|
|
25
|
+
* Renders each character in its own <text> element with a per-character
|
|
26
|
+
* foreground colour derived from the theme gradient palette, animated
|
|
27
|
+
* with a sine-wave shimmer at `fps` frames/sec.
|
|
28
|
+
*
|
|
29
|
+
* IMPORTANT: We do NOT use gradient-string here because that library
|
|
30
|
+
* produces ANSI escape sequences which OpenTUI renders as literal text.
|
|
31
|
+
* Instead each character gets its own <text style={{ fg: hexColor }}> leaf.
|
|
32
|
+
*/
|
|
33
|
+
export const AnimatedGradient = ({ text, flowSpeed = 0.5, fps = 30, }) => {
|
|
34
|
+
const { theme } = useTheme();
|
|
35
|
+
const [frame, setFrame] = useState(0);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const id = setInterval(() => {
|
|
38
|
+
setFrame((f) => f + 1);
|
|
39
|
+
}, 1000 / fps);
|
|
40
|
+
return () => clearInterval(id);
|
|
41
|
+
}, [fps]);
|
|
42
|
+
const chars = text.split("");
|
|
43
|
+
const total = chars.length || 1;
|
|
44
|
+
const elements = chars.map((char, i) => {
|
|
45
|
+
// Sine-wave phase: each char has a different phase offset, frame advances it
|
|
46
|
+
const phase = (i / total) * Math.PI * 2 + frame * flowSpeed * 0.1;
|
|
47
|
+
const t = (Math.sin(phase) + 1) / 2; // 0..1
|
|
48
|
+
// Interpolate start→middle in first half, middle→end in second half
|
|
49
|
+
const color = t < 0.5
|
|
50
|
+
? lerpColor(theme.gradient.start, theme.gradient.middle, t * 2)
|
|
51
|
+
: lerpColor(theme.gradient.middle, theme.gradient.end, (t - 0.5) * 2);
|
|
52
|
+
return (_jsx("text", { style: { fg: color }, children: char }, i));
|
|
53
|
+
});
|
|
54
|
+
// <box flexDirection="row"> arranges the per-character <text> nodes side by side
|
|
55
|
+
return _jsx("box", { style: { flexDirection: "row" }, children: elements });
|
|
56
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { TextAttributes } from "@opentui/core";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useKeyboard } from "@opentui/react";
|
|
5
|
+
import { t } from "../../i18n/index.js";
|
|
6
|
+
import { useTheme } from "../ThemeProvider.js";
|
|
7
|
+
import { KEYS } from "../../tui/constants/keybindings.js";
|
|
8
|
+
import { SingleLineTextInput } from "./SingleLineTextInput.js";
|
|
9
|
+
/**
|
|
10
|
+
* ConfirmationDialog shows a 3-option prompt for session rejection.
|
|
11
|
+
* Options: Reject & inform AI, Cancel, or Quit CLI.
|
|
12
|
+
* If user chooses to reject, shows a two-step flow to optionally collect rejection reason.
|
|
13
|
+
*/
|
|
14
|
+
export const ConfirmationDialog = ({ message, onReject, onCancel, onQuit, }) => {
|
|
15
|
+
const { theme } = useTheme();
|
|
16
|
+
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
17
|
+
const [showReasonInput, setShowReasonInput] = useState(false);
|
|
18
|
+
const [rejectionReason, setRejectionReason] = useState("");
|
|
19
|
+
const handleReasonSubmit = () => {
|
|
20
|
+
onReject(rejectionReason.trim() || null);
|
|
21
|
+
};
|
|
22
|
+
const handleSkipReason = () => {
|
|
23
|
+
onReject(null);
|
|
24
|
+
};
|
|
25
|
+
const options = [
|
|
26
|
+
{
|
|
27
|
+
key: "y",
|
|
28
|
+
label: t("confirmation.rejectYes"),
|
|
29
|
+
action: () => setShowReasonInput(true),
|
|
30
|
+
},
|
|
31
|
+
{ key: "n", label: t("confirmation.rejectNo"), action: onCancel },
|
|
32
|
+
];
|
|
33
|
+
useKeyboard((key) => {
|
|
34
|
+
// If in reason input mode, handle Esc to skip
|
|
35
|
+
if (showReasonInput) {
|
|
36
|
+
if (key.name === "escape") {
|
|
37
|
+
handleSkipReason();
|
|
38
|
+
}
|
|
39
|
+
return; // Let native input handle other keys
|
|
40
|
+
}
|
|
41
|
+
// Arrow key navigation
|
|
42
|
+
if (key.name === "up") {
|
|
43
|
+
setFocusedIndex((prev) => Math.max(0, prev - 1));
|
|
44
|
+
}
|
|
45
|
+
if (key.name === "down") {
|
|
46
|
+
setFocusedIndex((prev) => Math.min(options.length - 1, prev + 1));
|
|
47
|
+
}
|
|
48
|
+
// Enter key - select focused option
|
|
49
|
+
if (key.name === "return") {
|
|
50
|
+
options[focusedIndex].action();
|
|
51
|
+
}
|
|
52
|
+
// Letter shortcuts
|
|
53
|
+
if (KEYS.CONFIRM_YES.test(key.sequence || key.name)) {
|
|
54
|
+
setShowReasonInput(true);
|
|
55
|
+
}
|
|
56
|
+
if (KEYS.CONFIRM_NO.test(key.sequence || key.name)) {
|
|
57
|
+
onCancel();
|
|
58
|
+
}
|
|
59
|
+
// Esc key - same as quit
|
|
60
|
+
if (key.name === "escape") {
|
|
61
|
+
onQuit();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// Step 2: Reason input screen
|
|
65
|
+
if (showReasonInput) {
|
|
66
|
+
return (_jsxs("box", { style: {
|
|
67
|
+
borderColor: theme.borders.warning,
|
|
68
|
+
borderStyle: "rounded",
|
|
69
|
+
flexDirection: "column",
|
|
70
|
+
padding: 1,
|
|
71
|
+
}, children: [_jsx("box", { style: { marginBottom: 1 }, children: _jsx("text", { style: { attributes: TextAttributes.BOLD, fg: theme.colors.warning }, children: t("confirmation.rejectTitle") }) }), _jsx("box", { style: { marginBottom: 1 }, children: _jsx("text", { style: { attributes: TextAttributes.DIM }, children: t("confirmation.rejectMessage") }) }), _jsx("box", { style: { marginBottom: 1 }, children: _jsx(SingleLineTextInput, { isFocused: true, onChange: setRejectionReason, onSubmit: handleReasonSubmit, placeholder: "Type your reason here...", value: rejectionReason }) }), _jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { style: { attributes: TextAttributes.DIM }, children: `Enter ${t("footer.submit")} | Esc ${t("footer.cancel")}` }) })] }));
|
|
72
|
+
}
|
|
73
|
+
// Step 1: Confirmation options
|
|
74
|
+
return (_jsxs("box", { style: {
|
|
75
|
+
borderColor: theme.borders.warning,
|
|
76
|
+
borderStyle: "rounded",
|
|
77
|
+
flexDirection: "column",
|
|
78
|
+
padding: 1,
|
|
79
|
+
}, children: [_jsx("box", { style: { marginBottom: 1 }, children: _jsx("text", { style: { attributes: TextAttributes.BOLD, fg: theme.colors.warning }, children: message }) }), options.map((option, index) => {
|
|
80
|
+
const isFocused = index === focusedIndex;
|
|
81
|
+
return (_jsx("box", { style: { marginTop: index > 0 ? 1 : 0 }, children: _jsx("text", { style: {
|
|
82
|
+
bg: isFocused
|
|
83
|
+
? theme.components.options.focusedBg
|
|
84
|
+
: undefined,
|
|
85
|
+
attributes: isFocused ? TextAttributes.BOLD : TextAttributes.NONE,
|
|
86
|
+
fg: isFocused ? theme.colors.focused : theme.colors.text,
|
|
87
|
+
}, children: `${isFocused ? "> " : " "}${index + 1}. ${option.label} (${option.key})` }) }, index));
|
|
88
|
+
}), _jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { style: { attributes: TextAttributes.DIM }, children: "\u2191\u2193 " + t("confirmation.keybindings") }) })] }));
|
|
89
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { TextAttributes } from "@opentui/core";
|
|
3
|
+
import { t } from "../../i18n/index.js";
|
|
4
|
+
import { useTheme } from "../ThemeProvider.js";
|
|
5
|
+
/**
|
|
6
|
+
* CustomInput allows users to type free-text answers.
|
|
7
|
+
* Uses OpenTUI's native <input> component with visual focus indicator.
|
|
8
|
+
*/
|
|
9
|
+
export const CustomInput = ({ isFocused, onChange, value, }) => {
|
|
10
|
+
const { theme } = useTheme();
|
|
11
|
+
return (_jsxs("box", { style: {
|
|
12
|
+
borderColor: isFocused
|
|
13
|
+
? theme.components.input.borderFocused
|
|
14
|
+
: theme.components.input.border,
|
|
15
|
+
borderStyle: "rounded",
|
|
16
|
+
flexDirection: "column",
|
|
17
|
+
marginTop: 1,
|
|
18
|
+
padding: 1,
|
|
19
|
+
}, children: [_jsx("text", { style: {
|
|
20
|
+
fg: theme.colors.textDim,
|
|
21
|
+
attributes: !isFocused ? TextAttributes.DIM : TextAttributes.NONE,
|
|
22
|
+
}, children: `${isFocused ? ">" : " "} ${t("input.customAnswerLabel")}` }), _jsx("box", { style: { marginTop: 1 }, children: isFocused ? (_jsx("input", { placeholder: "Type your answer here...", value: value, focused: isFocused, onInput: (val) => onChange(val) })) : (_jsx("text", { style: {
|
|
23
|
+
fg: value ? theme.colors.text : theme.colors.textDim,
|
|
24
|
+
}, children: value || t("input.customAnswerHint") })) })] }));
|
|
25
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Error boundary for OpenTUI renderer errors.
|
|
4
|
+
* On error, renders nothing (silent fail) — fallback handled at entry point level.
|
|
5
|
+
*/
|
|
6
|
+
export class ErrorBoundary extends React.Component {
|
|
7
|
+
state = { hasError: false, error: null };
|
|
8
|
+
static getDerivedStateFromError(error) {
|
|
9
|
+
return { hasError: true, error };
|
|
10
|
+
}
|
|
11
|
+
componentDidCatch(error, info) {
|
|
12
|
+
// Always log to stderr so the error is visible even if rendering fails
|
|
13
|
+
process.stderr.write(`\n[OpenTUI Error] ${error.message}\n${error.stack ?? ""}\n`);
|
|
14
|
+
if (info?.componentStack) {
|
|
15
|
+
process.stderr.write(`Component stack: ${info.componentStack}\n`);
|
|
16
|
+
}
|
|
17
|
+
this.props.onError?.(error);
|
|
18
|
+
}
|
|
19
|
+
render() {
|
|
20
|
+
if (this.state.hasError) {
|
|
21
|
+
// Show error message instead of blank screen
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return this.props.children;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { t } from "../../i18n/index.js";
|
|
4
|
+
import { useTheme } from "../ThemeProvider.js";
|
|
5
|
+
import { KEY_LABELS } from "../../tui/constants/keybindings.js";
|
|
6
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
7
|
+
/**
|
|
8
|
+
* Footer component — displays context-aware keybinding hints.
|
|
9
|
+
* Shows different shortcuts based on current focus context and question type.
|
|
10
|
+
*/
|
|
11
|
+
export const Footer = ({ focusContext, multiSelect, isReviewScreen = false, showSessionSwitching = false, customInputValue: _customInputValue = "", hasRecommendedOptions = false, hasAnyRecommendedInSession = false, isSubmitting = false, hasUpdate = false, }) => {
|
|
12
|
+
const { theme } = useTheme();
|
|
13
|
+
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
14
|
+
// Animate spinner when submitting
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!isSubmitting)
|
|
17
|
+
return;
|
|
18
|
+
const interval = setInterval(() => {
|
|
19
|
+
setSpinnerFrame((prev) => (prev + 1) % SPINNER_FRAMES.length);
|
|
20
|
+
}, 80);
|
|
21
|
+
return () => clearInterval(interval);
|
|
22
|
+
}, [isSubmitting]);
|
|
23
|
+
const getKeybindings = () => {
|
|
24
|
+
// Review screen mode
|
|
25
|
+
if (isReviewScreen) {
|
|
26
|
+
return [
|
|
27
|
+
{ key: KEY_LABELS.SUBMIT, action: t("footer.submit") },
|
|
28
|
+
{ key: KEY_LABELS.BACK, action: t("footer.back") },
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
// Custom input focused
|
|
32
|
+
if (focusContext === "custom-input") {
|
|
33
|
+
return [
|
|
34
|
+
{ key: KEY_LABELS.NAVIGATE_OPTIONS, action: t("footer.options") },
|
|
35
|
+
{ key: KEY_LABELS.CURSOR, action: t("footer.cursor") },
|
|
36
|
+
{ key: KEY_LABELS.NAVIGATE_QUESTIONS_TAB, action: t("footer.questions") },
|
|
37
|
+
{ key: KEY_LABELS.NEWLINE, action: t("footer.newline") },
|
|
38
|
+
{ key: KEY_LABELS.REJECT, action: t("footer.reject") },
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
// Elaborate input focused (Enter skips, not newline)
|
|
42
|
+
if (focusContext === "elaborate-input") {
|
|
43
|
+
return [
|
|
44
|
+
{ key: KEY_LABELS.NAVIGATE_OPTIONS, action: t("footer.options") },
|
|
45
|
+
{ key: KEY_LABELS.CURSOR, action: t("footer.cursor") },
|
|
46
|
+
{ key: "Enter/Tab", action: t("footer.next") },
|
|
47
|
+
{ key: KEY_LABELS.REJECT, action: t("footer.reject") },
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
// Option focused
|
|
51
|
+
if (focusContext === "option") {
|
|
52
|
+
const bindings = [
|
|
53
|
+
{ key: KEY_LABELS.NAVIGATE_OPTIONS, action: t("footer.options") },
|
|
54
|
+
{ key: KEY_LABELS.NAVIGATE_QUESTIONS, action: t("footer.questions") },
|
|
55
|
+
{ key: KEY_LABELS.NAVIGATE_QUESTIONS_TAB, action: t("footer.questions") },
|
|
56
|
+
];
|
|
57
|
+
if (multiSelect) {
|
|
58
|
+
bindings.push({ key: KEY_LABELS.SELECT, action: t("footer.toggle") });
|
|
59
|
+
bindings.push({ key: KEY_LABELS.NEXT, action: t("footer.next") });
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
bindings.push({ key: KEY_LABELS.SELECT, action: t("footer.select") });
|
|
63
|
+
bindings.push({ key: KEY_LABELS.SELECT_NEXT, action: t("footer.selectNext") });
|
|
64
|
+
}
|
|
65
|
+
if (hasRecommendedOptions) {
|
|
66
|
+
bindings.push({ key: KEY_LABELS.RECOMMEND, action: t("footer.recommended") });
|
|
67
|
+
}
|
|
68
|
+
// Ctrl+R shows when ANY question in session has recommended (not just current)
|
|
69
|
+
if (hasAnyRecommendedInSession) {
|
|
70
|
+
bindings.push({ key: KEY_LABELS.QUICK_SUBMIT, action: t("footer.quickSubmit") });
|
|
71
|
+
}
|
|
72
|
+
if (showSessionSwitching) {
|
|
73
|
+
bindings.push({ key: KEY_LABELS.SESSION_SWITCH, action: t("footer.sessions") });
|
|
74
|
+
bindings.push({ key: "1-9", action: t("footer.jump") });
|
|
75
|
+
bindings.push({ key: KEY_LABELS.SESSION_LIST, action: t("footer.list") });
|
|
76
|
+
}
|
|
77
|
+
bindings.push({ key: KEY_LABELS.THEME, action: t("footer.theme") });
|
|
78
|
+
if (hasUpdate) {
|
|
79
|
+
bindings.push({ key: KEY_LABELS.UPDATE, action: "Update" });
|
|
80
|
+
}
|
|
81
|
+
bindings.push({ key: KEY_LABELS.REJECT, action: t("footer.reject") });
|
|
82
|
+
return bindings;
|
|
83
|
+
}
|
|
84
|
+
return [];
|
|
85
|
+
};
|
|
86
|
+
const keybindings = getKeybindings();
|
|
87
|
+
return (_jsx("box", { style: { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexWrap: "wrap", border: true, borderStyle: "rounded", borderColor: theme.colors.surface }, children: keybindings.map((binding, idx) => (_jsxs("box", { style: { paddingRight: 2 }, children: [_jsx("text", { style: {
|
|
88
|
+
bg: theme.components.footer.keyBg,
|
|
89
|
+
fg: theme.components.footer.keyFg,
|
|
90
|
+
bold: true,
|
|
91
|
+
}, children: ` ${binding.key} ` }), _jsx("text", { style: { fg: theme.components.footer.action, dim: true }, children: ` ${binding.action}` }), isSubmitting && binding.key === "Enter" && isReviewScreen ? (_jsx("text", { style: { fg: theme.colors.pending, bold: true }, children: ` ${SPINNER_FRAMES[spinnerFrame]}` })) : null] }, idx))) }));
|
|
92
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { t } from "../../i18n/index.js";
|
|
4
|
+
import packageJson from "../../../package.json" with { type: "json" };
|
|
5
|
+
import { useTheme } from "../ThemeProvider.js";
|
|
6
|
+
import { UpdateBadge as _UpdateBadge } from "./UpdateBadge.js";
|
|
7
|
+
// Cast to FC to avoid React type mismatch between @opentui/react bundled React
|
|
8
|
+
// and the project's @types/react (structural type incompatibility).
|
|
9
|
+
const UpdateBadge = _UpdateBadge;
|
|
10
|
+
/**
|
|
11
|
+
* Header component — displays app logo and status.
|
|
12
|
+
* Shows at the top of the TUI with solid accent branding and live-updating
|
|
13
|
+
* pending queue count pill.
|
|
14
|
+
*/
|
|
15
|
+
export const Header = ({ pendingCount, isCheckingUpdate, updateInfo, onUpdateBadgeActivate: _onUpdateBadgeActivate, }) => {
|
|
16
|
+
const { theme } = useTheme();
|
|
17
|
+
const [flash, setFlash] = useState(false);
|
|
18
|
+
const [prevCount, setPrevCount] = useState(pendingCount);
|
|
19
|
+
// Flash effect when count changes
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (pendingCount !== prevCount) {
|
|
22
|
+
setFlash(true);
|
|
23
|
+
setPrevCount(pendingCount);
|
|
24
|
+
const timer = setTimeout(() => setFlash(false), 300);
|
|
25
|
+
return () => clearTimeout(timer);
|
|
26
|
+
}
|
|
27
|
+
}, [pendingCount, prevCount]);
|
|
28
|
+
const version = useMemo(() => {
|
|
29
|
+
return packageJson.version || "unknown";
|
|
30
|
+
}, []);
|
|
31
|
+
const tagline = t("header.title");
|
|
32
|
+
// Queue pill color: flash > active > empty
|
|
33
|
+
const queueColor = flash
|
|
34
|
+
? theme.components.header.queueFlash
|
|
35
|
+
: pendingCount > 0
|
|
36
|
+
? theme.components.header.queueActive
|
|
37
|
+
: theme.components.header.queueEmpty;
|
|
38
|
+
const queueLabel = pendingCount > 0
|
|
39
|
+
? ` ${pendingCount} queued `
|
|
40
|
+
: " idle ";
|
|
41
|
+
return (_jsxs("box", { style: { flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, border: true, borderStyle: "rounded", borderColor: theme.colors.surface }, children: [_jsxs("box", { style: { flexDirection: "row", alignItems: "center" }, children: [_jsx("text", { style: { bold: true, fg: theme.colors.primary }, children: "AUQ" }), _jsx("text", { style: { fg: "#8A949E" }, children: ` ⋆ ${tagline}` })] }), _jsxs("box", { style: { flexDirection: "row", alignItems: "center" }, children: [_jsx("text", { style: { fg: theme.colors.textDim }, children: `v${version}` }), updateInfo ? (_jsx(UpdateBadge, { updateType: updateInfo.updateType, latestVersion: updateInfo.latestVersion })) : null, _jsx("text", { style: { fg: theme.colors.textDim }, children: " " }), isCheckingUpdate ? (_jsx("text", { style: { fg: theme.colors.textDim }, children: " checking... " })) : null, _jsx("text", { style: {
|
|
42
|
+
bg: theme.components.header.pillBg,
|
|
43
|
+
fg: queueColor,
|
|
44
|
+
bold: flash,
|
|
45
|
+
}, children: queueLabel })] })] }));
|
|
46
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useTheme } from "../ThemeProvider.js";
|
|
3
|
+
/**
|
|
4
|
+
* MarkdownPrompt renders markdown text using OpenTUI's native <markdown> element.
|
|
5
|
+
* Uses the theme-derived SyntaxStyle for code highlighting.
|
|
6
|
+
*/
|
|
7
|
+
export const MarkdownPrompt = ({ text }) => {
|
|
8
|
+
const { syntaxStyle } = useTheme();
|
|
9
|
+
if (!text || text.trim().length === 0) {
|
|
10
|
+
return _jsx("text", { children: text });
|
|
11
|
+
}
|
|
12
|
+
return _jsx("markdown", { content: text, syntaxStyle: syntaxStyle });
|
|
13
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { TextAttributes } from "@opentui/core";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useKeyboard } from "@opentui/react";
|
|
5
|
+
import { t } from "../../i18n/index.js";
|
|
6
|
+
import { useConfig } from "../ConfigContext.js";
|
|
7
|
+
import { useTheme } from "../ThemeProvider.js";
|
|
8
|
+
import { isRecommendedOption } from "../../tui/shared/utils/recommended.js";
|
|
9
|
+
import { useTerminalDimensions } from "../hooks/useTerminalDimensions.js";
|
|
10
|
+
/**
|
|
11
|
+
* OptionsList displays answer choices and handles arrow key navigation.
|
|
12
|
+
* Uses \u2191\u2193 to navigate, Enter to select.
|
|
13
|
+
*
|
|
14
|
+
* Custom multi-select built with <box>, <text>, useKeyboard().
|
|
15
|
+
* NO native <select> \u2014 spec requires custom for multi-select.
|
|
16
|
+
*/
|
|
17
|
+
export const OptionsList = ({ isFocused, onSelect, options, selectedOption, showCustomInput = false, customValue = "", onCustomChange, onAdvance, multiSelect = false, onToggle, selectedOptions = [], focusedIndex: focusedIndexProp, onFocusedIndexChange, onFocusContextChange, onRecommendedDetected, questionKey, autoSelectRecommended: autoSelectRecommendedProp, isElaborateMarked = false, onElaborateSelect, elaborateText = "", onElaborateTextChange, }) => {
|
|
18
|
+
const { theme } = useTheme();
|
|
19
|
+
const config = useConfig();
|
|
20
|
+
const { width: termWidth } = useTerminalDimensions();
|
|
21
|
+
const rowWidth = Math.max(20, termWidth - 6);
|
|
22
|
+
// Use prop if provided, otherwise use config value
|
|
23
|
+
const autoSelectRecommended = autoSelectRecommendedProp ?? config.autoSelectRecommended;
|
|
24
|
+
const [internalFocusedIndex, setInternalFocusedIndex] = useState(0);
|
|
25
|
+
const focusedIndex = focusedIndexProp ?? internalFocusedIndex;
|
|
26
|
+
const setFocusedIndex = (nextIndex) => {
|
|
27
|
+
const resolvedIndex = typeof nextIndex === "function" ? nextIndex(focusedIndex) : nextIndex;
|
|
28
|
+
if (focusedIndexProp === undefined) {
|
|
29
|
+
setInternalFocusedIndex(resolvedIndex);
|
|
30
|
+
}
|
|
31
|
+
onFocusedIndexChange?.(resolvedIndex);
|
|
32
|
+
};
|
|
33
|
+
// Calculate max index: include custom input and elaborate options if enabled
|
|
34
|
+
// Options: [0..n-1] = regular options, [n] = custom input, [n+1] = elaborate
|
|
35
|
+
const customInputIndex = options.length;
|
|
36
|
+
const elaborateIndex = options.length + 1;
|
|
37
|
+
const maxIndex = showCustomInput ? elaborateIndex : options.length - 1;
|
|
38
|
+
const isCustomInputFocused = showCustomInput && focusedIndex === customInputIndex;
|
|
39
|
+
const isElaborateFocused = showCustomInput && focusedIndex === elaborateIndex;
|
|
40
|
+
const customLines = customValue.replace(/\r\n?/g, "\n").split("\n");
|
|
41
|
+
const elaborateLines = elaborateText.replace(/\r\n?/g, "\n").split("\n");
|
|
42
|
+
// Track and emit focus context changes
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const newContext = isElaborateFocused
|
|
45
|
+
? "elaborate-input"
|
|
46
|
+
: isCustomInputFocused
|
|
47
|
+
? "custom-input"
|
|
48
|
+
: "option";
|
|
49
|
+
onFocusContextChange?.(newContext);
|
|
50
|
+
}, [
|
|
51
|
+
focusedIndex,
|
|
52
|
+
isCustomInputFocused,
|
|
53
|
+
isElaborateFocused,
|
|
54
|
+
onFocusContextChange,
|
|
55
|
+
]);
|
|
56
|
+
// Reset focus when question changes
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
setFocusedIndex(0);
|
|
59
|
+
}, [questionKey]);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (focusedIndex > maxIndex) {
|
|
62
|
+
setFocusedIndex(maxIndex);
|
|
63
|
+
}
|
|
64
|
+
}, [focusedIndex, maxIndex]);
|
|
65
|
+
// Detect recommended options and notify parent
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const recommendedOptions = options.filter((opt) => isRecommendedOption(opt.label));
|
|
68
|
+
const hasRecommended = recommendedOptions.length > 0;
|
|
69
|
+
onRecommendedDetected?.(hasRecommended);
|
|
70
|
+
}, [options, onRecommendedDetected]);
|
|
71
|
+
useKeyboard((key) => {
|
|
72
|
+
if (!isFocused)
|
|
73
|
+
return;
|
|
74
|
+
// Handle up/down navigation even when custom input is focused
|
|
75
|
+
if (key.name === "up") {
|
|
76
|
+
const newIndex = Math.max(0, focusedIndex - 1);
|
|
77
|
+
setFocusedIndex(newIndex);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (key.name === "down") {
|
|
81
|
+
const newIndex = Math.min(maxIndex, focusedIndex + 1);
|
|
82
|
+
setFocusedIndex(newIndex);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// When custom input is focused, only handle escape and tab to exit
|
|
86
|
+
if (isCustomInputFocused) {
|
|
87
|
+
if (key.name === "escape") {
|
|
88
|
+
setFocusedIndex(Math.max(0, options.length - 1));
|
|
89
|
+
}
|
|
90
|
+
else if (key.name === "tab" && !key.shift) {
|
|
91
|
+
onAdvance?.();
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// When elaborate input is focused, only handle escape and tab to exit
|
|
96
|
+
if (isElaborateFocused) {
|
|
97
|
+
if (key.name === "escape") {
|
|
98
|
+
setFocusedIndex(customInputIndex);
|
|
99
|
+
}
|
|
100
|
+
else if (key.name === "tab" && !key.shift) {
|
|
101
|
+
onAdvance?.();
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Spacebar: Select/toggle WITHOUT advancing (works for both modes)
|
|
106
|
+
if (key.name === "space") {
|
|
107
|
+
if (!isCustomInputFocused && !isElaborateFocused) {
|
|
108
|
+
if (multiSelect) {
|
|
109
|
+
onToggle?.(options[focusedIndex].label);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
onSelect(options[focusedIndex].label);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Enter: Advance to next question
|
|
117
|
+
if (key.name === "return") {
|
|
118
|
+
if (isCustomInputFocused || isElaborateFocused) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (multiSelect) {
|
|
122
|
+
// Multi-select: Enter just advances (spacebar toggles)
|
|
123
|
+
if (onAdvance) {
|
|
124
|
+
onAdvance();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Single-select: Enter selects AND advances
|
|
129
|
+
onSelect(options[focusedIndex].label);
|
|
130
|
+
if (onAdvance) {
|
|
131
|
+
onAdvance();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// ── Helper: Truncate text to fit row width ─────────────────────────────────────────
|
|
137
|
+
const fitRow = (text) => {
|
|
138
|
+
if (text.length > rowWidth) {
|
|
139
|
+
return text.slice(0, rowWidth - 1) + "\u2026";
|
|
140
|
+
}
|
|
141
|
+
return text; // Background is filled by parent <box>, no manual padding needed
|
|
142
|
+
};
|
|
143
|
+
return (_jsxs("box", { style: { flexDirection: "column" }, children: [options.map((option, index) => {
|
|
144
|
+
const isFocusedOption = isFocused && index === focusedIndex;
|
|
145
|
+
const isRecommended = isRecommendedOption(option.label);
|
|
146
|
+
// Different icons for single vs multi-select
|
|
147
|
+
const isSelected = multiSelect
|
|
148
|
+
? selectedOptions?.includes(option.label) || false
|
|
149
|
+
: selectedOption === option.label;
|
|
150
|
+
const rowBg = isFocusedOption
|
|
151
|
+
? theme.components.options.focusedBg
|
|
152
|
+
: isSelected
|
|
153
|
+
? theme.components.options.selectedBg
|
|
154
|
+
: undefined;
|
|
155
|
+
const rowColor = isFocusedOption
|
|
156
|
+
? theme.components.options.focused
|
|
157
|
+
: isSelected
|
|
158
|
+
? theme.components.options.selected
|
|
159
|
+
: theme.components.options.default;
|
|
160
|
+
const starSuffix = isRecommended ? " \u2605" : "";
|
|
161
|
+
const mainLine = multiSelect
|
|
162
|
+
? `${isFocusedOption ? ">" : " "} ${isSelected ? "[\u2713]" : "[ ]"} ${option.label}${starSuffix}`
|
|
163
|
+
: `${isFocusedOption ? ">" : " "} ${option.label}${isSelected ? " \u2713" : ""}${starSuffix}`;
|
|
164
|
+
return (_jsxs("box", { style: { flexDirection: "column" }, onMouseDown: () => {
|
|
165
|
+
setFocusedIndex(index);
|
|
166
|
+
if (multiSelect) {
|
|
167
|
+
onToggle?.(option.label);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
onSelect(option.label);
|
|
171
|
+
}
|
|
172
|
+
}, children: [_jsx("box", { style: { backgroundColor: rowBg }, children: _jsx("text", { style: {
|
|
173
|
+
attributes: (isFocusedOption || isSelected) ? TextAttributes.BOLD : TextAttributes.NONE,
|
|
174
|
+
fg: rowColor,
|
|
175
|
+
}, children: fitRow(mainLine) }) }), option.description && (_jsx("box", { style: { backgroundColor: isFocusedOption ? theme.components.options.focusedBg : isSelected ? theme.components.options.selectedBg : undefined }, children: _jsx("text", { style: {
|
|
176
|
+
fg: theme.components.options.description,
|
|
177
|
+
attributes: (!isFocusedOption && !isSelected) ? TextAttributes.DIM : TextAttributes.NONE,
|
|
178
|
+
}, children: fitRow(` ${option.description}`) }) }))] }, index));
|
|
179
|
+
}), showCustomInput && (_jsx("box", { style: { marginTop: 0 }, children: _jsxs("box", { style: { flexDirection: "column" }, children: [(() => {
|
|
180
|
+
const isSelected = customValue.trim().length > 0;
|
|
181
|
+
const rowBg = isCustomInputFocused
|
|
182
|
+
? theme.components.options.focusedBg
|
|
183
|
+
: isSelected
|
|
184
|
+
? theme.components.options.selectedBg
|
|
185
|
+
: undefined;
|
|
186
|
+
const rowColor = isCustomInputFocused
|
|
187
|
+
? theme.components.options.focused
|
|
188
|
+
: isSelected
|
|
189
|
+
? theme.components.options.selected
|
|
190
|
+
: theme.components.options.default;
|
|
191
|
+
const mainLine = multiSelect
|
|
192
|
+
? `${isCustomInputFocused ? ">" : " "} ${isSelected ? "[\u2713]" : "[ ]"} ${t("input.otherCustom")}`
|
|
193
|
+
: `${isCustomInputFocused ? ">" : " "} ${t("input.otherCustom")}${isSelected ? " \u2713" : ""}`;
|
|
194
|
+
return (_jsx("box", { style: { backgroundColor: rowBg }, onMouseDown: () => { setFocusedIndex(customInputIndex); }, children: _jsx("text", { style: {
|
|
195
|
+
attributes: (isCustomInputFocused || isSelected) ? TextAttributes.BOLD : TextAttributes.NONE,
|
|
196
|
+
fg: rowColor,
|
|
197
|
+
}, children: fitRow(mainLine) }) }));
|
|
198
|
+
})(), isCustomInputFocused && onCustomChange && (_jsx("box", { style: {
|
|
199
|
+
borderColor: theme.components.input.borderFocused,
|
|
200
|
+
borderStyle: "rounded",
|
|
201
|
+
marginBottom: 1,
|
|
202
|
+
marginLeft: 2,
|
|
203
|
+
marginTop: 0,
|
|
204
|
+
paddingX: 1,
|
|
205
|
+
paddingY: 0,
|
|
206
|
+
}, children: _jsx("input", { placeholder: t("input.placeholder"), value: customValue, focused: true, onInput: (val) => {
|
|
207
|
+
// Filter SGR mouse escape sequences that leak through stdin parser
|
|
208
|
+
const sanitized = val
|
|
209
|
+
.replace(/\x1b?\[<[\d;]*[Mm]/g, '')
|
|
210
|
+
.replace(/\[?[OI]/g, '');
|
|
211
|
+
if (sanitized !== val && sanitized.length === 0)
|
|
212
|
+
return;
|
|
213
|
+
onCustomChange?.(sanitized);
|
|
214
|
+
}, onSubmit: () => onAdvance?.() }) })), !isCustomInputFocused && customValue && (_jsx("box", { style: { marginLeft: 2, marginTop: 0 }, children: _jsxs("text", { style: { fg: theme.components.options.hint, attributes: TextAttributes.DIM }, children: [" ", customLines.slice(0, 3).join("\n "), customLines.length > 3 ? "\n \u2026" : ""] }) }))] }) })), showCustomInput && (_jsx("box", { style: { marginTop: 0 }, children: _jsxs("box", { style: { flexDirection: "column" }, children: [(() => {
|
|
215
|
+
const rowBg = isElaborateFocused
|
|
216
|
+
? theme.components.options.focusedBg
|
|
217
|
+
: isElaborateMarked
|
|
218
|
+
? theme.components.options.selectedBg
|
|
219
|
+
: undefined;
|
|
220
|
+
const rowColor = isElaborateFocused
|
|
221
|
+
? theme.components.options.focused
|
|
222
|
+
: isElaborateMarked
|
|
223
|
+
? theme.colors.warning
|
|
224
|
+
: theme.components.options.default;
|
|
225
|
+
const mainLine = multiSelect
|
|
226
|
+
? `${isElaborateFocused ? ">" : " "} ${isElaborateMarked ? "[\u2605]" : "[ ]"} ${t("footer.elaborate")}`
|
|
227
|
+
: `${isElaborateFocused ? ">" : " "} ${t("footer.elaborate")}${isElaborateMarked ? " \u2605" : ""}`;
|
|
228
|
+
return (_jsx("box", { style: { backgroundColor: rowBg }, onMouseDown: () => {
|
|
229
|
+
setFocusedIndex(elaborateIndex);
|
|
230
|
+
onElaborateSelect?.();
|
|
231
|
+
}, children: _jsx("text", { style: {
|
|
232
|
+
attributes: (isElaborateFocused || isElaborateMarked) ? TextAttributes.BOLD : TextAttributes.NONE,
|
|
233
|
+
fg: rowColor,
|
|
234
|
+
}, children: fitRow(mainLine) }) }));
|
|
235
|
+
})(), isElaborateFocused && onElaborateTextChange && (_jsx("box", { style: {
|
|
236
|
+
borderColor: theme.components.input.borderFocused,
|
|
237
|
+
borderStyle: "rounded",
|
|
238
|
+
marginBottom: 1,
|
|
239
|
+
marginLeft: 2,
|
|
240
|
+
marginTop: 0,
|
|
241
|
+
paddingX: 1,
|
|
242
|
+
paddingY: 0,
|
|
243
|
+
}, children: _jsx("input", { placeholder: t("input.elaboratePlaceholder"), value: elaborateText, focused: true, onInput: (val) => {
|
|
244
|
+
// Filter SGR mouse escape sequences that leak through stdin parser
|
|
245
|
+
const sanitized = val
|
|
246
|
+
.replace(/\x1b?\[<[\d;]*[Mm]/g, '')
|
|
247
|
+
.replace(/\[?[OI]/g, '');
|
|
248
|
+
if (sanitized !== val && sanitized.length === 0)
|
|
249
|
+
return;
|
|
250
|
+
onElaborateTextChange?.(sanitized);
|
|
251
|
+
}, onSubmit: () => {
|
|
252
|
+
// Enter submits and advance
|
|
253
|
+
if (!elaborateText.trim()) {
|
|
254
|
+
onElaborateSelect?.();
|
|
255
|
+
}
|
|
256
|
+
onAdvance?.();
|
|
257
|
+
} }) })), !isElaborateFocused && elaborateText && (_jsx("box", { style: { marginLeft: 2, marginTop: 0 }, children: _jsxs("text", { style: { fg: theme.components.options.hint, attributes: TextAttributes.DIM }, children: [" ", elaborateLines.slice(0, 3).join("\n "), elaborateLines.length > 3 ? "\n \u2026" : ""] }) }))] }) }))] }));
|
|
258
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { t } from "../../i18n/index.js";
|
|
3
|
+
import { useTheme } from "../ThemeProvider.js";
|
|
4
|
+
import { Footer } from "./Footer.js";
|
|
5
|
+
import { MarkdownPrompt } from "./MarkdownPrompt.js";
|
|
6
|
+
import { OptionsList } from "./OptionsList.js";
|
|
7
|
+
import { TabBar } from "./TabBar.js";
|
|
8
|
+
/**
|
|
9
|
+
* QuestionDisplay shows a single question with its options.
|
|
10
|
+
* Composes TabBar, MarkdownPrompt, OptionsList, and Footer.
|
|
11
|
+
*/
|
|
12
|
+
export const QuestionDisplay = ({ currentQuestion, currentQuestionIndex, customAnswer = "", showSessionSwitching, elapsedLabel, onChangeCustomAnswer, onSelectOption, questions, selectedOption, onAdvanceToNext, answers, onToggleOption, multiSelect, focusContext, onFocusContextChange, focusedOptionIndex, onFocusedOptionIndexChange, workingDirectory, onRecommendedDetected, hasRecommendedOptions, hasAnyRecommendedInSession, elaborateMarks, onElaborateSelect, elaborateText = "", onElaborateTextChange, onSelectIndex, }) => {
|
|
13
|
+
const { theme } = useTheme();
|
|
14
|
+
// Handle option selection
|
|
15
|
+
const handleSelectOption = (label) => {
|
|
16
|
+
onSelectOption(label);
|
|
17
|
+
};
|
|
18
|
+
// Handle custom answer change
|
|
19
|
+
const handleCustomAnswerChange = (text) => {
|
|
20
|
+
onChangeCustomAnswer(text);
|
|
21
|
+
};
|
|
22
|
+
return (_jsxs("box", { style: { flexDirection: "column" }, children: [workingDirectory && (_jsxs("box", { style: { flexDirection: "row" }, children: [_jsx("text", { fg: theme.components.directory.label, children: "📁" }), _jsx("text", { style: { fg: theme.components.directory.path }, children: ` ${workingDirectory}` })] })), _jsx(TabBar, { currentIndex: currentQuestionIndex, questions: questions, answers: answers, elaborateMarks: elaborateMarks, onSelectIndex: onSelectIndex }), _jsxs("box", { style: { flexDirection: "column" }, children: [_jsxs("box", { style: { flexDirection: "row" }, children: [_jsx(MarkdownPrompt, { text: currentQuestion.prompt }), _jsx("text", { style: { fg: theme.components.questionDisplay.typeIndicator }, children: ` [${multiSelect ? t("question.multipleChoice") : t("question.singleChoice")}]` })] }), _jsx("box", { children: _jsx("text", { fg: theme.components.questionDisplay.elapsed, children: elapsedLabel }) })] }), _jsx(OptionsList, { customValue: customAnswer, isFocused: true, onAdvance: onAdvanceToNext, onCustomChange: handleCustomAnswerChange, onSelect: handleSelectOption, options: currentQuestion.options, selectedOption: selectedOption, showCustomInput: true, onToggle: onToggleOption, multiSelect: multiSelect, selectedOptions: answers.get(currentQuestionIndex)?.selectedOptions, onFocusContextChange: onFocusContextChange, focusedIndex: focusedOptionIndex, onFocusedIndexChange: onFocusedOptionIndexChange, onRecommendedDetected: onRecommendedDetected, questionKey: currentQuestionIndex, isElaborateMarked: elaborateMarks?.has(currentQuestionIndex), onElaborateSelect: onElaborateSelect, elaborateText: elaborateText, onElaborateTextChange: onElaborateTextChange }), _jsx("box", { style: { marginTop: 1 }, children: _jsx(Footer, { focusContext: focusContext, multiSelect: multiSelect ?? false, customInputValue: customAnswer, hasRecommendedOptions: hasRecommendedOptions, hasAnyRecommendedInSession: hasAnyRecommendedInSession, showSessionSwitching: showSessionSwitching }) })] }));
|
|
23
|
+
};
|