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,536 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { createCliRenderer } from "@opentui/core";
|
|
3
|
+
import { createRoot, useKeyboard } from "@opentui/react";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import { DEFAULT_CONFIG } from "../config/defaults.js";
|
|
7
|
+
import { ConfigProvider } from "./ConfigContext.js";
|
|
8
|
+
import { ErrorBoundary } from "./components/ErrorBoundary.js";
|
|
9
|
+
import { ThemeProvider, useTheme } from "./ThemeProvider.js";
|
|
10
|
+
import { ensureDirectoryExists, getSessionDirectory, } from "../session/utils.js";
|
|
11
|
+
import { createTUIWatcher, } from "../tui/session-watcher.js";
|
|
12
|
+
import { isSessionStale, isSessionAbandoned, formatStaleToastMessage, } from "../tui/shared/utils/staleDetection.js";
|
|
13
|
+
import { getAdjustedIndexAfterRemoval, getDirectJumpIndex, getNextSessionIndex, getPrevSessionIndex, } from "../tui/shared/utils/sessionSwitching.js";
|
|
14
|
+
import { UpdateChecker, fetchChangelog, installUpdate, detectPackageManager, readCache, writeCache, } from "../update/index.js";
|
|
15
|
+
import { KEYS } from "../tui/constants/keybindings.js";
|
|
16
|
+
import { Header as _Header } from "./components/Header.js";
|
|
17
|
+
import { WaitingScreen as _WaitingScreen } from "./components/WaitingScreen.js";
|
|
18
|
+
import { StepperView as _StepperView } from "./components/StepperView.js";
|
|
19
|
+
import { SessionDots as _SessionDots } from "./components/SessionDots.js";
|
|
20
|
+
import { SessionPicker as _SessionPicker } from "./components/SessionPicker.js";
|
|
21
|
+
import { UpdateOverlay as _UpdateOverlay } from "./components/UpdateOverlay.js";
|
|
22
|
+
import { Toast as _Toast } from "./components/Toast.js";
|
|
23
|
+
import { ThemeIndicator as _ThemeIndicator } from "./components/ThemeIndicator.js";
|
|
24
|
+
// Cast to FC to avoid React class component type mismatch between @opentui/react
|
|
25
|
+
// bundled React version and the project's @types/react (structural type incompatibility).
|
|
26
|
+
// ErrorBoundary is still a valid class component at runtime.
|
|
27
|
+
const BoundedErrorBoundary = ErrorBoundary;
|
|
28
|
+
const Header = _Header;
|
|
29
|
+
const WaitingScreen = _WaitingScreen;
|
|
30
|
+
const StepperView = _StepperView;
|
|
31
|
+
const SessionDots = _SessionDots;
|
|
32
|
+
const SessionPicker = _SessionPicker;
|
|
33
|
+
const UpdateOverlay = _UpdateOverlay;
|
|
34
|
+
const Toast = _Toast;
|
|
35
|
+
const ThemeIndicator = _ThemeIndicator;
|
|
36
|
+
// Inner App component that has access to ThemeProvider context
|
|
37
|
+
function AppInner({ config }) {
|
|
38
|
+
const { cycleTheme, theme } = useTheme();
|
|
39
|
+
const [state, setState] = useState({ mode: "WAITING" });
|
|
40
|
+
const [sessionQueue, setSessionQueue] = useState([]);
|
|
41
|
+
const [activeSessionIndex, setActiveSessionIndex] = useState(0);
|
|
42
|
+
const [sessionUIStates, setSessionUIStates] = useState({});
|
|
43
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
44
|
+
const [toast, setToast] = useState(null);
|
|
45
|
+
const [showSessionPicker, setShowSessionPicker] = useState(false);
|
|
46
|
+
const [isInReviewOrRejection, setIsInReviewOrRejection] = useState(false);
|
|
47
|
+
const [sessionMeta, setSessionMeta] = useState(new Map());
|
|
48
|
+
const [lastInteractions, setLastInteractions] = useState(new Map());
|
|
49
|
+
const [staleToastShown, setStaleToastShown] = useState(new Set());
|
|
50
|
+
const [updateInfo, setUpdateInfo] = useState(null);
|
|
51
|
+
const [showUpdateOverlay, setShowUpdateOverlay] = useState(false);
|
|
52
|
+
const [isInstallingUpdate, setIsInstallingUpdate] = useState(false);
|
|
53
|
+
const [installError, setInstallError] = useState(null);
|
|
54
|
+
const [changelogContent, setChangelogContent] = useState(null);
|
|
55
|
+
const [updateDismissed, setUpdateDismissed] = useState(false);
|
|
56
|
+
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
|
|
57
|
+
const sessionDir = getSessionDirectory();
|
|
58
|
+
// ── Show toast helper ────────────────────────────────────────
|
|
59
|
+
const showToast = useCallback((message, type = "success", title) => {
|
|
60
|
+
setToast({ message, type, title });
|
|
61
|
+
}, []);
|
|
62
|
+
// ── Initialize: load existing sessions + start persistent watcher ───
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
let watcherInstance = null;
|
|
65
|
+
const initialize = async () => {
|
|
66
|
+
try {
|
|
67
|
+
await ensureDirectoryExists(sessionDir);
|
|
68
|
+
// Load existing pending sessions
|
|
69
|
+
const watcher = createTUIWatcher();
|
|
70
|
+
const sessionIds = await watcher.getPendingSessions();
|
|
71
|
+
const sessionsWithStatus = await watcher.getPendingSessionsWithStatus();
|
|
72
|
+
const sessionData = await Promise.all(sessionIds.map(async (sessionId) => {
|
|
73
|
+
const sessionRequest = await watcher.getSessionRequest(sessionId);
|
|
74
|
+
if (!sessionRequest)
|
|
75
|
+
return null;
|
|
76
|
+
return {
|
|
77
|
+
sessionId,
|
|
78
|
+
sessionRequest,
|
|
79
|
+
timestamp: new Date(sessionRequest.timestamp),
|
|
80
|
+
};
|
|
81
|
+
}));
|
|
82
|
+
const validSessions = sessionData
|
|
83
|
+
.filter((s) => s !== null)
|
|
84
|
+
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
85
|
+
setSessionQueue(validSessions);
|
|
86
|
+
const initialMeta = new Map();
|
|
87
|
+
for (const meta of sessionsWithStatus) {
|
|
88
|
+
initialMeta.set(meta.sessionId, { status: meta.status, createdAt: meta.createdAt });
|
|
89
|
+
}
|
|
90
|
+
setSessionMeta(initialMeta);
|
|
91
|
+
setIsInitialized(true);
|
|
92
|
+
// Start persistent watcher for new sessions
|
|
93
|
+
watcherInstance = createTUIWatcher({ autoLoadData: true });
|
|
94
|
+
watcherInstance.startEnhancedWatching((event) => {
|
|
95
|
+
setSessionQueue((prev) => {
|
|
96
|
+
if (prev.some((s) => s.sessionId === event.sessionId))
|
|
97
|
+
return prev;
|
|
98
|
+
return [
|
|
99
|
+
...prev,
|
|
100
|
+
{
|
|
101
|
+
sessionId: event.sessionId,
|
|
102
|
+
sessionRequest: event.sessionRequest,
|
|
103
|
+
timestamp: new Date(event.timestamp),
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.error("Failed to initialize:", error);
|
|
111
|
+
setIsInitialized(true);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
void initialize();
|
|
115
|
+
return () => {
|
|
116
|
+
if (watcherInstance)
|
|
117
|
+
watcherInstance.stop();
|
|
118
|
+
};
|
|
119
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
120
|
+
}, []);
|
|
121
|
+
// ── Auto-update checker ─────────────────────────────────────
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (config.updateCheck === false)
|
|
124
|
+
return;
|
|
125
|
+
if (process.env.NO_UPDATE_NOTIFIER === "1")
|
|
126
|
+
return;
|
|
127
|
+
if (process.env.CI === "true" || process.env.CI === "1")
|
|
128
|
+
return;
|
|
129
|
+
if (process.env.NODE_ENV === "test")
|
|
130
|
+
return;
|
|
131
|
+
if (!process.stdout.isTTY)
|
|
132
|
+
return;
|
|
133
|
+
const checker = new UpdateChecker();
|
|
134
|
+
let intervalId = null;
|
|
135
|
+
const runCheck = async () => {
|
|
136
|
+
try {
|
|
137
|
+
const result = await checker.check();
|
|
138
|
+
if (result) {
|
|
139
|
+
setUpdateInfo(result);
|
|
140
|
+
const changelog = await fetchChangelog(result.latestVersion);
|
|
141
|
+
setChangelogContent(changelog.content);
|
|
142
|
+
if (result.updateType === "patch" && !updateDismissed) {
|
|
143
|
+
try {
|
|
144
|
+
const pm = detectPackageManager();
|
|
145
|
+
const success = await installUpdate(pm);
|
|
146
|
+
if (success) {
|
|
147
|
+
showToast(`Updated to v${result.latestVersion}. Please restart auq.`, "success");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Silent — patch auto-install is best-effort
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (!updateDismissed) {
|
|
155
|
+
setShowUpdateOverlay(true);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Silently fail — update checks should never break the TUI
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
setIsCheckingUpdate(true);
|
|
164
|
+
void runCheck().finally(() => { setIsCheckingUpdate(false); });
|
|
165
|
+
intervalId = setInterval(() => {
|
|
166
|
+
checker.clearCache();
|
|
167
|
+
void runCheck();
|
|
168
|
+
}, 3600000); // 1 hour
|
|
169
|
+
return () => {
|
|
170
|
+
if (intervalId)
|
|
171
|
+
clearInterval(intervalId);
|
|
172
|
+
};
|
|
173
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
174
|
+
}, [config.updateCheck]);
|
|
175
|
+
// ── Auto-transition: WAITING ↔ PROCESSING ────────────────────
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (!isInitialized)
|
|
178
|
+
return;
|
|
179
|
+
if (state.mode === "WAITING" && sessionQueue.length > 0) {
|
|
180
|
+
setState({ mode: "PROCESSING" });
|
|
181
|
+
setActiveSessionIndex(0);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (state.mode === "PROCESSING" && sessionQueue.length === 0) {
|
|
185
|
+
setState({ mode: "WAITING" });
|
|
186
|
+
setActiveSessionIndex(0);
|
|
187
|
+
}
|
|
188
|
+
}, [state.mode, sessionQueue.length, isInitialized]);
|
|
189
|
+
// ── Stale detection + background session status polling ──────
|
|
190
|
+
const sessionQueueRef = useRef(sessionQueue);
|
|
191
|
+
sessionQueueRef.current = sessionQueue;
|
|
192
|
+
const activeSessionIndexRef = useRef(activeSessionIndex);
|
|
193
|
+
activeSessionIndexRef.current = activeSessionIndex;
|
|
194
|
+
const statusIntervalRef = useRef(null);
|
|
195
|
+
const staleIntervalRef = useRef(null);
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (state.mode !== "PROCESSING" || sessionQueue.length <= 1)
|
|
198
|
+
return;
|
|
199
|
+
let isCancelled = false;
|
|
200
|
+
let isChecking = false;
|
|
201
|
+
const checkPausedSessionStatuses = async () => {
|
|
202
|
+
if (isCancelled || isChecking)
|
|
203
|
+
return;
|
|
204
|
+
isChecking = true;
|
|
205
|
+
try {
|
|
206
|
+
const queue = sessionQueueRef.current;
|
|
207
|
+
const activeIdx = activeSessionIndexRef.current;
|
|
208
|
+
const checks = await Promise.all(queue.map(async (session, index) => {
|
|
209
|
+
if (index === activeIdx)
|
|
210
|
+
return null;
|
|
211
|
+
const statusPath = `${sessionDir}/${session.sessionId}/status.json`;
|
|
212
|
+
try {
|
|
213
|
+
const content = await fs.readFile(statusPath, "utf8");
|
|
214
|
+
const parsed = JSON.parse(content);
|
|
215
|
+
if (parsed.status === "timed_out" ||
|
|
216
|
+
parsed.status === "completed" ||
|
|
217
|
+
parsed.status === "rejected") {
|
|
218
|
+
return { notifyAsTimedOut: parsed.status === "timed_out", session };
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return { notifyAsTimedOut: true, session };
|
|
224
|
+
}
|
|
225
|
+
}));
|
|
226
|
+
if (isCancelled)
|
|
227
|
+
return;
|
|
228
|
+
const sessionsToRemove = checks.filter((entry) => entry !== null);
|
|
229
|
+
if (sessionsToRemove.length === 0)
|
|
230
|
+
return;
|
|
231
|
+
const timedOutSession = sessionsToRemove.find((entry) => entry.notifyAsTimedOut);
|
|
232
|
+
if (timedOutSession) {
|
|
233
|
+
const title = timedOutSession.session.sessionRequest.questions[0]?.title ||
|
|
234
|
+
timedOutSession.session.sessionId.slice(0, 8);
|
|
235
|
+
showToast(`Session '${title}' timed out`, "info");
|
|
236
|
+
}
|
|
237
|
+
const idsToRemove = new Set(sessionsToRemove.map((entry) => entry.session.sessionId));
|
|
238
|
+
setSessionUIStates((prev) => {
|
|
239
|
+
const next = { ...prev };
|
|
240
|
+
for (const sessionId of idsToRemove)
|
|
241
|
+
delete next[sessionId];
|
|
242
|
+
return next;
|
|
243
|
+
});
|
|
244
|
+
setSessionQueue((prev) => {
|
|
245
|
+
let nextQueue = [...prev];
|
|
246
|
+
let nextActiveIndex = activeSessionIndexRef.current;
|
|
247
|
+
const removalIndices = Array.from(idsToRemove)
|
|
248
|
+
.map((sessionId) => nextQueue.findIndex((s) => s.sessionId === sessionId))
|
|
249
|
+
.filter((idx) => idx !== -1)
|
|
250
|
+
.sort((a, b) => b - a);
|
|
251
|
+
for (const removalIndex of removalIndices) {
|
|
252
|
+
nextQueue = nextQueue.filter((_, idx) => idx !== removalIndex);
|
|
253
|
+
nextActiveIndex = getAdjustedIndexAfterRemoval(removalIndex, nextActiveIndex, nextQueue.length);
|
|
254
|
+
}
|
|
255
|
+
setActiveSessionIndex(nextActiveIndex);
|
|
256
|
+
setState(nextQueue.length === 0 ? { mode: "WAITING" } : { mode: "PROCESSING" });
|
|
257
|
+
return nextQueue;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
isChecking = false;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
const interval = setInterval(() => { void checkPausedSessionStatuses(); }, 2000);
|
|
265
|
+
statusIntervalRef.current = interval;
|
|
266
|
+
// Stale detection
|
|
267
|
+
const staleThreshold = config.staleThreshold ?? 7200000;
|
|
268
|
+
const notifyOnStale = config.notifyOnStale ?? true;
|
|
269
|
+
const runStaleDetection = async () => {
|
|
270
|
+
const watcher = createTUIWatcher();
|
|
271
|
+
let freshMeta = [];
|
|
272
|
+
try {
|
|
273
|
+
freshMeta = await watcher.getPendingSessionsWithStatus();
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// Non-critical
|
|
277
|
+
}
|
|
278
|
+
if (freshMeta.length > 0) {
|
|
279
|
+
setSessionMeta((prev) => {
|
|
280
|
+
const next = new Map(prev);
|
|
281
|
+
for (const meta of freshMeta) {
|
|
282
|
+
next.set(meta.sessionId, { status: meta.status, createdAt: meta.createdAt });
|
|
283
|
+
}
|
|
284
|
+
return next;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const queue = sessionQueueRef.current;
|
|
288
|
+
for (const session of queue) {
|
|
289
|
+
const stale = isSessionStale(session.timestamp.getTime(), staleThreshold, lastInteractions.get(session.sessionId));
|
|
290
|
+
if (stale && notifyOnStale && !staleToastShown.has(session.sessionId)) {
|
|
291
|
+
const title = session.sessionRequest.questions[0]?.title ?? session.sessionId.slice(0, 8);
|
|
292
|
+
showToast(formatStaleToastMessage(title, session.timestamp.getTime()), "info");
|
|
293
|
+
setStaleToastShown((prev) => new Set(prev).add(session.sessionId));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const staleInterval = setInterval(() => { void runStaleDetection(); }, 2000);
|
|
298
|
+
staleIntervalRef.current = staleInterval;
|
|
299
|
+
return () => {
|
|
300
|
+
isCancelled = true;
|
|
301
|
+
clearInterval(interval);
|
|
302
|
+
clearInterval(staleInterval);
|
|
303
|
+
statusIntervalRef.current = null;
|
|
304
|
+
staleIntervalRef.current = null;
|
|
305
|
+
};
|
|
306
|
+
}, [activeSessionIndex, sessionDir, sessionQueue, state.mode, config.staleThreshold, config.notifyOnStale, lastInteractions, staleToastShown, showToast]);
|
|
307
|
+
// ── Session switching helper ──────────────────────────────────
|
|
308
|
+
const switchToSession = useCallback((targetIndex) => {
|
|
309
|
+
if (state.mode !== "PROCESSING" || sessionQueueRef.current.length <= 1)
|
|
310
|
+
return;
|
|
311
|
+
const clampedIndex = Math.max(0, Math.min(targetIndex, sessionQueueRef.current.length - 1));
|
|
312
|
+
if (clampedIndex === activeSessionIndexRef.current)
|
|
313
|
+
return;
|
|
314
|
+
setActiveSessionIndex(clampedIndex);
|
|
315
|
+
setShowSessionPicker(false);
|
|
316
|
+
setLastInteractions((prev) => {
|
|
317
|
+
const session = sessionQueueRef.current[clampedIndex];
|
|
318
|
+
if (!session)
|
|
319
|
+
return prev;
|
|
320
|
+
return new Map(prev).set(session.sessionId, Date.now());
|
|
321
|
+
});
|
|
322
|
+
}, [state.mode]);
|
|
323
|
+
// ── Keyboard shortcuts ────────────────────────────────────────
|
|
324
|
+
const activeSession = state.mode === "PROCESSING" ? sessionQueue[activeSessionIndex] : undefined;
|
|
325
|
+
const canUseDirectJump = !activeSession ||
|
|
326
|
+
sessionUIStates[activeSession.sessionId]?.focusContext === "option" ||
|
|
327
|
+
sessionUIStates[activeSession.sessionId] === undefined;
|
|
328
|
+
const isNavActive = state.mode === "PROCESSING" &&
|
|
329
|
+
!isInReviewOrRejection &&
|
|
330
|
+
!showSessionPicker &&
|
|
331
|
+
!showUpdateOverlay &&
|
|
332
|
+
sessionQueue.length >= 2;
|
|
333
|
+
useKeyboard((key) => {
|
|
334
|
+
if (!isNavActive)
|
|
335
|
+
return;
|
|
336
|
+
// Ctrl+S / Ctrl+L: open session picker
|
|
337
|
+
if (key.ctrl && (key.name === "s" || key.sequence === "\x13" || key.name === "l" || key.sequence === "\x0c")) {
|
|
338
|
+
setShowSessionPicker(true);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!key.ctrl && !key.meta) {
|
|
342
|
+
const seq = key.sequence || key.name || "";
|
|
343
|
+
// Session navigation: ] and [
|
|
344
|
+
if (seq === KEYS.SESSION_NEXT && canUseDirectJump) {
|
|
345
|
+
switchToSession(getNextSessionIndex(activeSessionIndex, sessionQueue.length));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (seq === KEYS.SESSION_PREV && canUseDirectJump) {
|
|
349
|
+
switchToSession(getPrevSessionIndex(activeSessionIndex, sessionQueue.length));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// 1-9: jump to session
|
|
353
|
+
if (/^[1-9]$/.test(seq) && canUseDirectJump) {
|
|
354
|
+
const keyNumber = Number(seq);
|
|
355
|
+
const targetIndex = getDirectJumpIndex(keyNumber, activeSessionIndex, sessionQueue.length);
|
|
356
|
+
if (targetIndex !== null)
|
|
357
|
+
switchToSession(targetIndex);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// u: activate update overlay
|
|
361
|
+
if (seq === KEYS.UPDATE && updateInfo && !showUpdateOverlay) {
|
|
362
|
+
setShowUpdateOverlay(true);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// t: cycle theme
|
|
366
|
+
if (seq === "t") {
|
|
367
|
+
cycleTheme();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
// Ctrl+S outside isNavActive (fewer conditions)
|
|
373
|
+
useKeyboard((key) => {
|
|
374
|
+
if (state.mode !== "PROCESSING")
|
|
375
|
+
return;
|
|
376
|
+
if (showSessionPicker || showUpdateOverlay)
|
|
377
|
+
return;
|
|
378
|
+
if (key.ctrl && (key.sequence === "\x13" || key.name === "s")) {
|
|
379
|
+
setShowSessionPicker(true);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
// ── Update overlay handlers ────────────────────────────────────
|
|
383
|
+
const handleUpdateInstall = async () => {
|
|
384
|
+
try {
|
|
385
|
+
setIsInstallingUpdate(true);
|
|
386
|
+
setInstallError(null);
|
|
387
|
+
const pm = detectPackageManager();
|
|
388
|
+
const success = await installUpdate(pm);
|
|
389
|
+
if (success) {
|
|
390
|
+
setShowUpdateOverlay(false);
|
|
391
|
+
showToast(`Updated to v${updateInfo.latestVersion}. Please restart auq.`, "success");
|
|
392
|
+
setTimeout(() => process.exit(0), 2000);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
setInstallError("Installation failed. Please try manually.");
|
|
396
|
+
}
|
|
397
|
+
setIsInstallingUpdate(false);
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
setIsInstallingUpdate(false);
|
|
401
|
+
setInstallError(err instanceof Error ? err.message : "Installation failed");
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
const handleSkipVersion = async () => {
|
|
405
|
+
if (updateInfo) {
|
|
406
|
+
try {
|
|
407
|
+
const cache = await readCache();
|
|
408
|
+
if (cache) {
|
|
409
|
+
await writeCache({ ...cache, skippedVersion: updateInfo.latestVersion });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
// Non-critical
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
setShowUpdateOverlay(false);
|
|
417
|
+
setUpdateInfo(null);
|
|
418
|
+
};
|
|
419
|
+
const handleRemindLater = () => {
|
|
420
|
+
setShowUpdateOverlay(false);
|
|
421
|
+
setUpdateDismissed(true);
|
|
422
|
+
};
|
|
423
|
+
// ── Session completion handler ─────────────────────────────────
|
|
424
|
+
const handleSessionComplete = (wasRejected = false, rejectionReason) => {
|
|
425
|
+
if (wasRejected) {
|
|
426
|
+
if (rejectionReason) {
|
|
427
|
+
showToast(`Reason: ${rejectionReason}`, "info", "🙅 Question set rejected");
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
showToast("Question set rejected", "info");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
showToast("✅ Answers submitted successfully!", "success");
|
|
435
|
+
}
|
|
436
|
+
const completedSession = sessionQueue[activeSessionIndex];
|
|
437
|
+
if (completedSession) {
|
|
438
|
+
setSessionUIStates((prev) => {
|
|
439
|
+
if (!(completedSession.sessionId in prev))
|
|
440
|
+
return prev;
|
|
441
|
+
const next = { ...prev };
|
|
442
|
+
delete next[completedSession.sessionId];
|
|
443
|
+
return next;
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
setSessionQueue((prev) => {
|
|
447
|
+
const removedIndex = activeSessionIndex;
|
|
448
|
+
const nextQueue = prev.filter((_, i) => i !== removedIndex);
|
|
449
|
+
const nextActiveIndex = getAdjustedIndexAfterRemoval(removedIndex, activeSessionIndex, nextQueue.length);
|
|
450
|
+
setActiveSessionIndex(nextActiveIndex);
|
|
451
|
+
setState(nextQueue.length === 0 ? { mode: "WAITING" } : { mode: "PROCESSING" });
|
|
452
|
+
if (nextQueue.length === 0) {
|
|
453
|
+
if (statusIntervalRef.current) {
|
|
454
|
+
clearInterval(statusIntervalRef.current);
|
|
455
|
+
statusIntervalRef.current = null;
|
|
456
|
+
}
|
|
457
|
+
if (staleIntervalRef.current) {
|
|
458
|
+
clearInterval(staleIntervalRef.current);
|
|
459
|
+
staleIntervalRef.current = null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return nextQueue;
|
|
463
|
+
});
|
|
464
|
+
};
|
|
465
|
+
// ── State snapshot handler ─────────────────────────────────────
|
|
466
|
+
const handleStateSnapshot = useCallback((sessionId, ui) => {
|
|
467
|
+
setSessionUIStates((prev) => ({ ...prev, [sessionId]: ui }));
|
|
468
|
+
setLastInteractions((prev) => new Map(prev).set(sessionId, Date.now()));
|
|
469
|
+
}, []);
|
|
470
|
+
// ── Flow state change handler ──────────────────────────────────
|
|
471
|
+
const handleFlowStateChange = useCallback((flowState) => {
|
|
472
|
+
setIsInReviewOrRejection(flowState.showReview || flowState.showRejectionConfirm);
|
|
473
|
+
}, []);
|
|
474
|
+
// ── Render ────────────────────────────────────────────────────
|
|
475
|
+
const staleThreshold = config.staleThreshold ?? 7200000;
|
|
476
|
+
// Compute derived session data for SessionDots and SessionPicker
|
|
477
|
+
const sessionsWithMeta = useMemo(() => sessionQueue.map((s) => ({
|
|
478
|
+
...s,
|
|
479
|
+
isStale: isSessionStale(s.timestamp.getTime(), staleThreshold, lastInteractions.get(s.sessionId)),
|
|
480
|
+
isAbandoned: isSessionAbandoned(sessionMeta.get(s.sessionId)?.status ?? ""),
|
|
481
|
+
})), [sessionQueue, staleThreshold, lastInteractions, sessionMeta]);
|
|
482
|
+
if (!isInitialized) {
|
|
483
|
+
return (_jsx("box", { style: { flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.colors.bg }, children: _jsx("text", { style: { fg: "#888888" }, children: "Loading..." }) }));
|
|
484
|
+
}
|
|
485
|
+
// Determine main content
|
|
486
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
487
|
+
let mainContent;
|
|
488
|
+
if (state.mode === "WAITING") {
|
|
489
|
+
mainContent = _jsx(WaitingScreen, { queueCount: sessionQueue.length });
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
const session = sessionQueue[activeSessionIndex];
|
|
493
|
+
if (!session) {
|
|
494
|
+
mainContent = _jsx(WaitingScreen, { queueCount: sessionQueue.length });
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
mainContent = (_jsx(StepperView, { onComplete: handleSessionComplete, onProgress: undefined, initialState: sessionUIStates[session.sessionId], onStateSnapshot: handleStateSnapshot, onFlowStateChange: handleFlowStateChange, hasMultipleSessions: sessionQueue.length >= 2, sessionId: session.sessionId, sessionRequest: session.sessionRequest, isAbandoned: isSessionAbandoned(sessionMeta.get(session.sessionId)?.status ?? "") }, session.sessionId));
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return (_jsx("box", { style: { flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.colors.bg }, children: _jsxs("box", { style: { flexDirection: "column", paddingLeft: 1, paddingRight: 1, flexGrow: 1 }, children: [_jsx(Header, { pendingCount: state.mode === "PROCESSING"
|
|
501
|
+
? Math.max(0, sessionQueue.length - 1)
|
|
502
|
+
: sessionQueue.length, updateInfo: !showUpdateOverlay && updateInfo
|
|
503
|
+
? { updateType: updateInfo.updateType, latestVersion: updateInfo.latestVersion }
|
|
504
|
+
: null, onUpdateBadgeActivate: () => setShowUpdateOverlay(true), isCheckingUpdate: isCheckingUpdate }), _jsxs("box", { style: { flexGrow: 1, flexDirection: "column" }, children: [showSessionPicker && state.mode === "PROCESSING" ? (_jsx(SessionPicker, { isOpen: showSessionPicker, sessions: sessionsWithMeta, activeIndex: activeSessionIndex, sessionUIStates: sessionUIStates, onSelectIndex: (idx) => {
|
|
505
|
+
switchToSession(idx);
|
|
506
|
+
setShowSessionPicker(false);
|
|
507
|
+
}, onClose: () => setShowSessionPicker(false) })) : mainContent, state.mode === "PROCESSING" && sessionQueue.length >= 2 && (_jsx(SessionDots, { sessions: sessionsWithMeta, activeIndex: activeSessionIndex, sessionUIStates: sessionUIStates })), toast && (_jsx("box", { style: { marginTop: 1, justifyContent: "center" }, children: _jsx(Toast, { message: toast.message, onDismiss: () => setToast(null), type: toast.type, title: toast.title, duration: 5000 }) })), showUpdateOverlay && updateInfo && (_jsx(UpdateOverlay, { isOpen: showUpdateOverlay, currentVersion: updateInfo.currentVersion, latestVersion: updateInfo.latestVersion, updateType: updateInfo.updateType, changelog: changelogContent, changelogUrl: updateInfo.changelogUrl, isInstalling: isInstallingUpdate, installError: installError, onInstall: handleUpdateInstall, onSkipVersion: handleSkipVersion, onRemindLater: handleRemindLater }))] }), _jsx("box", { style: { marginTop: 1 }, children: _jsx(ThemeIndicator, {}) })] }) }));
|
|
508
|
+
}
|
|
509
|
+
function App({ config }) {
|
|
510
|
+
return (_jsx(ConfigProvider, { config: config, children: _jsx(ThemeProvider, { initialTheme: config.theme, children: _jsx(BoundedErrorBoundary, { children: _jsx(AppInner, { config: config }) }) }) }));
|
|
511
|
+
}
|
|
512
|
+
export async function runTui(config) {
|
|
513
|
+
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
514
|
+
const renderer = await createCliRenderer({
|
|
515
|
+
exitOnCtrlC: true,
|
|
516
|
+
useMouse: true,
|
|
517
|
+
autoFocus: false,
|
|
518
|
+
useAlternateScreen: true,
|
|
519
|
+
useKittyKeyboard: {},
|
|
520
|
+
useConsole: process.env.AUQ_DEBUG === "1",
|
|
521
|
+
targetFps: 60,
|
|
522
|
+
});
|
|
523
|
+
const root = createRoot(renderer);
|
|
524
|
+
root.render(_jsx(App, { config: mergedConfig }));
|
|
525
|
+
// Handle graceful shutdown
|
|
526
|
+
const cleanup = () => {
|
|
527
|
+
renderer.destroy();
|
|
528
|
+
process.exit(0);
|
|
529
|
+
};
|
|
530
|
+
process.on("SIGINT", cleanup);
|
|
531
|
+
process.on("SIGTERM", cleanup);
|
|
532
|
+
// Keep process alive — renderer keeps process alive via its internal event loop
|
|
533
|
+
await new Promise(() => {
|
|
534
|
+
// Intentionally never resolves; renderer lifecycle drives exit
|
|
535
|
+
});
|
|
536
|
+
}
|
|
@@ -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
|
+
};
|