auq-mcp-server 1.2.7 → 1.2.9
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/README.md +55 -12
- package/dist/bin/auq.js +9 -158
- package/dist/bin/tui-app.js +160 -0
- package/dist/package.json +1 -1
- package/dist/src/tui/components/OptionsList.js +9 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|

|
|
2
2
|
|
|
3
|
-
# AUQ -
|
|
3
|
+
# AUQ - Ask User Questions
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/auq-mcp-server)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://cursor.com/en-US/install-mcp?name=ask-user-questions&config=eyJlbnYiOnt9LCJjb21tYW5kIjoibnB4IC15IGF1cS1tY3Atc2VydmVyIHNlcnZlciJ9)
|
|
8
8
|
|
|
9
|
-
**A lightweight
|
|
9
|
+
**A lightweight CLI tool that allows your LLMs to ask questions to you in a separate space with clean terminal UX. Supports both MCP server and official OpenCode plugin integration. Made for multi-agent parallel coding workflows.**
|
|
10
|
+
|
|
11
|
+
🤔 [Why do I need it when I already have question tool in CC/OC?](#-why-auq-vs-built-in-ask-tools)
|
|
12
|
+
|
|
13
|
+
[Setup](#setup-instructions) • [Features](#-features)
|
|
10
14
|
|
|
11
15
|
---
|
|
12
16
|
|
|
13
17
|
## What does it do?
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
AUQ lets your AI assistants generate clarifying questions consisting of multiple-choice/single-choice questions (with an "Other" option for custom input) while coding or working, and wait for your answers through a separate CLI tool without messing up your workflow.
|
|
16
20
|
|
|
17
21
|
You can keep the CLI running in advance, or start it when questions are pending. With simple arrow key navigation, you can select answers and send them back to the AI—all within a clean terminal interface.
|
|
18
22
|
|
|
@@ -22,7 +26,7 @@ In AI-assisted coding, guiding LLMs to ask **clarifying questions** have been wi
|
|
|
22
26
|
|
|
23
27
|
On October 18th, Claude Code 2.0.21 introduced an internal `ask-user-question` tool. Inspired by it, I decided to build a similar tool that is:
|
|
24
28
|
|
|
25
|
-
- **
|
|
29
|
+
- **Integration-flexible** - Works with MCP clients (Claude Desktop, Cursor, etc.) and has official OpenCode plugin support
|
|
26
30
|
- **Non-invasive** - Doesn't heavily integrate with your coding CLI workflow or occupy UI space
|
|
27
31
|
- **Multi-agent friendly** - Supports receiving questions from multiple agents simultaneously in parallel workflows
|
|
28
32
|
|
|
@@ -56,9 +60,42 @@ Recent AI workflows often use parallel sub-agents for concurrent coding. AUQ han
|
|
|
56
60
|
|
|
57
61
|
---
|
|
58
62
|
|
|
63
|
+
## 🤔 Why AUQ vs. Built-in Ask Tools?
|
|
64
|
+
|
|
65
|
+
**Why should I use AUQ instead of the built-in "Question" tools in OpenCode, Claude Code, or other coding agents?**
|
|
66
|
+
|
|
67
|
+
AUQ is designed for the era of parallel multi-agent workflows, with several key advantages:
|
|
68
|
+
|
|
69
|
+
### 🚀 Non-Blocking Parallel Operation
|
|
70
|
+
Unlike built-in ask tools that halt the entire AI workflow until you respond, AUQ **doesn't block the AI from continuing work**. Questions are queued asynchronously, allowing your AI assistants to keep coding while you review and answer questions at your own pace.
|
|
71
|
+
|
|
72
|
+
### 🎯 Multi-Agent Question Set Support
|
|
73
|
+
**The killer feature**: AUQ can handle question sets from **multiple agents simultaneously**. In modern AI coding workflows, you often have several sub-agents working in parallel—each might need clarification on different aspects of your codebase. With AUQ:
|
|
74
|
+
|
|
75
|
+
- **No more screen switching** between different agent conversations
|
|
76
|
+
- **Unified queue** for all agent questions, regardless of which AI tool they're coming from
|
|
77
|
+
- **Sequential processing** of questions from multiple sources in one interface
|
|
78
|
+
|
|
79
|
+
### 🌐 Cross-Platform Question Aggregation
|
|
80
|
+
AUQ's **unified data origin** means you can answer questions from **different AI clients simultaneously**:
|
|
81
|
+
- Claude Code questions
|
|
82
|
+
- OpenCode questions
|
|
83
|
+
- Cursor/MCP questions
|
|
84
|
+
|
|
85
|
+
All appear in the **same CLI interface**, creating a single source of truth for all AI-agent questions across your entire development environment.
|
|
86
|
+
|
|
87
|
+
### 💡 Perfect for Parallel Agent Workflows
|
|
88
|
+
As AI coding moves toward sophisticated multi-agent systems, AUQ becomes essential. Instead of managing blocking questions across multiple agent screens, you get one streamlined interface that handles questions from your entire AI coding ecosystem—keeping your focus on the code, not the coordination.
|
|
89
|
+
|
|
90
|
+
**TL;DR**: AUQ transforms AI-agent questions from blocking interruptions into a smooth, unified workflow that scales with your AI coding setup.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
59
94
|
# Setup Instructions
|
|
60
95
|
|
|
61
|
-
## 🚀
|
|
96
|
+
## 🚀 Install CLI Tool
|
|
97
|
+
|
|
98
|
+
First, install the AUQ CLI tool:
|
|
62
99
|
|
|
63
100
|
### Global Installation (Recommended)
|
|
64
101
|
|
|
@@ -87,7 +124,11 @@ npx auq
|
|
|
87
124
|
|
|
88
125
|
---
|
|
89
126
|
|
|
90
|
-
## 🔌
|
|
127
|
+
## 🔌 Choose Your Integration Method
|
|
128
|
+
|
|
129
|
+
AUQ supports multiple AI coding environments. Choose the one that fits your workflow:
|
|
130
|
+
|
|
131
|
+
### Option A: MCP Server (Recommended for most users)
|
|
91
132
|
|
|
92
133
|
### Cursor
|
|
93
134
|
|
|
@@ -168,28 +209,30 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
168
209
|
|
|
169
210
|
**Restart Claude Desktop** after saving.
|
|
170
211
|
|
|
171
|
-
|
|
212
|
+
### Option B: Official OpenCode Plugin
|
|
172
213
|
|
|
173
|
-
|
|
214
|
+
We now have **official OpenCode plugin support**! We support OpenCode because OpenCode's MCP server seems to have some limitations (timeout), so we created an OpenCode-specific plugin that works perfectly together with OpenCode.
|
|
174
215
|
|
|
175
|
-
|
|
176
|
-
|
|
216
|
+
The OpenCode plugin allows OpenCode to call `auq ask` directly (without MCP), providing seamless integration with OpenCode's workflow.
|
|
217
|
+
|
|
218
|
+
#### Installation
|
|
177
219
|
|
|
178
220
|
```bash
|
|
221
|
+
# Install both CLI and plugin
|
|
179
222
|
npm install -g auq-mcp-server
|
|
180
223
|
npm install -g @paulp-o/opencode-auq
|
|
181
224
|
```
|
|
182
225
|
|
|
226
|
+
#### Configuration
|
|
227
|
+
|
|
183
228
|
Add to `opencode.json`:
|
|
184
229
|
|
|
185
230
|
```json
|
|
186
231
|
{
|
|
187
|
-
"$schema": "https://opencode.ai/config.json",
|
|
188
232
|
"plugin": ["@paulp-o/opencode-auq"]
|
|
189
233
|
}
|
|
190
234
|
```
|
|
191
235
|
|
|
192
|
-
The plugin assumes `auq` is available on `PATH` (global install or equivalent).
|
|
193
236
|
|
|
194
237
|
---
|
|
195
238
|
|
package/dist/bin/auq.js
CHANGED
|
@@ -3,14 +3,7 @@
|
|
|
3
3
|
import { readFileSync } from "fs";
|
|
4
4
|
import { dirname, join } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import {
|
|
7
|
-
import React, { useEffect, useState } from "react";
|
|
8
|
-
import { ensureDirectoryExists, getSessionDirectory, } from "../src/session/utils.js";
|
|
9
|
-
import { Header } from "../src/tui/components/Header.js";
|
|
10
|
-
import { StepperView } from "../src/tui/components/StepperView.js";
|
|
11
|
-
import { Toast } from "../src/tui/components/Toast.js";
|
|
12
|
-
import { WaitingScreen } from "../src/tui/components/WaitingScreen.js";
|
|
13
|
-
import { createTUIWatcher } from "../src/tui/session-watcher.js";
|
|
6
|
+
import { getSessionDirectory } from "../src/session/utils.js";
|
|
14
7
|
// import { goodbyeText } from "../src/tui/utils/gradientText.js";
|
|
15
8
|
// Handle command-line arguments
|
|
16
9
|
const args = process.argv.slice(2);
|
|
@@ -180,153 +173,11 @@ if (command === "ask") {
|
|
|
180
173
|
process.exit(1);
|
|
181
174
|
}
|
|
182
175
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// Auto-hide session log after 3 seconds
|
|
192
|
-
useEffect(() => {
|
|
193
|
-
const timer = setTimeout(() => {
|
|
194
|
-
setShowSessionLog(false);
|
|
195
|
-
}, 3000);
|
|
196
|
-
return () => clearTimeout(timer);
|
|
197
|
-
}, []);
|
|
198
|
-
// Initialize: Load existing sessions + start persistent watcher
|
|
199
|
-
useEffect(() => {
|
|
200
|
-
let watcherInstance = null;
|
|
201
|
-
const initialize = async () => {
|
|
202
|
-
try {
|
|
203
|
-
// Step 0: Ensure session directory exists
|
|
204
|
-
await ensureDirectoryExists(sessionDir);
|
|
205
|
-
// Step 1: Load existing pending sessions
|
|
206
|
-
const watcher = createTUIWatcher();
|
|
207
|
-
const sessionIds = await watcher.getPendingSessions();
|
|
208
|
-
const sessionData = await Promise.all(sessionIds.map(async (sessionId) => {
|
|
209
|
-
const sessionRequest = await watcher.getSessionRequest(sessionId);
|
|
210
|
-
if (!sessionRequest)
|
|
211
|
-
return null;
|
|
212
|
-
return {
|
|
213
|
-
sessionId,
|
|
214
|
-
sessionRequest,
|
|
215
|
-
timestamp: new Date(sessionRequest.timestamp),
|
|
216
|
-
};
|
|
217
|
-
}));
|
|
218
|
-
// Filter out null entries and sort by timestamp (FIFO - oldest first)
|
|
219
|
-
const validSessions = sessionData
|
|
220
|
-
.filter((s) => s !== null)
|
|
221
|
-
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
222
|
-
setSessionQueue(validSessions);
|
|
223
|
-
setIsInitialized(true);
|
|
224
|
-
// Step 2: Start persistent watcher for new sessions
|
|
225
|
-
watcherInstance = createTUIWatcher({ autoLoadData: true });
|
|
226
|
-
watcherInstance.startEnhancedWatching((event) => {
|
|
227
|
-
// Add new session to queue (FIFO - append to end)
|
|
228
|
-
setSessionQueue((prev) => {
|
|
229
|
-
// Check for duplicates
|
|
230
|
-
if (prev.some((s) => s.sessionId === event.sessionId)) {
|
|
231
|
-
return prev;
|
|
232
|
-
}
|
|
233
|
-
// Add to end of queue
|
|
234
|
-
return [
|
|
235
|
-
...prev,
|
|
236
|
-
{
|
|
237
|
-
sessionId: event.sessionId,
|
|
238
|
-
sessionRequest: event.sessionRequest,
|
|
239
|
-
timestamp: new Date(event.timestamp),
|
|
240
|
-
},
|
|
241
|
-
];
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
console.error("Failed to initialize:", error);
|
|
247
|
-
setIsInitialized(true); // Continue even if initialization fails
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
initialize();
|
|
251
|
-
// Cleanup: stop watcher on unmount
|
|
252
|
-
return () => {
|
|
253
|
-
if (watcherInstance) {
|
|
254
|
-
watcherInstance.stop();
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
}, []);
|
|
258
|
-
// Auto-transition: WAITING → PROCESSING when queue has items
|
|
259
|
-
useEffect(() => {
|
|
260
|
-
if (!isInitialized)
|
|
261
|
-
return;
|
|
262
|
-
if (state.mode === "WAITING" && sessionQueue.length > 0) {
|
|
263
|
-
const [nextSession, ...rest] = sessionQueue;
|
|
264
|
-
setSessionQueue(rest);
|
|
265
|
-
setState({ mode: "PROCESSING", session: nextSession });
|
|
266
|
-
}
|
|
267
|
-
}, [state, sessionQueue, isInitialized]);
|
|
268
|
-
// Show toast notification
|
|
269
|
-
const showToast = (message, type = "success", title) => {
|
|
270
|
-
setToast({ message, type, title });
|
|
271
|
-
};
|
|
272
|
-
// Handle session completion
|
|
273
|
-
const handleSessionComplete = (wasRejected = false, rejectionReason) => {
|
|
274
|
-
// Show appropriate toast
|
|
275
|
-
if (wasRejected) {
|
|
276
|
-
if (rejectionReason) {
|
|
277
|
-
showToast(`Rejection reason: ${rejectionReason}`, "info", "Question set rejected");
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
showToast("", "info", "Question set rejected");
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
showToast("✓ Answers submitted successfully!", "success");
|
|
285
|
-
}
|
|
286
|
-
if (sessionQueue.length > 0) {
|
|
287
|
-
// Auto-load next session
|
|
288
|
-
const [nextSession, ...rest] = sessionQueue;
|
|
289
|
-
setSessionQueue(rest);
|
|
290
|
-
setState({ mode: "PROCESSING", session: nextSession });
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
// Return to WAITING
|
|
294
|
-
setState({ mode: "WAITING" });
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
// Render based on state
|
|
298
|
-
if (!isInitialized) {
|
|
299
|
-
return React.createElement(Text, null, "Loading...");
|
|
300
|
-
}
|
|
301
|
-
let mainContent;
|
|
302
|
-
if (state.mode === "WAITING") {
|
|
303
|
-
mainContent = React.createElement(WaitingScreen, { queueCount: sessionQueue.length });
|
|
304
|
-
}
|
|
305
|
-
else {
|
|
306
|
-
// PROCESSING mode
|
|
307
|
-
const { session } = state;
|
|
308
|
-
mainContent = (React.createElement(StepperView, { key: session.sessionId, onComplete: handleSessionComplete, sessionId: session.sessionId, sessionRequest: session.sessionRequest }));
|
|
309
|
-
}
|
|
310
|
-
// Render with header, toast overlay, and main content
|
|
311
|
-
return (React.createElement(Box, { flexDirection: "column", paddingX: 1 },
|
|
312
|
-
React.createElement(Header, { pendingCount: sessionQueue.length }),
|
|
313
|
-
toast && (React.createElement(Box, { marginBottom: 1, marginTop: 1 },
|
|
314
|
-
React.createElement(Toast, { message: toast.message, onDismiss: () => setToast(null), type: toast.type, title: toast.title }))),
|
|
315
|
-
mainContent,
|
|
316
|
-
showSessionLog && (React.createElement(Box, { marginTop: 1 },
|
|
317
|
-
React.createElement(Text, { dimColor: true },
|
|
318
|
-
"[AUQ] Session directory: ",
|
|
319
|
-
sessionDir)))));
|
|
320
|
-
};
|
|
321
|
-
// Clear terminal before showing app
|
|
322
|
-
console.clear();
|
|
323
|
-
const { waitUntilExit } = render(React.createElement(App, null));
|
|
324
|
-
// Handle Ctrl+C gracefully
|
|
325
|
-
process.on("SIGINT", () => {
|
|
326
|
-
process.exit(0);
|
|
327
|
-
});
|
|
328
|
-
// Show goodbye after Ink unmounts
|
|
329
|
-
waitUntilExit().then(() => {
|
|
330
|
-
process.stdout.write("\n");
|
|
331
|
-
console.log("👋 Goodbye! See you next time.");
|
|
332
|
-
});
|
|
176
|
+
// Default: Start TUI
|
|
177
|
+
// Important: Lazy-load Ink/React so non-interactive commands (ask/server) don't pull them in.
|
|
178
|
+
// Also force production mode before importing React/Ink to avoid perf_hooks measure accumulation warnings.
|
|
179
|
+
if (!process.env.NODE_ENV) {
|
|
180
|
+
process.env.NODE_ENV = "production";
|
|
181
|
+
}
|
|
182
|
+
const { runTui } = await import("./tui-app.js");
|
|
183
|
+
runTui();
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Box, render, Text } from "ink";
|
|
2
|
+
import React, { useEffect, useState } from "react";
|
|
3
|
+
import { ensureDirectoryExists, getSessionDirectory, } from "../src/session/utils.js";
|
|
4
|
+
import { Header } from "../src/tui/components/Header.js";
|
|
5
|
+
import { StepperView } from "../src/tui/components/StepperView.js";
|
|
6
|
+
import { Toast } from "../src/tui/components/Toast.js";
|
|
7
|
+
import { WaitingScreen } from "../src/tui/components/WaitingScreen.js";
|
|
8
|
+
import { createTUIWatcher } from "../src/tui/session-watcher.js";
|
|
9
|
+
const App = () => {
|
|
10
|
+
const [state, setState] = useState({ mode: "WAITING" });
|
|
11
|
+
const [sessionQueue, setSessionQueue] = useState([]);
|
|
12
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
13
|
+
const [toast, setToast] = useState(null);
|
|
14
|
+
const [showSessionLog, setShowSessionLog] = useState(true);
|
|
15
|
+
// Get session directory for logging
|
|
16
|
+
const sessionDir = getSessionDirectory();
|
|
17
|
+
// Auto-hide session log after 3 seconds
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const timer = setTimeout(() => {
|
|
20
|
+
setShowSessionLog(false);
|
|
21
|
+
}, 3000);
|
|
22
|
+
return () => clearTimeout(timer);
|
|
23
|
+
}, []);
|
|
24
|
+
// Initialize: Load existing sessions + start persistent watcher
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
let watcherInstance = null;
|
|
27
|
+
const initialize = async () => {
|
|
28
|
+
try {
|
|
29
|
+
// Step 0: Ensure session directory exists
|
|
30
|
+
await ensureDirectoryExists(sessionDir);
|
|
31
|
+
// Step 1: Load existing pending sessions
|
|
32
|
+
const watcher = createTUIWatcher();
|
|
33
|
+
const sessionIds = await watcher.getPendingSessions();
|
|
34
|
+
const sessionData = await Promise.all(sessionIds.map(async (sessionId) => {
|
|
35
|
+
const sessionRequest = await watcher.getSessionRequest(sessionId);
|
|
36
|
+
if (!sessionRequest)
|
|
37
|
+
return null;
|
|
38
|
+
return {
|
|
39
|
+
sessionId,
|
|
40
|
+
sessionRequest,
|
|
41
|
+
timestamp: new Date(sessionRequest.timestamp),
|
|
42
|
+
};
|
|
43
|
+
}));
|
|
44
|
+
// Filter out null entries and sort by timestamp (FIFO - oldest first)
|
|
45
|
+
const validSessions = sessionData
|
|
46
|
+
.filter((s) => s !== null)
|
|
47
|
+
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
48
|
+
setSessionQueue(validSessions);
|
|
49
|
+
setIsInitialized(true);
|
|
50
|
+
// Step 2: Start persistent watcher for new sessions
|
|
51
|
+
watcherInstance = createTUIWatcher({ autoLoadData: true });
|
|
52
|
+
watcherInstance.startEnhancedWatching((event) => {
|
|
53
|
+
// Add new session to queue (FIFO - append to end)
|
|
54
|
+
setSessionQueue((prev) => {
|
|
55
|
+
// Check for duplicates
|
|
56
|
+
if (prev.some((s) => s.sessionId === event.sessionId)) {
|
|
57
|
+
return prev;
|
|
58
|
+
}
|
|
59
|
+
// Add to end of queue
|
|
60
|
+
return [
|
|
61
|
+
...prev,
|
|
62
|
+
{
|
|
63
|
+
sessionId: event.sessionId,
|
|
64
|
+
sessionRequest: event.sessionRequest,
|
|
65
|
+
timestamp: new Date(event.timestamp),
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error("Failed to initialize:", error);
|
|
73
|
+
setIsInitialized(true); // Continue even if initialization fails
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
initialize();
|
|
77
|
+
// Cleanup: stop watcher on unmount
|
|
78
|
+
return () => {
|
|
79
|
+
if (watcherInstance) {
|
|
80
|
+
watcherInstance.stop();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}, []);
|
|
84
|
+
// Auto-transition: WAITING → PROCESSING when queue has items
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!isInitialized)
|
|
87
|
+
return;
|
|
88
|
+
if (state.mode === "WAITING" && sessionQueue.length > 0) {
|
|
89
|
+
const [nextSession, ...rest] = sessionQueue;
|
|
90
|
+
setSessionQueue(rest);
|
|
91
|
+
setState({ mode: "PROCESSING", session: nextSession });
|
|
92
|
+
}
|
|
93
|
+
}, [state, sessionQueue, isInitialized]);
|
|
94
|
+
// Show toast notification
|
|
95
|
+
const showToast = (message, type = "success", title) => {
|
|
96
|
+
setToast({ message, type, title });
|
|
97
|
+
};
|
|
98
|
+
// Handle session completion
|
|
99
|
+
const handleSessionComplete = (wasRejected = false, rejectionReason) => {
|
|
100
|
+
// Show appropriate toast
|
|
101
|
+
if (wasRejected) {
|
|
102
|
+
if (rejectionReason) {
|
|
103
|
+
showToast(`Rejection reason: ${rejectionReason}`, "info", "Question set rejected");
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
showToast("", "info", "Question set rejected");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
showToast("✓ Answers submitted successfully!", "success");
|
|
111
|
+
}
|
|
112
|
+
if (sessionQueue.length > 0) {
|
|
113
|
+
// Auto-load next session
|
|
114
|
+
const [nextSession, ...rest] = sessionQueue;
|
|
115
|
+
setSessionQueue(rest);
|
|
116
|
+
setState({ mode: "PROCESSING", session: nextSession });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Return to WAITING
|
|
120
|
+
setState({ mode: "WAITING" });
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
// Render based on state
|
|
124
|
+
if (!isInitialized) {
|
|
125
|
+
return React.createElement(Text, null, "Loading...");
|
|
126
|
+
}
|
|
127
|
+
let mainContent;
|
|
128
|
+
if (state.mode === "WAITING") {
|
|
129
|
+
mainContent = React.createElement(WaitingScreen, { queueCount: sessionQueue.length });
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// PROCESSING mode
|
|
133
|
+
const { session } = state;
|
|
134
|
+
mainContent = (React.createElement(StepperView, { key: session.sessionId, onComplete: handleSessionComplete, sessionId: session.sessionId, sessionRequest: session.sessionRequest }));
|
|
135
|
+
}
|
|
136
|
+
// Render with header, toast overlay, and main content
|
|
137
|
+
return (React.createElement(Box, { flexDirection: "column", paddingX: 1 },
|
|
138
|
+
React.createElement(Header, { pendingCount: sessionQueue.length }),
|
|
139
|
+
toast && (React.createElement(Box, { marginBottom: 1, marginTop: 1 },
|
|
140
|
+
React.createElement(Toast, { message: toast.message, onDismiss: () => setToast(null), type: toast.type, title: toast.title }))),
|
|
141
|
+
mainContent,
|
|
142
|
+
showSessionLog && (React.createElement(Box, { marginTop: 1 },
|
|
143
|
+
React.createElement(Text, { dimColor: true },
|
|
144
|
+
"[AUQ] Session directory: ",
|
|
145
|
+
sessionDir)))));
|
|
146
|
+
};
|
|
147
|
+
export const runTui = () => {
|
|
148
|
+
// Clear terminal before showing app
|
|
149
|
+
console.clear();
|
|
150
|
+
const { waitUntilExit } = render(React.createElement(App, null));
|
|
151
|
+
// Handle Ctrl+C gracefully
|
|
152
|
+
process.on("SIGINT", () => {
|
|
153
|
+
process.exit(0);
|
|
154
|
+
});
|
|
155
|
+
// Show goodbye after Ink unmounts
|
|
156
|
+
waitUntilExit().then(() => {
|
|
157
|
+
process.stdout.write("\n");
|
|
158
|
+
console.log("👋 Goodbye! See you next time.");
|
|
159
|
+
});
|
|
160
|
+
};
|
package/dist/package.json
CHANGED
|
@@ -22,6 +22,15 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
|
|
|
22
22
|
useInput((input, key) => {
|
|
23
23
|
if (!isFocused)
|
|
24
24
|
return;
|
|
25
|
+
// Handle up/down navigation even when custom input is focused
|
|
26
|
+
if (key.upArrow) {
|
|
27
|
+
setFocusedIndex((prev) => Math.max(0, prev - 1));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (key.downArrow) {
|
|
31
|
+
setFocusedIndex((prev) => Math.min(maxIndex, prev + 1));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
25
34
|
// When custom input is focused, only handle escape to exit, let MultiLineTextInput handle other keys
|
|
26
35
|
if (isCustomInputFocused) {
|
|
27
36
|
if (key.escape) {
|
|
@@ -30,12 +39,6 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
|
|
|
30
39
|
}
|
|
31
40
|
return;
|
|
32
41
|
}
|
|
33
|
-
if (key.upArrow) {
|
|
34
|
-
setFocusedIndex((prev) => Math.max(0, prev - 1));
|
|
35
|
-
}
|
|
36
|
-
if (key.downArrow) {
|
|
37
|
-
setFocusedIndex((prev) => Math.min(maxIndex, prev + 1));
|
|
38
|
-
}
|
|
39
42
|
if (multiSelect) {
|
|
40
43
|
// Multi-select mode
|
|
41
44
|
if (input === " ") {
|