auq-mcp-server 2.7.0 → 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/config/__tests__/ConfigLoader.test.js +7 -7
- package/dist/src/config/defaults.js +1 -1
- package/dist/src/config/types.js +1 -1
- 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,81 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useKeyboard } from "@opentui/react";
|
|
3
|
+
import { t } from "../../i18n/index.js";
|
|
4
|
+
import { useTheme } from "../ThemeProvider.js";
|
|
5
|
+
import { Footer } from "./Footer.js";
|
|
6
|
+
import { MarkdownPrompt } from "./MarkdownPrompt.js";
|
|
7
|
+
import { KEYS } from "../../tui/constants/keybindings.js";
|
|
8
|
+
/**
|
|
9
|
+
* ReviewScreen displays a summary of all answers for confirmation.
|
|
10
|
+
* User can press Enter to confirm and submit, or 'n' to go back and edit.
|
|
11
|
+
*/
|
|
12
|
+
export const ReviewScreen = ({ answers, elapsedLabel, onConfirm, onGoBack, questions, elaborateMarks, isSubmitting = false, }) => {
|
|
13
|
+
const { theme } = useTheme();
|
|
14
|
+
useKeyboard((key) => {
|
|
15
|
+
// Disable input while submitting
|
|
16
|
+
if (isSubmitting)
|
|
17
|
+
return;
|
|
18
|
+
if (key.name === "return") {
|
|
19
|
+
// Convert answers to UserAnswer format
|
|
20
|
+
const userAnswers = [];
|
|
21
|
+
answers.forEach((answer, questionIndex) => {
|
|
22
|
+
if (answer.selectedOption ||
|
|
23
|
+
answer.selectedOptions ||
|
|
24
|
+
answer.customText) {
|
|
25
|
+
userAnswers.push({
|
|
26
|
+
customText: answer.customText,
|
|
27
|
+
questionIndex,
|
|
28
|
+
selectedOption: answer.selectedOption,
|
|
29
|
+
selectedOptions: answer.selectedOptions,
|
|
30
|
+
timestamp: new Date().toISOString(),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
onConfirm(userAnswers);
|
|
35
|
+
}
|
|
36
|
+
if (key.name && KEYS.GO_BACK.test(key.name)) {
|
|
37
|
+
onGoBack();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return (_jsxs("box", { style: { flexDirection: "column" }, children: [_jsx("box", { style: {
|
|
41
|
+
flexDirection: "column",
|
|
42
|
+
borderColor: theme.components.review.confirmBorder,
|
|
43
|
+
borderStyle: "rounded",
|
|
44
|
+
marginBottom: 1,
|
|
45
|
+
paddingX: 1,
|
|
46
|
+
}, children: _jsxs("box", { style: { flexDirection: "row", justifyContent: "space-between", width: "100%" }, children: [_jsx("text", { style: { bold: true }, children: t("review.title") }), _jsx("text", { fg: theme.colors.textDim, children: elapsedLabel })] }) }), _jsx("box", { style: { flexDirection: "column", marginBottom: 1 }, children: questions.map((question, index) => {
|
|
47
|
+
const answer = answers.get(index);
|
|
48
|
+
const questionId = `Q${index}`;
|
|
49
|
+
const questionTitle = question.title || questionId;
|
|
50
|
+
return (_jsxs("box", { style: { flexDirection: "column", marginBottom: 1 }, children: [_jsxs("box", { style: { flexDirection: "column" }, children: [_jsxs("box", { children: [_jsx("text", { fg: theme.components.review.questionId, children: `[${questionId}] ` }), _jsx("text", { style: { bold: true }, children: `${questionTitle}. ` })] }), _jsx(MarkdownPrompt, { text: question.prompt })] }), _jsxs("box", { style: { flexDirection: "column", marginLeft: 2, marginTop: 1 }, children: [answer?.selectedOptions &&
|
|
51
|
+
answer.selectedOptions.length > 0 &&
|
|
52
|
+
answer.selectedOptions.map((option, idx) => (_jsx("text", { style: { fg: theme.components.review.selectedOption }, children: `> ${option}` }, idx))), answer?.selectedOption && (_jsx("text", { style: { fg: theme.components.review.selectedOption }, children: `> ${answer.selectedOption}` })), answer?.customText && (_jsx("box", { style: { flexDirection: "column" }, children: answer.customText
|
|
53
|
+
.replace(/\r\n?/g, "\n")
|
|
54
|
+
.split("\n")
|
|
55
|
+
.map((line, lineIndex, lines) => {
|
|
56
|
+
const isFirstLine = lineIndex === 0;
|
|
57
|
+
const isLastLine = lineIndex === lines.length - 1;
|
|
58
|
+
return (_jsx("text", { style: { fg: theme.components.review.customAnswer }, children: `${isFirstLine ? ` ${t("review.customAnswer")}: "` : " "}${line}${isLastLine ? '"' : ""}` }, lineIndex));
|
|
59
|
+
}) })), !answer?.selectedOption &&
|
|
60
|
+
!answer?.selectedOptions &&
|
|
61
|
+
!answer?.customText && (_jsxs("box", { style: { flexDirection: "row" }, children: [_jsx("text", { style: { fg: theme.colors.unansweredHighlight }, children: ` ${t("review.unanswered")}` }), elaborateMarks?.has(index) && (() => {
|
|
62
|
+
const elaborateText = elaborateMarks.get(index);
|
|
63
|
+
const elaborationStr = elaborateText
|
|
64
|
+
? `, ${t("review.markedForElaboration")}: "${elaborateText.length > 50 ? elaborateText.slice(0, 50) + "..." : elaborateText}"`
|
|
65
|
+
: `, ${t("review.markedForElaboration")}`;
|
|
66
|
+
return (_jsx("text", { style: { fg: theme.colors.warning }, children: elaborationStr }));
|
|
67
|
+
})()] })), (answer?.selectedOption ||
|
|
68
|
+
answer?.selectedOptions ||
|
|
69
|
+
answer?.customText) &&
|
|
70
|
+
elaborateMarks?.has(index) && (_jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { style: { fg: theme.colors.warning }, children: (() => {
|
|
71
|
+
const elaborateText = elaborateMarks.get(index);
|
|
72
|
+
if (elaborateText) {
|
|
73
|
+
const displayText = elaborateText.length > 50
|
|
74
|
+
? elaborateText.slice(0, 50) + "..."
|
|
75
|
+
: elaborateText;
|
|
76
|
+
return `${t("review.markedForElaboration")}: "${displayText}"`;
|
|
77
|
+
}
|
|
78
|
+
return t("review.markedForElaboration");
|
|
79
|
+
})() }) }))] })] }, index));
|
|
80
|
+
}) }), _jsx(Footer, { focusContext: "option", multiSelect: false, isReviewScreen: true, isSubmitting: isSubmitting })] }));
|
|
81
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useTheme } from "../ThemeProvider.js";
|
|
3
|
+
/* ------------------------------------------------------------------ */
|
|
4
|
+
/* Helpers */
|
|
5
|
+
/* ------------------------------------------------------------------ */
|
|
6
|
+
/** Check whether at least one answer in the map has meaningful content. */
|
|
7
|
+
function hasAnswers(answers) {
|
|
8
|
+
if (!answers || answers.size === 0)
|
|
9
|
+
return false;
|
|
10
|
+
for (const ans of answers.values()) {
|
|
11
|
+
if (ans.selectedOption || ans.customText)
|
|
12
|
+
return true;
|
|
13
|
+
if (ans.selectedOptions && ans.selectedOptions.length > 0)
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
/* ------------------------------------------------------------------ */
|
|
19
|
+
/* Component */
|
|
20
|
+
/* ------------------------------------------------------------------ */
|
|
21
|
+
/**
|
|
22
|
+
* SessionDots — a compact row of numbered dots rendered below the footer.
|
|
23
|
+
*
|
|
24
|
+
* Visual language:
|
|
25
|
+
* ● 1 ○ 2 ✕ 3 ○ 4
|
|
26
|
+
*
|
|
27
|
+
* • Active session: filled ● + bold number in theme primary
|
|
28
|
+
* • Abandoned: red ✕ + \"(AI disconnected)\" when active
|
|
29
|
+
* • Stale: yellow ○ + \"(stale)\" when active
|
|
30
|
+
* • Has answers: green (theme.success)
|
|
31
|
+
* • Touched/no answers: yellow (theme.warning)
|
|
32
|
+
* • Untouched: dim (theme.textDim)
|
|
33
|
+
*/
|
|
34
|
+
export const SessionDots = ({ sessions, activeIndex, sessionUIStates, onSelectIndex, }) => {
|
|
35
|
+
const { theme } = useTheme();
|
|
36
|
+
// Don't render when fewer than 2 sessions
|
|
37
|
+
if (sessions.length < 2)
|
|
38
|
+
return null;
|
|
39
|
+
return (_jsx("box", { style: { flexDirection: "row", justifyContent: "center", paddingLeft: 2, paddingRight: 2 }, children: sessions.map((session, idx) => {
|
|
40
|
+
const isActive = idx === activeIndex;
|
|
41
|
+
const uiState = sessionUIStates[session.sessionId];
|
|
42
|
+
const isStale = session.isStale ?? false;
|
|
43
|
+
const isAbandoned = session.isAbandoned ?? false;
|
|
44
|
+
// Determine the progress color for this session's dot
|
|
45
|
+
// Abandoned/stale take priority over normal state colors
|
|
46
|
+
let dotColor;
|
|
47
|
+
if (isAbandoned) {
|
|
48
|
+
dotColor =
|
|
49
|
+
theme.components.sessionDots
|
|
50
|
+
.abandoned ?? theme.colors.error;
|
|
51
|
+
}
|
|
52
|
+
else if (isStale) {
|
|
53
|
+
dotColor =
|
|
54
|
+
theme.components.sessionDots
|
|
55
|
+
.stale ?? theme.colors.warning;
|
|
56
|
+
}
|
|
57
|
+
else if (isActive) {
|
|
58
|
+
dotColor = theme.components.sessionDots.active;
|
|
59
|
+
}
|
|
60
|
+
else if (uiState && hasAnswers(uiState.answers)) {
|
|
61
|
+
dotColor = theme.components.sessionDots.answered;
|
|
62
|
+
}
|
|
63
|
+
else if (uiState) {
|
|
64
|
+
dotColor = theme.components.sessionDots.inProgress;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
dotColor = theme.components.sessionDots.untouched;
|
|
68
|
+
}
|
|
69
|
+
// Abandoned inactive sessions use ✕ to signal a problem
|
|
70
|
+
const dot = isAbandoned && !isActive
|
|
71
|
+
? "✕"
|
|
72
|
+
: isActive
|
|
73
|
+
? "●"
|
|
74
|
+
: "○";
|
|
75
|
+
const numberColor = isActive
|
|
76
|
+
? theme.components.sessionDots.activeNumber
|
|
77
|
+
: theme.components.sessionDots.number;
|
|
78
|
+
// Status label shown next to active abandoned/stale sessions
|
|
79
|
+
const statusLabel = isActive && isAbandoned
|
|
80
|
+
? "(AI disconnected)"
|
|
81
|
+
: isActive && isStale
|
|
82
|
+
? "(stale)"
|
|
83
|
+
: null;
|
|
84
|
+
return (_jsxs("box", { style: { flexDirection: "row", paddingRight: idx < sessions.length - 1 ? 1 : 0 }, onMouseDown: () => onSelectIndex?.(idx), children: [_jsx("text", { style: { fg: dotColor, bold: isActive }, children: dot }), _jsx("text", { style: { fg: numberColor, bold: isActive }, children: ` ${idx + 1}` }), statusLabel ? (_jsx("text", { style: { fg: dotColor, dim: true }, children: ` ${statusLabel}` })) : null] }, session.sessionId));
|
|
85
|
+
}) }));
|
|
86
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
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 { useTheme } from "../ThemeProvider.js";
|
|
6
|
+
import { formatRelativeTime } from "../../tui/shared/utils/relativeTime.js";
|
|
7
|
+
import { KEYS } from "../../tui/constants/keybindings.js";
|
|
8
|
+
import { useTerminalDimensions } from "../hooks/useTerminalDimensions.js";
|
|
9
|
+
/* ------------------------------------------------------------------ */
|
|
10
|
+
/* Helpers */
|
|
11
|
+
/* ------------------------------------------------------------------ */
|
|
12
|
+
function countAnswered(answers) {
|
|
13
|
+
if (!answers)
|
|
14
|
+
return 0;
|
|
15
|
+
let count = 0;
|
|
16
|
+
for (const ans of answers.values()) {
|
|
17
|
+
if (ans.selectedOption || ans.customText) {
|
|
18
|
+
count++;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (ans.selectedOptions && ans.selectedOptions.length > 0)
|
|
22
|
+
count++;
|
|
23
|
+
}
|
|
24
|
+
return count;
|
|
25
|
+
}
|
|
26
|
+
function truncate(text, max) {
|
|
27
|
+
if (text.length <= max)
|
|
28
|
+
return text;
|
|
29
|
+
return text.slice(0, max - 1) + "\u2026";
|
|
30
|
+
}
|
|
31
|
+
/* ------------------------------------------------------------------ */
|
|
32
|
+
/* Component */
|
|
33
|
+
/* ------------------------------------------------------------------ */
|
|
34
|
+
/**
|
|
35
|
+
* SessionPicker \u2014 a modal overlay listing all queued sessions.
|
|
36
|
+
*
|
|
37
|
+
* Opened via Ctrl+S. Each row shows:
|
|
38
|
+
* {index}. {title} \u2014 {workDir} [{answered}/{total}] {age}
|
|
39
|
+
*
|
|
40
|
+
* Navigation:
|
|
41
|
+
* \u2191 / \u2193 : move highlight
|
|
42
|
+
* Enter : select highlighted session
|
|
43
|
+
* Esc : close without switching
|
|
44
|
+
*/
|
|
45
|
+
export const SessionPicker = ({ isOpen, sessions, activeIndex, sessionUIStates, onSelectIndex, onClose, }) => {
|
|
46
|
+
const { theme } = useTheme();
|
|
47
|
+
const { width: termWidth, height: termHeight } = useTerminalDimensions();
|
|
48
|
+
// \u2500\u2500 Highlight state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
49
|
+
const [highlightIndex, setHighlightIndex] = useState(activeIndex);
|
|
50
|
+
// Reset highlight to active index each time the picker opens
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (isOpen) {
|
|
53
|
+
setHighlightIndex(activeIndex);
|
|
54
|
+
}
|
|
55
|
+
}, [isOpen, activeIndex]);
|
|
56
|
+
// \u2500\u2500 Keyboard handling (only active when overlay is open) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
57
|
+
useKeyboard((key) => {
|
|
58
|
+
if (!isOpen)
|
|
59
|
+
return;
|
|
60
|
+
if (key.name === "up") {
|
|
61
|
+
setHighlightIndex((prev) => Math.max(0, prev - 1));
|
|
62
|
+
}
|
|
63
|
+
else if (key.name === "down") {
|
|
64
|
+
setHighlightIndex((prev) => Math.min(sessions.length - 1, prev + 1));
|
|
65
|
+
}
|
|
66
|
+
else if (key.name === "return") {
|
|
67
|
+
onSelectIndex(highlightIndex);
|
|
68
|
+
onClose();
|
|
69
|
+
}
|
|
70
|
+
else if (key.name === "escape") {
|
|
71
|
+
onClose();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Direct number jump (1-9)
|
|
75
|
+
const num = parseInt(key.sequence || key.name, 10);
|
|
76
|
+
if (num >= KEYS.SESSION_JUMP_MIN &&
|
|
77
|
+
num <= Math.min(KEYS.SESSION_JUMP_MAX, sessions.length)) {
|
|
78
|
+
onSelectIndex(num - 1);
|
|
79
|
+
onClose();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
// \u2500\u2500 Render nothing when closed \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
84
|
+
if (!isOpen)
|
|
85
|
+
return null;
|
|
86
|
+
// \u2500\u2500 Scrolling logic \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
87
|
+
const chromeLines = 7;
|
|
88
|
+
const maxVisibleRows = Math.max(1, termHeight - chromeLines);
|
|
89
|
+
const needsScroll = sessions.length > maxVisibleRows;
|
|
90
|
+
let scrollOffset = 0;
|
|
91
|
+
if (needsScroll) {
|
|
92
|
+
scrollOffset = Math.max(0, Math.min(highlightIndex - Math.floor(maxVisibleRows / 2), sessions.length - maxVisibleRows));
|
|
93
|
+
}
|
|
94
|
+
const visibleSessions = sessions.slice(scrollOffset, scrollOffset + maxVisibleRows);
|
|
95
|
+
// \u2500\u2500 Derive max label widths from terminal size \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
96
|
+
const innerWidth = Math.max(30, termWidth - 6);
|
|
97
|
+
const fixedOverhead = 18;
|
|
98
|
+
const dynamicWidth = Math.max(10, innerWidth - fixedOverhead);
|
|
99
|
+
const titleMax = Math.max(6, Math.floor(dynamicWidth * 0.55));
|
|
100
|
+
const dirMax = Math.max(4, dynamicWidth - titleMax);
|
|
101
|
+
return (_jsx("box", { style: {
|
|
102
|
+
flexDirection: "column",
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
justifyContent: "center",
|
|
105
|
+
flexGrow: 1,
|
|
106
|
+
}, children: _jsxs("box", { style: {
|
|
107
|
+
flexDirection: "column",
|
|
108
|
+
border: true,
|
|
109
|
+
borderStyle: "rounded",
|
|
110
|
+
borderColor: theme.colors.surface,
|
|
111
|
+
paddingX: 2,
|
|
112
|
+
paddingY: 1,
|
|
113
|
+
width: Math.min(innerWidth + 6, termWidth),
|
|
114
|
+
}, children: [_jsxs("box", { style: { flexDirection: "row", justifyContent: "center", marginBottom: 1 }, children: [_jsx("text", { style: { attributes: TextAttributes.BOLD, fg: theme.components.sessionPicker.title }, children: "Switch Session" }), _jsx("text", { style: { fg: theme.components.sessionPicker.rowDim }, children: ` (${sessions.length} queued)` })] }), needsScroll && scrollOffset > 0 && (_jsx("box", { style: { justifyContent: "center" }, children: _jsx("text", { style: { fg: theme.components.sessionPicker.rowDim }, children: "\\u25b2 more" }) })), _jsx("box", { onMouseScroll: (event) => {
|
|
115
|
+
if (event.scroll?.direction === "down") {
|
|
116
|
+
setHighlightIndex((prev) => Math.min(sessions.length - 1, prev + 1));
|
|
117
|
+
}
|
|
118
|
+
else if (event.scroll?.direction === "up") {
|
|
119
|
+
setHighlightIndex((prev) => Math.max(0, prev - 1));
|
|
120
|
+
}
|
|
121
|
+
}, children: visibleSessions.map((session, visibleIdx) => {
|
|
122
|
+
const realIdx = scrollOffset + visibleIdx;
|
|
123
|
+
const isHighlighted = realIdx === highlightIndex;
|
|
124
|
+
const isActive = realIdx === activeIndex;
|
|
125
|
+
const uiState = sessionUIStates[session.sessionId];
|
|
126
|
+
const questions = session.sessionRequest.questions;
|
|
127
|
+
const title = truncate(questions[0]?.title || "Untitled", titleMax);
|
|
128
|
+
const dir = truncate(session.sessionRequest.workingDirectory || "unknown", dirMax);
|
|
129
|
+
const total = questions.length;
|
|
130
|
+
const answered = countAnswered(uiState?.answers);
|
|
131
|
+
const age = formatRelativeTime(session.timestamp);
|
|
132
|
+
// Row colors
|
|
133
|
+
const rowBg = isHighlighted
|
|
134
|
+
? theme.components.sessionPicker.highlightBg
|
|
135
|
+
: undefined;
|
|
136
|
+
const textColor = isHighlighted
|
|
137
|
+
? theme.components.sessionPicker.highlightFg
|
|
138
|
+
: isActive
|
|
139
|
+
? theme.components.sessionPicker.activeMark
|
|
140
|
+
: theme.components.sessionPicker.rowText;
|
|
141
|
+
// Progress color
|
|
142
|
+
const progressColor = answered === total && total > 0
|
|
143
|
+
? theme.components.sessionPicker.progress
|
|
144
|
+
: answered > 0
|
|
145
|
+
? theme.components.sessionPicker.progress
|
|
146
|
+
: theme.components.sessionPicker.rowDim;
|
|
147
|
+
const isStaleOrAbandoned = session.isStale || session.isAbandoned;
|
|
148
|
+
return (_jsxs("box", { style: { flexDirection: "column", backgroundColor: isHighlighted ? theme.colors.surfaceAlt : undefined }, onMouseDown: () => { onSelectIndex(realIdx); onClose(); }, children: [_jsxs("box", { style: { flexDirection: "row" }, children: [isStaleOrAbandoned && (_jsx("text", { style: {
|
|
149
|
+
fg: theme.components.sessionPicker.staleIcon,
|
|
150
|
+
}, children: "⚠ " })), _jsx("text", { style: {
|
|
151
|
+
attributes: isHighlighted ? TextAttributes.BOLD : TextAttributes.NONE,
|
|
152
|
+
fg: isStaleOrAbandoned
|
|
153
|
+
? theme.components.sessionPicker.staleText
|
|
154
|
+
: textColor,
|
|
155
|
+
}, children: `${isActive ? "►" : " "} ${realIdx + 1}. ${title}` }), _jsx("text", { style: { fg: theme.components.sessionPicker.rowDim }, children: ` — ${dir}` }), _jsx("text", { style: { fg: progressColor }, children: ` [${answered}/${total}]` }), _jsx("text", { style: {
|
|
156
|
+
fg: isStaleOrAbandoned
|
|
157
|
+
? theme.components.sessionPicker.staleAge
|
|
158
|
+
: theme.components.sessionPicker.rowDim,
|
|
159
|
+
attributes: !isStaleOrAbandoned ? TextAttributes.DIM : TextAttributes.NONE,
|
|
160
|
+
}, children: age })] }), session.isStale && !session.isAbandoned && (_jsx("box", { style: { marginLeft: 4 }, children: _jsx("text", { style: { fg: theme.components.sessionPicker.staleSubtitle, attributes: TextAttributes.DIM }, children: "may be orphaned" }) })), session.isAbandoned && (_jsx("box", { style: { marginLeft: 4 }, children: _jsx("text", { style: { fg: theme.components.sessionPicker.staleSubtitle, attributes: TextAttributes.BOLD }, children: "session abandoned" }) }))] }, session.sessionId));
|
|
161
|
+
}) }), needsScroll && scrollOffset + maxVisibleRows < sessions.length && (_jsx("box", { style: { justifyContent: "center" }, children: _jsx("text", { style: { fg: theme.components.sessionPicker.rowDim }, children: "\\u25bc more" }) })), _jsx("box", { style: { justifyContent: "center", marginTop: 1 }, children: _jsx("text", { style: { fg: theme.components.sessionPicker.rowDim, attributes: TextAttributes.DIM }, children: "\u2191\u2193 navigate \u00B7 Enter select \u00B7 Esc close \u00B7 1-9 jump \u00B7 scroll" }) })] }) }));
|
|
162
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { t } from "../../i18n/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* SingleLineTextInput wraps OpenTUI's native <input> for single-line text entry.
|
|
5
|
+
* Used for rejection reasons and other short text fields.
|
|
6
|
+
*/
|
|
7
|
+
export const SingleLineTextInput = ({ isFocused = true, onChange, onSubmit, placeholder = t("input.singleLinePlaceholder"), value, }) => {
|
|
8
|
+
return (_jsx("input", { placeholder: placeholder, value: value, focused: isFocused, onInput: (val) => onChange(val), onSubmit: () => onSubmit?.() }));
|
|
9
|
+
};
|