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.
Files changed (33) hide show
  1. package/dist/package.json +3 -3
  2. package/dist/src/i18n/types.js +0 -1
  3. package/dist/src/tui/shared/session-events.js +0 -1
  4. package/dist/src/tui/shared/themes/types.js +0 -1
  5. package/dist/src/tui/shared/types.js +0 -1
  6. package/dist/src/tui-opentui/ConfigContext.js +10 -0
  7. package/dist/src/tui-opentui/ThemeProvider.js +73 -0
  8. package/dist/src/tui-opentui/app.js +536 -0
  9. package/dist/src/tui-opentui/components/AnimatedGradient.js +56 -0
  10. package/dist/src/tui-opentui/components/ConfirmationDialog.js +89 -0
  11. package/dist/src/tui-opentui/components/CustomInput.js +25 -0
  12. package/dist/src/tui-opentui/components/ErrorBoundary.js +26 -0
  13. package/dist/src/tui-opentui/components/Footer.js +92 -0
  14. package/dist/src/tui-opentui/components/Header.js +46 -0
  15. package/dist/src/tui-opentui/components/MarkdownPrompt.js +13 -0
  16. package/dist/src/tui-opentui/components/OptionsList.js +258 -0
  17. package/dist/src/tui-opentui/components/QuestionDisplay.js +23 -0
  18. package/dist/src/tui-opentui/components/ReviewScreen.js +81 -0
  19. package/dist/src/tui-opentui/components/SessionDots.js +86 -0
  20. package/dist/src/tui-opentui/components/SessionPicker.js +162 -0
  21. package/dist/src/tui-opentui/components/SingleLineTextInput.js +9 -0
  22. package/dist/src/tui-opentui/components/StepperView.js +493 -0
  23. package/dist/src/tui-opentui/components/TabBar.js +79 -0
  24. package/dist/src/tui-opentui/components/ThemeIndicator.js +35 -0
  25. package/dist/src/tui-opentui/components/Toast.js +44 -0
  26. package/dist/src/tui-opentui/components/UpdateBadge.js +24 -0
  27. package/dist/src/tui-opentui/components/UpdateOverlay.js +162 -0
  28. package/dist/src/tui-opentui/components/WaitingScreen.js +44 -0
  29. package/dist/src/tui-opentui/hooks/useSessionWatcher.js +69 -0
  30. package/dist/src/tui-opentui/hooks/useTerminalDimensions.js +8 -0
  31. package/dist/src/tui-opentui/utils/syntaxStyle.js +64 -0
  32. package/dist/src/update/types.js +0 -1
  33. package/package.json +3 -3
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auq-mcp-server",
3
- "version": "2.7.1",
3
+ "version": "2.7.2",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "auq": "dist/bin/auq.js"
@@ -15,7 +15,7 @@
15
15
  "packages/opencode-plugin"
16
16
  ],
17
17
  "scripts": {
18
- "build": "bun run sync-plugin-schemas && bun run generate:skill && tsc",
18
+ "build": "bun run sync-plugin-schemas && bun run generate:skill && tsc && tsc -p tsconfig.opentui.json",
19
19
  "prepare": "bun run build",
20
20
  "postinstall": "node scripts/postinstall.cjs",
21
21
  "release": "semantic-release",
@@ -29,7 +29,7 @@
29
29
  "validate:skill": "bunx skills-ref validate skills/ask-user-questions",
30
30
  "test": "vitest run",
31
31
  "format": "prettier --write . && eslint --fix .",
32
- "build:opentui": "tsc -p tsconfig.opentui.json --noEmit",
32
+ "build:opentui": "tsc -p tsconfig.opentui.json",
33
33
  "test:opentui": "bun test src/tui-opentui",
34
34
  "typecheck:all": "tsc --noEmit && tsc -p tsconfig.opentui.json --noEmit"
35
35
  },
@@ -1,2 +1 @@
1
1
  // Translation key types for type-safe internationalization
