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
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auq-mcp-server",
|
|
3
|
-
"version": "2.7.
|
|
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
|
|
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
|
},
|
package/dist/src/i18n/types.js
CHANGED
|
@@ -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
|
+
}
|