auq-mcp-server 2.2.2 → 2.4.0
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 +82 -0
- package/dist/bin/auq.js +45 -39
- package/dist/bin/tui-app.js +78 -8
- package/dist/package.json +1 -1
- package/dist/src/__tests__/server.abort.test.js +214 -0
- package/dist/src/cli/commands/__tests__/answer.test.js +199 -0
- package/dist/src/cli/commands/__tests__/config.test.js +218 -0
- package/dist/src/cli/commands/__tests__/sessions.test.js +282 -0
- package/dist/src/cli/commands/answer.js +128 -0
- package/dist/src/cli/commands/config.js +263 -0
- package/dist/src/cli/commands/sessions.js +164 -0
- package/dist/src/cli/utils.js +95 -0
- package/dist/src/config/__tests__/ConfigLoader.test.js +41 -0
- package/dist/src/config/defaults.js +3 -0
- package/dist/src/config/types.js +4 -0
- package/dist/src/core/ask-user-questions.js +3 -2
- package/dist/src/i18n/locales/en.js +8 -1
- package/dist/src/i18n/locales/ko.js +8 -1
- package/dist/src/server.js +64 -11
- package/dist/src/session/SessionManager.js +69 -4
- package/dist/src/session/__tests__/SessionManager.test.js +65 -0
- package/dist/src/tui/ThemeProvider.js +2 -1
- package/dist/src/tui/__tests__/session-watcher.test.js +109 -0
- package/dist/src/tui/components/ConfirmationDialog.js +5 -4
- package/dist/src/tui/components/Footer.js +24 -23
- package/dist/src/tui/components/ReviewScreen.js +2 -1
- package/dist/src/tui/components/SessionDots.js +33 -4
- package/dist/src/tui/components/SessionPicker.js +27 -18
- package/dist/src/tui/components/Spinner.js +19 -0
- package/dist/src/tui/components/StepperView.js +71 -7
- package/dist/src/tui/components/WaitingScreen.js +2 -1
- package/dist/src/tui/components/__tests__/ConfirmationDialog.test.js +134 -0
- package/dist/src/tui/components/__tests__/Footer.test.js +121 -0
- package/dist/src/tui/components/__tests__/ReviewScreen.test.js +89 -0
- package/dist/src/tui/components/__tests__/SessionDots.test.js +160 -1
- package/dist/src/tui/components/__tests__/SessionPicker.test.js +43 -1
- package/dist/src/tui/components/__tests__/StepperView.abandoned.test.js +160 -0
- package/dist/src/tui/components/__tests__/StepperView.keyboard.test.js +135 -0
- package/dist/src/tui/components/__tests__/StepperView.state.test.js +1 -0
- package/dist/src/tui/components/__tests__/WaitingScreen.test.js +60 -0
- package/dist/src/tui/constants/keybindings.js +40 -0
- package/dist/src/tui/session-watcher.js +50 -0
- package/dist/src/tui/themes/catppuccin-latte.js +7 -0
- package/dist/src/tui/themes/catppuccin-mocha.js +7 -0
- package/dist/src/tui/themes/dark.js +7 -0
- package/dist/src/tui/themes/dracula.js +7 -0
- package/dist/src/tui/themes/github-dark.js +7 -0
- package/dist/src/tui/themes/github-light.js +7 -0
- package/dist/src/tui/themes/gruvbox-dark.js +7 -0
- package/dist/src/tui/themes/gruvbox-light.js +7 -0
- package/dist/src/tui/themes/light.js +7 -0
- package/dist/src/tui/themes/monokai.js +7 -0
- package/dist/src/tui/themes/nord.js +7 -0
- package/dist/src/tui/themes/one-dark.js +7 -0
- package/dist/src/tui/themes/rose-pine.js +7 -0
- package/dist/src/tui/themes/solarized-dark.js +7 -0
- package/dist/src/tui/themes/solarized-light.js +7 -0
- package/dist/src/tui/themes/tokyo-night.js +7 -0
- package/dist/src/tui/utils/__tests__/detectTheme.test.js +78 -0
- package/dist/src/tui/utils/__tests__/staleDetection.test.js +118 -0
- package/dist/src/tui/utils/staleDetection.js +51 -0
- package/package.json +1 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cleanup, render } from "ink-testing-library";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ConfigProvider } from "../../ConfigContext.js";
|
|
5
|
+
import { ThemeContext } from "../../ThemeContext.js";
|
|
6
|
+
import { darkTheme } from "../../themes/dark.js";
|
|
7
|
+
import { StepperView } from "../StepperView.js";
|
|
8
|
+
const mockThemeValue = {
|
|
9
|
+
theme: darkTheme,
|
|
10
|
+
themeName: "AUQ dark",
|
|
11
|
+
cycleTheme: () => { },
|
|
12
|
+
};
|
|
13
|
+
const sessionRequest = {
|
|
14
|
+
sessionId: "abandoned-test-session",
|
|
15
|
+
status: "pending",
|
|
16
|
+
timestamp: new Date().toISOString(),
|
|
17
|
+
callId: "call-abandoned-1",
|
|
18
|
+
questions: [
|
|
19
|
+
{
|
|
20
|
+
title: "Language",
|
|
21
|
+
prompt: "Pick a language",
|
|
22
|
+
options: [{ label: "TypeScript" }, { label: "Python" }],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
title: "Framework",
|
|
26
|
+
prompt: "Pick a framework",
|
|
27
|
+
options: [{ label: "React" }, { label: "Vue" }],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
function renderStepper(props = {}) {
|
|
32
|
+
return render(React.createElement(ThemeContext.Provider, { value: mockThemeValue },
|
|
33
|
+
React.createElement(ConfigProvider, null,
|
|
34
|
+
React.createElement(StepperView, { sessionId: sessionRequest.sessionId, sessionRequest: sessionRequest, ...props }))));
|
|
35
|
+
}
|
|
36
|
+
function getOutput(frame) {
|
|
37
|
+
return (frame ?? "").replace(/\x1b\[[0-9;]*m/g, "").replace(/\r/g, "");
|
|
38
|
+
}
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
cleanup();
|
|
41
|
+
vi.restoreAllMocks();
|
|
42
|
+
});
|
|
43
|
+
describe("StepperView abandoned session confirmation", () => {
|
|
44
|
+
it("shows abandoned confirmation dialog when isAbandoned is true", async () => {
|
|
45
|
+
const instance = renderStepper({ isAbandoned: true });
|
|
46
|
+
await vi.waitFor(() => {
|
|
47
|
+
const output = getOutput(instance.lastFrame());
|
|
48
|
+
expect(output).toContain("AI Disconnected");
|
|
49
|
+
});
|
|
50
|
+
const output = getOutput(instance.lastFrame());
|
|
51
|
+
expect(output).toContain("Answer anyway");
|
|
52
|
+
expect(output).toContain("Cancel");
|
|
53
|
+
// Should NOT show question content behind the dialog
|
|
54
|
+
expect(output).not.toContain("Pick a language");
|
|
55
|
+
});
|
|
56
|
+
it("does not show abandoned dialog when isAbandoned is false", () => {
|
|
57
|
+
const instance = renderStepper({ isAbandoned: false });
|
|
58
|
+
const output = getOutput(instance.lastFrame());
|
|
59
|
+
expect(output).not.toContain("AI Disconnected");
|
|
60
|
+
expect(output).toContain("Pick a language");
|
|
61
|
+
});
|
|
62
|
+
it("does not show abandoned dialog when isAbandoned is undefined", () => {
|
|
63
|
+
const instance = renderStepper();
|
|
64
|
+
const output = getOutput(instance.lastFrame());
|
|
65
|
+
expect(output).not.toContain("AI Disconnected");
|
|
66
|
+
expect(output).toContain("Pick a language");
|
|
67
|
+
});
|
|
68
|
+
it("selecting 'Answer anyway' dismisses dialog and shows questions", async () => {
|
|
69
|
+
const instance = renderStepper({ isAbandoned: true });
|
|
70
|
+
await vi.waitFor(() => {
|
|
71
|
+
const output = getOutput(instance.lastFrame());
|
|
72
|
+
expect(output).toContain("AI Disconnected");
|
|
73
|
+
});
|
|
74
|
+
// Press Enter to select the first option ("Answer anyway")
|
|
75
|
+
instance.stdin.write("\r");
|
|
76
|
+
await vi.waitFor(() => {
|
|
77
|
+
const output = getOutput(instance.lastFrame());
|
|
78
|
+
expect(output).toContain("Pick a language");
|
|
79
|
+
});
|
|
80
|
+
const output = getOutput(instance.lastFrame());
|
|
81
|
+
expect(output).not.toContain("AI Disconnected");
|
|
82
|
+
});
|
|
83
|
+
it("selecting 'Cancel' calls onAbandonedCancel", async () => {
|
|
84
|
+
const onAbandonedCancel = vi.fn();
|
|
85
|
+
const instance = renderStepper({
|
|
86
|
+
isAbandoned: true,
|
|
87
|
+
onAbandonedCancel,
|
|
88
|
+
});
|
|
89
|
+
await vi.waitFor(() => {
|
|
90
|
+
const output = getOutput(instance.lastFrame());
|
|
91
|
+
expect(output).toContain("AI Disconnected");
|
|
92
|
+
});
|
|
93
|
+
// Navigate down to "Cancel" then press Enter
|
|
94
|
+
// Use OA/OB sequences which ink reliably parses as arrows
|
|
95
|
+
instance.stdin.write("\x1bOB"); // Down arrow (application mode)
|
|
96
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
97
|
+
// Press Enter to select "Cancel"
|
|
98
|
+
instance.stdin.write("\r");
|
|
99
|
+
await vi.waitFor(() => {
|
|
100
|
+
expect(onAbandonedCancel).toHaveBeenCalledOnce();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
it("pressing Escape calls onAbandonedCancel", async () => {
|
|
104
|
+
const onAbandonedCancel = vi.fn();
|
|
105
|
+
const instance = renderStepper({
|
|
106
|
+
isAbandoned: true,
|
|
107
|
+
onAbandonedCancel,
|
|
108
|
+
});
|
|
109
|
+
await vi.waitFor(() => {
|
|
110
|
+
const output = getOutput(instance.lastFrame());
|
|
111
|
+
expect(output).toContain("AI Disconnected");
|
|
112
|
+
});
|
|
113
|
+
// Press Escape
|
|
114
|
+
instance.stdin.write("\x1b");
|
|
115
|
+
await vi.waitFor(() => {
|
|
116
|
+
expect(onAbandonedCancel).toHaveBeenCalledOnce();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
it("after confirming, dialog does not reappear for the same session", async () => {
|
|
120
|
+
const instance = renderStepper({ isAbandoned: true });
|
|
121
|
+
// Wait for dialog
|
|
122
|
+
await vi.waitFor(() => {
|
|
123
|
+
expect(getOutput(instance.lastFrame())).toContain("AI Disconnected");
|
|
124
|
+
});
|
|
125
|
+
// Confirm ("Answer anyway")
|
|
126
|
+
instance.stdin.write("\r");
|
|
127
|
+
// Wait for questions to appear
|
|
128
|
+
await vi.waitFor(() => {
|
|
129
|
+
expect(getOutput(instance.lastFrame())).toContain("Pick a language");
|
|
130
|
+
});
|
|
131
|
+
// The dialog should stay dismissed - verify questions are still shown
|
|
132
|
+
const output = getOutput(instance.lastFrame());
|
|
133
|
+
expect(output).toContain("Pick a language");
|
|
134
|
+
expect(output).not.toContain("AI Disconnected");
|
|
135
|
+
});
|
|
136
|
+
it("emits showAbandonedConfirm in onFlowStateChange", async () => {
|
|
137
|
+
const onFlowStateChange = vi.fn();
|
|
138
|
+
renderStepper({
|
|
139
|
+
isAbandoned: true,
|
|
140
|
+
onFlowStateChange,
|
|
141
|
+
});
|
|
142
|
+
await vi.waitFor(() => {
|
|
143
|
+
expect(onFlowStateChange).toHaveBeenCalledWith(expect.objectContaining({ showAbandonedConfirm: true }));
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
it("blocks keyboard navigation while dialog is shown", async () => {
|
|
147
|
+
const instance = renderStepper({ isAbandoned: true });
|
|
148
|
+
await vi.waitFor(() => {
|
|
149
|
+
expect(getOutput(instance.lastFrame())).toContain("AI Disconnected");
|
|
150
|
+
});
|
|
151
|
+
// Try Tab to navigate questions - should do nothing
|
|
152
|
+
instance.stdin.write("\t");
|
|
153
|
+
// Dialog should still be shown
|
|
154
|
+
await vi.waitFor(() => {
|
|
155
|
+
const output = getOutput(instance.lastFrame());
|
|
156
|
+
expect(output).toContain("AI Disconnected");
|
|
157
|
+
expect(output).not.toContain("Pick a language");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cleanup, render } from "ink-testing-library";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ConfigProvider } from "../../ConfigContext.js";
|
|
5
|
+
import { ThemeContext } from "../../ThemeContext.js";
|
|
6
|
+
import { darkTheme } from "../../themes/dark.js";
|
|
7
|
+
import { StepperView } from "../StepperView.js";
|
|
8
|
+
const mockThemeValue = {
|
|
9
|
+
theme: darkTheme,
|
|
10
|
+
themeName: "AUQ dark",
|
|
11
|
+
cycleTheme: () => { },
|
|
12
|
+
};
|
|
13
|
+
const sessionRequest = {
|
|
14
|
+
sessionId: "kbd-test-session",
|
|
15
|
+
status: "pending",
|
|
16
|
+
timestamp: new Date().toISOString(),
|
|
17
|
+
callId: "call-kbd-1",
|
|
18
|
+
questions: [
|
|
19
|
+
{
|
|
20
|
+
title: "Q1",
|
|
21
|
+
prompt: "First question?",
|
|
22
|
+
options: [{ label: "A" }, { label: "B" }],
|
|
23
|
+
multiSelect: false,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
title: "Q2",
|
|
27
|
+
prompt: "Second question?",
|
|
28
|
+
options: [{ label: "C" }, { label: "D" }],
|
|
29
|
+
multiSelect: false,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
title: "Q3",
|
|
33
|
+
prompt: "Third question?",
|
|
34
|
+
options: [{ label: "E" }, { label: "F" }],
|
|
35
|
+
multiSelect: false,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
function renderStepper(props = {}) {
|
|
40
|
+
return render(React.createElement(ThemeContext.Provider, { value: mockThemeValue },
|
|
41
|
+
React.createElement(ConfigProvider, null,
|
|
42
|
+
React.createElement(StepperView, { sessionId: sessionRequest.sessionId, sessionRequest: sessionRequest, ...props }))));
|
|
43
|
+
}
|
|
44
|
+
function getOutput(frame) {
|
|
45
|
+
return (frame ?? "").replace(/\x1b\[[0-9;]*m/g, "").replace(/\r/g, "");
|
|
46
|
+
}
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
cleanup();
|
|
49
|
+
vi.restoreAllMocks();
|
|
50
|
+
});
|
|
51
|
+
describe("StepperView keyboard shortcuts", () => {
|
|
52
|
+
it("Escape shows rejection confirmation dialog", async () => {
|
|
53
|
+
const instance = renderStepper();
|
|
54
|
+
// Verify we start on question 1
|
|
55
|
+
let output = getOutput(instance.lastFrame());
|
|
56
|
+
expect(output).toContain("First question?");
|
|
57
|
+
// Press Escape
|
|
58
|
+
instance.stdin.write("\x1b");
|
|
59
|
+
await vi.waitFor(() => {
|
|
60
|
+
const out = getOutput(instance.lastFrame());
|
|
61
|
+
expect(out).toContain("Reject");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
it("Tab navigates to next question", async () => {
|
|
65
|
+
const instance = renderStepper();
|
|
66
|
+
// Verify starting on question 1
|
|
67
|
+
let output = getOutput(instance.lastFrame());
|
|
68
|
+
expect(output).toContain("First question?");
|
|
69
|
+
// Press Tab to advance to Q2
|
|
70
|
+
instance.stdin.write("\t");
|
|
71
|
+
await vi.waitFor(() => {
|
|
72
|
+
const out = getOutput(instance.lastFrame());
|
|
73
|
+
expect(out).toContain("Second question?");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
it("Shift+Tab navigates to previous question", async () => {
|
|
77
|
+
const onStateSnapshot = vi.fn();
|
|
78
|
+
const instance = renderStepper({ onStateSnapshot });
|
|
79
|
+
// First advance to Q2 with Tab
|
|
80
|
+
instance.stdin.write("\t");
|
|
81
|
+
await vi.waitFor(() => {
|
|
82
|
+
const out = getOutput(instance.lastFrame());
|
|
83
|
+
expect(out).toContain("Second question?");
|
|
84
|
+
});
|
|
85
|
+
// Press Shift+Tab (escape sequence for shift-tab in ink)
|
|
86
|
+
instance.stdin.write("\x1b[Z");
|
|
87
|
+
await vi.waitFor(() => {
|
|
88
|
+
const out = getOutput(instance.lastFrame());
|
|
89
|
+
expect(out).toContain("First question?");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
it("Tab does not navigate past the last question", async () => {
|
|
93
|
+
const instance = renderStepper();
|
|
94
|
+
// Navigate to Q3 (last question)
|
|
95
|
+
instance.stdin.write("\t"); // Q1 -> Q2
|
|
96
|
+
await vi.waitFor(() => {
|
|
97
|
+
const out = getOutput(instance.lastFrame());
|
|
98
|
+
expect(out).toContain("Second question?");
|
|
99
|
+
});
|
|
100
|
+
instance.stdin.write("\t"); // Q2 -> Q3
|
|
101
|
+
await vi.waitFor(() => {
|
|
102
|
+
const out = getOutput(instance.lastFrame());
|
|
103
|
+
expect(out).toContain("Third question?");
|
|
104
|
+
});
|
|
105
|
+
// Tab again should stay on Q3 (clamped)
|
|
106
|
+
instance.stdin.write("\t");
|
|
107
|
+
await vi.waitFor(() => {
|
|
108
|
+
const out = getOutput(instance.lastFrame());
|
|
109
|
+
expect(out).toContain("Third question?");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
it("Shift+Tab does not navigate before the first question", async () => {
|
|
113
|
+
const instance = renderStepper();
|
|
114
|
+
// Verify on Q1
|
|
115
|
+
let output = getOutput(instance.lastFrame());
|
|
116
|
+
expect(output).toContain("First question?");
|
|
117
|
+
// Press Shift+Tab at Q1 — should remain on Q1
|
|
118
|
+
instance.stdin.write("\x1b[Z");
|
|
119
|
+
// Allow state to settle
|
|
120
|
+
await vi.waitFor(() => {
|
|
121
|
+
const out = getOutput(instance.lastFrame());
|
|
122
|
+
expect(out).toContain("First question?");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
it("emits onProgress when Tab advances question", async () => {
|
|
126
|
+
const onProgress = vi.fn();
|
|
127
|
+
const instance = renderStepper({ onProgress });
|
|
128
|
+
// Tab to advance from Q1 to Q2
|
|
129
|
+
instance.stdin.write("\t");
|
|
130
|
+
await vi.waitFor(() => {
|
|
131
|
+
// onProgress should be called with (answered=1, total=3)
|
|
132
|
+
expect(onProgress).toHaveBeenCalledWith(1, 3);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cleanup, render } from "ink-testing-library";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
const inputState = vi.hoisted(() => ({
|
|
5
|
+
handler: null,
|
|
6
|
+
}));
|
|
7
|
+
vi.mock("ink", async () => {
|
|
8
|
+
const actual = await vi.importActual("ink");
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
useInput: (handler, options) => {
|
|
12
|
+
// WaitingScreen calls useInput without options, so always capture
|
|
13
|
+
if (!options || options.isActive !== false) {
|
|
14
|
+
inputState.handler = handler;
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
// Mock AnimatedGradient to avoid timer/animation side effects
|
|
20
|
+
vi.mock("../AnimatedGradient.js", () => ({
|
|
21
|
+
AnimatedGradient: ({ text }) => React.createElement("ink-text", null, text),
|
|
22
|
+
}));
|
|
23
|
+
import { ThemeContext } from "../../ThemeContext.js";
|
|
24
|
+
import { darkTheme } from "../../themes/dark.js";
|
|
25
|
+
import { WaitingScreen } from "../WaitingScreen.js";
|
|
26
|
+
const mockThemeValue = {
|
|
27
|
+
theme: darkTheme,
|
|
28
|
+
themeName: "AUQ dark",
|
|
29
|
+
cycleTheme: () => { },
|
|
30
|
+
};
|
|
31
|
+
function renderWithTheme(ui) {
|
|
32
|
+
return render(React.createElement(ThemeContext.Provider, { value: mockThemeValue }, ui));
|
|
33
|
+
}
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
cleanup();
|
|
36
|
+
inputState.handler = null;
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
describe("WaitingScreen keyboard handling", () => {
|
|
40
|
+
it("q key triggers quit", async () => {
|
|
41
|
+
const exitSpy = vi
|
|
42
|
+
.spyOn(process, "exit")
|
|
43
|
+
.mockImplementation((() => { }));
|
|
44
|
+
renderWithTheme(React.createElement(WaitingScreen, { queueCount: 0 }));
|
|
45
|
+
expect(inputState.handler).not.toBeNull();
|
|
46
|
+
inputState.handler("q", {});
|
|
47
|
+
await Promise.resolve();
|
|
48
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
49
|
+
});
|
|
50
|
+
it("Q key (uppercase) triggers quit (case-insensitive)", async () => {
|
|
51
|
+
const exitSpy = vi
|
|
52
|
+
.spyOn(process, "exit")
|
|
53
|
+
.mockImplementation((() => { }));
|
|
54
|
+
renderWithTheme(React.createElement(WaitingScreen, { queueCount: 0 }));
|
|
55
|
+
expect(inputState.handler).not.toBeNull();
|
|
56
|
+
inputState.handler("Q", {});
|
|
57
|
+
await Promise.resolve();
|
|
58
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Keybinding constants for the TUI application
|
|
2
|
+
// All keyboard shortcut definitions are centralized here.
|
|
3
|
+
export const KEYS = {
|
|
4
|
+
// Session switching (bare keys, no Ctrl)
|
|
5
|
+
SESSION_NEXT: "]",
|
|
6
|
+
SESSION_PREV: "[",
|
|
7
|
+
SESSION_JUMP_MIN: 1,
|
|
8
|
+
SESSION_JUMP_MAX: 9,
|
|
9
|
+
// Question navigation
|
|
10
|
+
RECOMMEND: "r",
|
|
11
|
+
QUICK_SUBMIT: "r", // used with key.ctrl
|
|
12
|
+
// Theme
|
|
13
|
+
THEME_CYCLE: "t", // used with key.ctrl
|
|
14
|
+
// Confirmation shortcuts
|
|
15
|
+
CONFIRM_YES: /^[yY]$/,
|
|
16
|
+
CONFIRM_NO: /^[nN]$/,
|
|
17
|
+
// Review
|
|
18
|
+
GO_BACK: /^[nN]$/,
|
|
19
|
+
// Waiting
|
|
20
|
+
QUIT: /^[qQ]$/,
|
|
21
|
+
};
|
|
22
|
+
// Display labels for Footer keybinding hints
|
|
23
|
+
export const KEY_LABELS = {
|
|
24
|
+
SESSION_SWITCH: "]/[",
|
|
25
|
+
SESSION_LIST: "Ctrl+S",
|
|
26
|
+
QUICK_SUBMIT: "Ctrl+R",
|
|
27
|
+
RECOMMEND: "R",
|
|
28
|
+
THEME: "Ctrl+T",
|
|
29
|
+
NAVIGATE_QUESTIONS: "←→",
|
|
30
|
+
NAVIGATE_QUESTIONS_TAB: "Tab/S+Tab",
|
|
31
|
+
NAVIGATE_OPTIONS: "↑↓",
|
|
32
|
+
SELECT: "Space",
|
|
33
|
+
SELECT_NEXT: "Enter",
|
|
34
|
+
NEXT: "Enter",
|
|
35
|
+
CURSOR: "←→",
|
|
36
|
+
NEWLINE: "Enter",
|
|
37
|
+
REJECT: "Esc",
|
|
38
|
+
BACK: "n",
|
|
39
|
+
SUBMIT: "Enter",
|
|
40
|
+
};
|
|
@@ -72,6 +72,56 @@ export class EnhancedTUISessionWatcher extends TUISessionWatcher {
|
|
|
72
72
|
return [];
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Get pending and abandoned sessions with full status metadata.
|
|
77
|
+
* Unlike getPendingSessions(), this includes abandoned sessions
|
|
78
|
+
* and returns status + createdAt for stale detection.
|
|
79
|
+
*/
|
|
80
|
+
async getPendingSessionsWithStatus() {
|
|
81
|
+
const fs = await import("fs/promises");
|
|
82
|
+
const { join } = await import("path");
|
|
83
|
+
try {
|
|
84
|
+
const sessionDir = this.watchedPath;
|
|
85
|
+
const entries = await fs.readdir(sessionDir, { withFileTypes: true });
|
|
86
|
+
const results = [];
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (!entry.isDirectory())
|
|
89
|
+
continue;
|
|
90
|
+
const sessionPath = join(sessionDir, entry.name);
|
|
91
|
+
const answersPath = join(sessionPath, SESSION_FILES.ANSWERS);
|
|
92
|
+
const statusPath = join(sessionPath, SESSION_FILES.STATUS);
|
|
93
|
+
try {
|
|
94
|
+
// Skip sessions that already have answers
|
|
95
|
+
await fs.access(answersPath);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// No answers file — read status.json
|
|
99
|
+
try {
|
|
100
|
+
const statusContent = await fs.readFile(statusPath, "utf-8");
|
|
101
|
+
const status = JSON.parse(statusContent);
|
|
102
|
+
// Include pending, in-progress, AND abandoned sessions
|
|
103
|
+
if (status.status === "pending" ||
|
|
104
|
+
status.status === "in-progress" ||
|
|
105
|
+
status.status === "abandoned") {
|
|
106
|
+
results.push({
|
|
107
|
+
createdAt: status.createdAt,
|
|
108
|
+
sessionId: entry.name,
|
|
109
|
+
status: status.status,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// No valid status file — skip
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return results.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.warn("Failed to scan for pending sessions with status:", error);
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
75
125
|
/**
|
|
76
126
|
* Get session request data for a specific session
|
|
77
127
|
*/
|
|
@@ -91,6 +91,7 @@ export const catppuccinLatteTheme = {
|
|
|
91
91
|
successPillBg: "#dce0e8",
|
|
92
92
|
error: "#d20f39",
|
|
93
93
|
info: "#1e66f5",
|
|
94
|
+
warning: "#df8e1d",
|
|
94
95
|
border: "#ccd0da",
|
|
95
96
|
},
|
|
96
97
|
markdown: {
|
|
@@ -105,6 +106,8 @@ export const catppuccinLatteTheme = {
|
|
|
105
106
|
untouched: "#acb0c0",
|
|
106
107
|
number: "#4c4f69",
|
|
107
108
|
activeNumber: "#1e66f5",
|
|
109
|
+
stale: "#df8e1d",
|
|
110
|
+
abandoned: "#d20f39",
|
|
108
111
|
},
|
|
109
112
|
sessionPicker: {
|
|
110
113
|
border: "#1e66f5",
|
|
@@ -115,6 +118,10 @@ export const catppuccinLatteTheme = {
|
|
|
115
118
|
highlightFg: "#40a02b",
|
|
116
119
|
activeMark: "#1e66f5",
|
|
117
120
|
progress: "#04a5e5",
|
|
121
|
+
staleIcon: "#df8e1d",
|
|
122
|
+
staleText: "#df8e1d",
|
|
123
|
+
staleAge: "#df8e1d",
|
|
124
|
+
staleSubtitle: "#acb0c0",
|
|
118
125
|
},
|
|
119
126
|
},
|
|
120
127
|
};
|
|
@@ -91,6 +91,7 @@ export const catppuccinMochaTheme = {
|
|
|
91
91
|
successPillBg: "#313244",
|
|
92
92
|
error: "#f38ba8",
|
|
93
93
|
info: "#89b4fa",
|
|
94
|
+
warning: "#f9e2af",
|
|
94
95
|
border: "#313244",
|
|
95
96
|
},
|
|
96
97
|
markdown: {
|
|
@@ -105,6 +106,8 @@ export const catppuccinMochaTheme = {
|
|
|
105
106
|
untouched: "#8688a0",
|
|
106
107
|
number: "#cdd6f4",
|
|
107
108
|
activeNumber: "#89b4fa",
|
|
109
|
+
stale: "#f9e2af",
|
|
110
|
+
abandoned: "#f38ba8",
|
|
108
111
|
},
|
|
109
112
|
sessionPicker: {
|
|
110
113
|
border: "#89b4fa",
|
|
@@ -115,6 +118,10 @@ export const catppuccinMochaTheme = {
|
|
|
115
118
|
highlightFg: "#a6e3a1",
|
|
116
119
|
activeMark: "#89b4fa",
|
|
117
120
|
progress: "#89dceb",
|
|
121
|
+
staleIcon: "#f9e2af",
|
|
122
|
+
staleText: "#f9e2af",
|
|
123
|
+
staleAge: "#f9e2af",
|
|
124
|
+
staleSubtitle: "#8688a0",
|
|
118
125
|
},
|
|
119
126
|
},
|
|
120
127
|
};
|
|
@@ -92,6 +92,7 @@ export const darkTheme = {
|
|
|
92
92
|
successPillBg: "#0F2417",
|
|
93
93
|
error: "#FF5C57",
|
|
94
94
|
info: "#46D9FF",
|
|
95
|
+
warning: "#FFD36A",
|
|
95
96
|
border: "#2A3238",
|
|
96
97
|
},
|
|
97
98
|
markdown: {
|
|
@@ -106,6 +107,8 @@ export const darkTheme = {
|
|
|
106
107
|
untouched: "#A0AAB4",
|
|
107
108
|
number: "#E7EEF5",
|
|
108
109
|
activeNumber: "#46D9FF",
|
|
110
|
+
stale: "#FFD36A",
|
|
111
|
+
abandoned: "#FF5C57",
|
|
109
112
|
},
|
|
110
113
|
sessionPicker: {
|
|
111
114
|
border: "#46D9FF",
|
|
@@ -116,6 +119,10 @@ export const darkTheme = {
|
|
|
116
119
|
highlightFg: "#5AF78E",
|
|
117
120
|
activeMark: "#46D9FF",
|
|
118
121
|
progress: "#46D9FF",
|
|
122
|
+
staleIcon: "#FFD36A",
|
|
123
|
+
staleText: "#FFD36A",
|
|
124
|
+
staleAge: "#FFD36A",
|
|
125
|
+
staleSubtitle: "#A0AAB4",
|
|
119
126
|
},
|
|
120
127
|
},
|
|
121
128
|
};
|
|
@@ -91,6 +91,7 @@ export const draculaTheme = {
|
|
|
91
91
|
successPillBg: "#44475a", // current line
|
|
92
92
|
error: "#ff5555",
|
|
93
93
|
info: "#bd93f9",
|
|
94
|
+
warning: "#f1fa8c",
|
|
94
95
|
border: "#44475a",
|
|
95
96
|
},
|
|
96
97
|
markdown: {
|
|
@@ -105,6 +106,8 @@ export const draculaTheme = {
|
|
|
105
106
|
untouched: "#7C8BBE",
|
|
106
107
|
number: "#f8f8f2",
|
|
107
108
|
activeNumber: "#bd93f9",
|
|
109
|
+
stale: "#f1fa8c",
|
|
110
|
+
abandoned: "#ff5555",
|
|
108
111
|
},
|
|
109
112
|
sessionPicker: {
|
|
110
113
|
border: "#bd93f9",
|
|
@@ -115,6 +118,10 @@ export const draculaTheme = {
|
|
|
115
118
|
highlightFg: "#50fa7b",
|
|
116
119
|
activeMark: "#bd93f9",
|
|
117
120
|
progress: "#8be9fd",
|
|
121
|
+
staleIcon: "#f1fa8c",
|
|
122
|
+
staleText: "#f1fa8c",
|
|
123
|
+
staleAge: "#f1fa8c",
|
|
124
|
+
staleSubtitle: "#7C8BBE",
|
|
118
125
|
},
|
|
119
126
|
},
|
|
120
127
|
};
|
|
@@ -90,6 +90,7 @@ export const githubDarkTheme = {
|
|
|
90
90
|
successPillBg: "#161b22",
|
|
91
91
|
error: "#f85149",
|
|
92
92
|
info: "#58a6ff",
|
|
93
|
+
warning: "#d29922",
|
|
93
94
|
border: "#30363d",
|
|
94
95
|
},
|
|
95
96
|
markdown: {
|
|
@@ -104,6 +105,8 @@ export const githubDarkTheme = {
|
|
|
104
105
|
untouched: "#a0a8b4",
|
|
105
106
|
number: "#c9d1d9",
|
|
106
107
|
activeNumber: "#58a6ff",
|
|
108
|
+
stale: "#d29922",
|
|
109
|
+
abandoned: "#f85149",
|
|
107
110
|
},
|
|
108
111
|
sessionPicker: {
|
|
109
112
|
border: "#58a6ff",
|
|
@@ -114,6 +117,10 @@ export const githubDarkTheme = {
|
|
|
114
117
|
highlightFg: "#3fb950",
|
|
115
118
|
activeMark: "#58a6ff",
|
|
116
119
|
progress: "#58a6ff",
|
|
120
|
+
staleIcon: "#d29922",
|
|
121
|
+
staleText: "#d29922",
|
|
122
|
+
staleAge: "#d29922",
|
|
123
|
+
staleSubtitle: "#a0a8b4",
|
|
117
124
|
},
|
|
118
125
|
},
|
|
119
126
|
};
|
|
@@ -90,6 +90,7 @@ export const githubLightTheme = {
|
|
|
90
90
|
successPillBg: "#f6f8fa",
|
|
91
91
|
error: "#CF222E",
|
|
92
92
|
info: "#0969DA",
|
|
93
|
+
warning: "#9A6700",
|
|
93
94
|
border: "#D0D7DE",
|
|
94
95
|
},
|
|
95
96
|
markdown: {
|
|
@@ -104,6 +105,8 @@ export const githubLightTheme = {
|
|
|
104
105
|
untouched: "#6E7781",
|
|
105
106
|
number: "#24292F",
|
|
106
107
|
activeNumber: "#0969DA",
|
|
108
|
+
stale: "#9A6700",
|
|
109
|
+
abandoned: "#CF222E",
|
|
107
110
|
},
|
|
108
111
|
sessionPicker: {
|
|
109
112
|
border: "#0969DA",
|
|
@@ -114,6 +117,10 @@ export const githubLightTheme = {
|
|
|
114
117
|
highlightFg: "#1A7F37",
|
|
115
118
|
activeMark: "#0969DA",
|
|
116
119
|
progress: "#0969DA",
|
|
120
|
+
staleIcon: "#9A6700",
|
|
121
|
+
staleText: "#9A6700",
|
|
122
|
+
staleAge: "#9A6700",
|
|
123
|
+
staleSubtitle: "#6E7781",
|
|
117
124
|
},
|
|
118
125
|
},
|
|
119
126
|
};
|
|
@@ -91,6 +91,7 @@ export const gruvboxDarkTheme = {
|
|
|
91
91
|
successPillBg: "#3c3836", // dark1
|
|
92
92
|
error: "#cc241d",
|
|
93
93
|
info: "#458588",
|
|
94
|
+
warning: "#d79921",
|
|
94
95
|
border: "#3c3836",
|
|
95
96
|
},
|
|
96
97
|
markdown: {
|
|
@@ -105,6 +106,8 @@ export const gruvboxDarkTheme = {
|
|
|
105
106
|
untouched: "#a89984",
|
|
106
107
|
number: "#ebdbb2",
|
|
107
108
|
activeNumber: "#458588",
|
|
109
|
+
stale: "#d79921",
|
|
110
|
+
abandoned: "#cc241d",
|
|
108
111
|
},
|
|
109
112
|
sessionPicker: {
|
|
110
113
|
border: "#458588",
|
|
@@ -115,6 +118,10 @@ export const gruvboxDarkTheme = {
|
|
|
115
118
|
highlightFg: "#98971a",
|
|
116
119
|
activeMark: "#458588",
|
|
117
120
|
progress: "#458588",
|
|
121
|
+
staleIcon: "#d79921",
|
|
122
|
+
staleText: "#d79921",
|
|
123
|
+
staleAge: "#d79921",
|
|
124
|
+
staleSubtitle: "#a89984",
|
|
118
125
|
},
|
|
119
126
|
},
|
|
120
127
|
};
|