2
- export {};
@@ -1,4 +1,3 @@
1
1
  /**
2
2
  * Shared session event types for both ink and OpenTUI TUI implementations.
3
3
  */
4
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ import { DEFAULT_CONFIG } from "../config/defaults.js";
4
+ const ConfigContext = createContext({ config: DEFAULT_CONFIG });
5
+ export function ConfigProvider({ config, children, }) {
6
+ return (_jsx(ConfigContext.Provider, { value: { config }, children: children }));
7
+ }
8
+ export function useConfig() {
9
+ return useContext(ConfigContext).config;
10
+ }
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
3
+ import { useKeyboard } from "@opentui/react";
4
+ import { getTheme, listThemes, hasTheme, darkTheme, } from "../tui/shared/themes/index.js";
5
+ import { detectSystemTheme } from "../tui/shared/utils/detectTheme.js";
6
+ import { getSavedTheme, saveTheme } from "../tui/shared/utils/config.js";
7
+ import { generateSyntaxStyle } from "./utils/syntaxStyle.js";
8
+ // ---------------------------------------------------------------------------
9
+ // Context
10
+ // ---------------------------------------------------------------------------
11
+ const ThemeContext = createContext(null);
12
+ /**
13
+ * Consume the nearest ThemeProvider.
14
+ * Throws if called outside of one (fail-fast, no silent undefined).
15
+ */
16
+ export function useTheme() {
17
+ const ctx = useContext(ThemeContext);
18
+ if (!ctx) {
19
+ throw new Error("useTheme must be used within a ThemeProvider");
20
+ }
21
+ return ctx;
22
+ }
23
+ // ---------------------------------------------------------------------------
24
+ // Internals
25
+ // ---------------------------------------------------------------------------
26
+ /** Resolve a ThemeMode into a concrete Theme object. */
27
+ function resolveTheme(mode) {
28
+ if (mode === "system") {
29
+ const detected = detectSystemTheme();
30
+ return getTheme(detected) ?? darkTheme;
31
+ }
32
+ return getTheme(mode) ?? darkTheme;
33
+ }
34
+ /**
35
+ * Determine initial theme:
36
+ * 1. Explicit prop override (if not "system")
37
+ * 2. Persisted user preference from config
38
+ * 3. Fallback to "system"
39
+ */
40
+ function getInitialTheme(prop) {
41
+ if (prop && prop !== "system")
42
+ return prop;
43
+ const saved = getSavedTheme();
44
+ if (saved && (saved === "system" || hasTheme(saved)))
45
+ return saved;
46
+ return "system";
47
+ }
48
+ export function ThemeProvider({ initialTheme, children, }) {
49
+ const [themeName, setThemeName] = useState(() => getInitialTheme(initialTheme));
50
+ // Resolve the full Theme + SyntaxStyle when the mode changes.
51
+ const theme = useMemo(() => resolveTheme(themeName), [themeName]);
52
+ const syntaxStyle = useMemo(() => generateSyntaxStyle(theme), [theme]);
53
+ const cycleTheme = useCallback(() => {
54
+ setThemeName((current) => {
55
+ const allThemes = ["system", ...listThemes()];
56
+ const idx = allThemes.indexOf(current);
57
+ const next = (idx + 1) % allThemes.length;
58
+ return allThemes[next];
59
+ });
60
+ }, []);
61
+ // Ctrl+T to cycle theme — matches ink behaviour.
62
+ useKeyboard((key) => {
63
+ if (key.ctrl && key.name === "t") {
64
+ cycleTheme();
65
+ }
66
+ });
67
+ // Persist theme choice whenever it changes.
68
+ useEffect(() => {
69
+ saveTheme(themeName);
70
+ }, [themeName]);
71
+ const value = useMemo(() => ({ theme, themeName, syntaxStyle, cycleTheme }), [theme, themeName, syntaxStyle, cycleTheme]);
72
+ return (_jsx(ThemeContext.Provider, { value: value, children: children }));
73
+ }
@@ -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
+ }