auq-mcp-server 2.1.1 → 2.2.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.
Files changed (35) hide show
  1. package/dist/bin/tui-app.js +199 -18
  2. package/dist/package.json +1 -1
  3. package/dist/src/i18n/locales/en.js +3 -0
  4. package/dist/src/i18n/locales/ko.js +3 -0
  5. package/dist/src/tui/components/Footer.js +6 -1
  6. package/dist/src/tui/components/OptionsList.js +16 -3
  7. package/dist/src/tui/components/QuestionDisplay.js +5 -9
  8. package/dist/src/tui/components/SessionDots.js +65 -0
  9. package/dist/src/tui/components/SessionPicker.js +159 -0
  10. package/dist/src/tui/components/StepperView.js +55 -7
  11. package/dist/src/tui/components/__tests__/SessionDots.test.js +92 -0
  12. package/dist/src/tui/components/__tests__/SessionPicker.test.js +125 -0
  13. package/dist/src/tui/components/__tests__/StepperView.state.test.js +101 -0
  14. package/dist/src/tui/themes/catppuccin-latte.js +18 -0
  15. package/dist/src/tui/themes/catppuccin-mocha.js +18 -0
  16. package/dist/src/tui/themes/dark.js +18 -0
  17. package/dist/src/tui/themes/dracula.js +18 -0
  18. package/dist/src/tui/themes/github-dark.js +18 -0
  19. package/dist/src/tui/themes/github-light.js +18 -0
  20. package/dist/src/tui/themes/gruvbox-dark.js +18 -0
  21. package/dist/src/tui/themes/gruvbox-light.js +18 -0
  22. package/dist/src/tui/themes/light.js +18 -0
  23. package/dist/src/tui/themes/monokai.js +18 -0
  24. package/dist/src/tui/themes/nord.js +18 -0
  25. package/dist/src/tui/themes/one-dark.js +18 -0
  26. package/dist/src/tui/themes/rose-pine.js +18 -0
  27. package/dist/src/tui/themes/solarized-dark.js +18 -0
  28. package/dist/src/tui/themes/solarized-light.js +18 -0
  29. package/dist/src/tui/themes/tokyo-night.js +18 -0
  30. package/dist/src/tui/types.js +1 -0
  31. package/dist/src/tui/utils/__tests__/relativeTime.test.js +31 -0
  32. package/dist/src/tui/utils/__tests__/sessionSwitching.test.js +82 -0
  33. package/dist/src/tui/utils/relativeTime.js +24 -0
  34. package/dist/src/tui/utils/sessionSwitching.js +56 -0
  35. package/package.json +1 -1
@@ -14,7 +14,7 @@ import { ReviewScreen } from "./ReviewScreen.js";
14
14
  * StepperView orchestrates the question-answering flow
15
15
  * Manages state for current question, answers, and navigation
16
16
  */
17
- export const StepperView = ({ onComplete, onProgress, sessionId, sessionRequest, }) => {
17
+ export const StepperView = ({ onComplete, onProgress, hasMultipleSessions, initialState, onStateSnapshot, onFlowStateChange, sessionId, sessionRequest, }) => {
18
18
  const { theme } = useTheme();
19
19
  const config = useConfig();
20
20
  const { exit } = useApp();
@@ -25,6 +25,7 @@ export const StepperView = ({ onComplete, onProgress, sessionId, sessionRequest,
25
25
  const [showRejectionConfirm, setShowRejectionConfirm] = useState(false);
26
26
  const [elapsedSeconds, setElapsedSeconds] = useState(0);
27
27
  const [focusContext, setFocusContext] = useState("option");
28
+ const [focusedOptionIndex, setFocusedOptionIndex] = useState(0);
28
29
  const [hasRecommendedOptions, setHasRecommendedOptions] = useState(false);
29
30
  // Session-level flag: true if ANY question in the session has recommended options
30
31
  const [hasAnyRecommendedInSession, setHasAnyRecommendedInSession] = useState(false);
@@ -135,6 +136,7 @@ export const StepperView = ({ onComplete, onProgress, sessionId, sessionRequest,
135
136
  };
136
137
  // Track mount status to avoid state updates after unmount
137
138
  const isMountedRef = useRef(true);
139
+ const skipSnapshotRef = useRef(true);
138
140
  useEffect(() => {
139
141
  isMountedRef.current = true;
140
142
  return () => {
@@ -143,17 +145,63 @@ export const StepperView = ({ onComplete, onProgress, sessionId, sessionRequest,
143
145
  }, []);
144
146
  // Reset internal stepper state when the session changes (safety in case component isn't remounted)
145
147
  useEffect(() => {
146
- setCurrentQuestionIndex(0);
147
- setAnswers(new Map());
148
- setShowReview(false);
148
+ const maxQuestionIndex = Math.max(0, sessionRequest.questions.length - 1);
149
+ if (initialState) {
150
+ const hydratedQuestionIndex = Math.min(Math.max(initialState.currentQuestionIndex, 0), maxQuestionIndex);
151
+ const hydratedQuestion = sessionRequest.questions[hydratedQuestionIndex];
152
+ const maxFocusedOptionIndex = (hydratedQuestion?.options.length ?? 0) + 1;
153
+ setCurrentQuestionIndex(hydratedQuestionIndex);
154
+ setAnswers(new Map(initialState.answers));
155
+ setElaborateMarks(new Map(initialState.elaborateMarks));
156
+ setFocusContext(initialState.focusContext);
157
+ setFocusedOptionIndex(Math.min(Math.max(initialState.focusedOptionIndex, 0), Math.max(0, maxFocusedOptionIndex)));
158
+ setShowReview(initialState.showReview);
159
+ }
160
+ else {
161
+ setCurrentQuestionIndex(0);
162
+ setAnswers(new Map());
163
+ setElaborateMarks(new Map());
164
+ setFocusContext("option");
165
+ setFocusedOptionIndex(0);
166
+ setShowReview(false);
167
+ }
149
168
  setSubmitting(false);
150
169
  setShowRejectionConfirm(false);
151
170
  setElapsedSeconds(0);
152
- setElaborateMarks(new Map());
171
+ skipSnapshotRef.current = true;
153
172
  // Compute session-level recommended flag: true if ANY question has recommended options
154
173
  const anyHasRecommended = sessionRequest.questions.some((question) => question.options.some((opt) => isRecommendedOption(opt.label)));
155
174
  setHasAnyRecommendedInSession(anyHasRecommended);
156
- }, [sessionId, sessionRequest.questions]);
175
+ }, [initialState, sessionId, sessionRequest.questions]);
176
+ useEffect(() => {
177
+ if (!onStateSnapshot) {
178
+ return;
179
+ }
180
+ if (skipSnapshotRef.current) {
181
+ skipSnapshotRef.current = false;
182
+ return;
183
+ }
184
+ onStateSnapshot(sessionId, {
185
+ currentQuestionIndex,
186
+ answers: new Map(answers),
187
+ elaborateMarks: new Map(elaborateMarks),
188
+ focusContext,
189
+ focusedOptionIndex,
190
+ showReview,
191
+ });
192
+ }, [
193
+ answers,
194
+ currentQuestionIndex,
195
+ elaborateMarks,
196
+ focusContext,
197
+ focusedOptionIndex,
198
+ onStateSnapshot,
199
+ sessionId,
200
+ showReview,
201
+ ]);
202
+ useEffect(() => {
203
+ onFlowStateChange?.({ showReview, showRejectionConfirm });
204
+ }, [onFlowStateChange, showRejectionConfirm, showReview]);
157
205
  // Update elapsed time since session creation
158
206
  // IMPORTANT: Pause when content overflows terminal to prevent scroll-snapping
159
207
  useEffect(() => {
@@ -434,5 +482,5 @@ export const StepperView = ({ onComplete, onProgress, sessionId, sessionRequest,
434
482
  return (React.createElement(ReviewScreen, { isSubmitting: submitting, answers: answers, elapsedLabel: elapsedLabel, onConfirm: handleConfirm, onGoBack: handleGoBack, questions: sessionRequest.questions, sessionId: sessionId, elaborateMarks: elaborateMarks }));
435
483
  }
436
484
  // Show question display (default)
437
- return (React.createElement(QuestionDisplay, { currentQuestion: currentQuestion, currentQuestionIndex: currentQuestionIndex, customAnswer: currentAnswer?.customText, elapsedLabel: elapsedLabel, onAdvanceToNext: handleAdvanceToNext, onChangeCustomAnswer: handleChangeCustomAnswer, onSelectOption: handleSelectOption, onToggleOption: handleToggleOption, multiSelect: currentQuestion.multiSelect, questions: sessionRequest.questions, selectedOption: currentAnswer?.selectedOption, answers: answers, onFocusContextChange: setFocusContext, workingDirectory: sessionRequest.workingDirectory, onRecommendedDetected: setHasRecommendedOptions, hasRecommendedOptions: hasRecommendedOptions, hasAnyRecommendedInSession: hasAnyRecommendedInSession, elaborateMarks: elaborateMarks, onElaborateSelect: handleElaborateSelect, elaborateText: elaborateMarks.get(currentQuestionIndex) || "", onElaborateTextChange: handleElaborateTextChange }));
485
+ return (React.createElement(QuestionDisplay, { currentQuestion: currentQuestion, currentQuestionIndex: currentQuestionIndex, customAnswer: currentAnswer?.customText, elapsedLabel: elapsedLabel, onAdvanceToNext: handleAdvanceToNext, onChangeCustomAnswer: handleChangeCustomAnswer, onSelectOption: handleSelectOption, onToggleOption: handleToggleOption, multiSelect: currentQuestion.multiSelect, questions: sessionRequest.questions, selectedOption: currentAnswer?.selectedOption, answers: answers, focusContext: focusContext, onFocusContextChange: setFocusContext, focusedOptionIndex: focusedOptionIndex, onFocusedOptionIndexChange: setFocusedOptionIndex, workingDirectory: sessionRequest.workingDirectory, onRecommendedDetected: setHasRecommendedOptions, hasRecommendedOptions: hasRecommendedOptions, hasAnyRecommendedInSession: hasAnyRecommendedInSession, elaborateMarks: elaborateMarks, onElaborateSelect: handleElaborateSelect, elaborateText: elaborateMarks.get(currentQuestionIndex) || "", onElaborateTextChange: handleElaborateTextChange, showSessionSwitching: hasMultipleSessions && !showReview && !showRejectionConfirm }));
438
486
  };
@@ -0,0 +1,92 @@
1
+ import React from "react";
2
+ import { cleanup, render } from "ink-testing-library";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { ThemeContext } from "../../ThemeContext.js";
5
+ import { darkTheme } from "../../themes/dark.js";
6
+ import { SessionDots } from "../SessionDots.js";
7
+ const mockThemeValue = {
8
+ theme: darkTheme,
9
+ themeName: "AUQ dark",
10
+ cycleTheme: () => { },
11
+ };
12
+ function renderWithTheme(ui) {
13
+ return render(React.createElement(ThemeContext.Provider, { value: mockThemeValue }, ui));
14
+ }
15
+ function getOutput(frame) {
16
+ return (frame ?? "").replace(/\x1b\[[0-9;]*m/g, "").replace(/\r/g, "");
17
+ }
18
+ function createSession(id) {
19
+ return {
20
+ sessionId: `test-id-${id}`,
21
+ sessionRequest: {
22
+ sessionId: `test-id-${id}`,
23
+ status: "pending",
24
+ timestamp: new Date("2026-01-01T00:00:00.000Z").toISOString(),
25
+ workingDirectory: `/test/${id}`,
26
+ questions: [
27
+ {
28
+ title: `Q${id}`,
29
+ prompt: "test?",
30
+ options: [{ label: "A" }],
31
+ multiSelect: false,
32
+ },
33
+ ],
34
+ },
35
+ timestamp: new Date("2026-01-01T00:00:00.000Z"),
36
+ };
37
+ }
38
+ afterEach(() => {
39
+ cleanup();
40
+ vi.restoreAllMocks();
41
+ });
42
+ describe("SessionDots", () => {
43
+ it("renders nothing when sessions length is less than 2", () => {
44
+ const instance = renderWithTheme(React.createElement(SessionDots, { sessions: [createSession(1)], activeIndex: 0, sessionUIStates: {} }));
45
+ expect(getOutput(instance.lastFrame())).toBe("");
46
+ });
47
+ it("renders numbered dots for each session", () => {
48
+ const sessions = [createSession(1), createSession(2), createSession(3)];
49
+ const instance = renderWithTheme(React.createElement(SessionDots, { sessions: sessions, activeIndex: 1, sessionUIStates: {} }));
50
+ const output = getOutput(instance.lastFrame());
51
+ expect(output).toContain("1");
52
+ expect(output).toContain("2");
53
+ expect(output).toContain("3");
54
+ expect(output).toContain("●");
55
+ expect(output).toContain("○");
56
+ });
57
+ it("renders progress-state sessions with one active and remaining inactive dots", () => {
58
+ const sessions = [
59
+ createSession(1),
60
+ createSession(2),
61
+ createSession(3),
62
+ createSession(4),
63
+ ];
64
+ const answeredState = {
65
+ currentQuestionIndex: 0,
66
+ answers: new Map([[0, { selectedOption: "A" }]]),
67
+ elaborateMarks: new Map(),
68
+ focusContext: "option",
69
+ focusedOptionIndex: 0,
70
+ showReview: false,
71
+ };
72
+ const inProgressState = {
73
+ currentQuestionIndex: 0,
74
+ answers: new Map(),
75
+ elaborateMarks: new Map(),
76
+ focusContext: "option",
77
+ focusedOptionIndex: 0,
78
+ showReview: false,
79
+ };
80
+ const instance = renderWithTheme(React.createElement(SessionDots, { sessions: sessions, activeIndex: 0, sessionUIStates: {
81
+ [sessions[1].sessionId]: answeredState,
82
+ [sessions[2].sessionId]: inProgressState,
83
+ } }));
84
+ const output = getOutput(instance.lastFrame());
85
+ expect((output.match(/●/g) ?? []).length).toBe(1);
86
+ expect((output.match(/○/g) ?? []).length).toBe(3);
87
+ expect(output).toContain("1");
88
+ expect(output).toContain("2");
89
+ expect(output).toContain("3");
90
+ expect(output).toContain("4");
91
+ });
92
+ });
@@ -0,0 +1,125 @@
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
+ if (options?.isActive) {
13
+ inputState.handler = handler;
14
+ }
15
+ },
16
+ };
17
+ });
18
+ import { ThemeContext } from "../../ThemeContext.js";
19
+ import { darkTheme } from "../../themes/dark.js";
20
+ import { SessionPicker, } from "../SessionPicker.js";
21
+ const mockThemeValue = {
22
+ theme: darkTheme,
23
+ themeName: "AUQ dark",
24
+ cycleTheme: () => { },
25
+ };
26
+ function renderWithTheme(ui) {
27
+ return render(React.createElement(ThemeContext.Provider, { value: mockThemeValue }, ui));
28
+ }
29
+ function getOutput(frame) {
30
+ return (frame ?? "").replace(/\x1b\[[0-9;]*m/g, "").replace(/\r/g, "");
31
+ }
32
+ function createSession(id) {
33
+ return {
34
+ sessionId: `picker-id-${id}`,
35
+ sessionRequest: {
36
+ sessionId: `picker-id-${id}`,
37
+ status: "pending",
38
+ timestamp: new Date("2026-01-01T00:00:00.000Z").toISOString(),
39
+ workingDirectory: `/work/dir-${id}`,
40
+ questions: [
41
+ {
42
+ title: `Title ${id}`,
43
+ prompt: "choose",
44
+ options: [{ label: "A" }],
45
+ multiSelect: false,
46
+ },
47
+ ],
48
+ },
49
+ timestamp: new Date("2026-01-01T00:00:00.000Z"),
50
+ };
51
+ }
52
+ afterEach(() => {
53
+ cleanup();
54
+ inputState.handler = null;
55
+ vi.restoreAllMocks();
56
+ });
57
+ describe("SessionPicker", () => {
58
+ it("renders nothing when isOpen is false", () => {
59
+ const instance = renderWithTheme(React.createElement(SessionPicker, { isOpen: false, sessions: [createSession(1), createSession(2)], activeIndex: 0, sessionUIStates: {}, onSelectIndex: () => { }, onClose: () => { } }));
60
+ expect(getOutput(instance.lastFrame())).toBe("");
61
+ });
62
+ it("renders session rows with number, title, directory, progress, and active marker", () => {
63
+ const sessions = [createSession(1), createSession(2)];
64
+ const answeredState = {
65
+ currentQuestionIndex: 0,
66
+ answers: new Map([[0, { selectedOption: "A" }]]),
67
+ elaborateMarks: new Map(),
68
+ focusContext: "option",
69
+ focusedOptionIndex: 0,
70
+ showReview: false,
71
+ };
72
+ const instance = renderWithTheme(React.createElement(SessionPicker, { isOpen: true, sessions: sessions, activeIndex: 0, sessionUIStates: { [sessions[0].sessionId]: answeredState }, onSelectIndex: () => { }, onClose: () => { } }));
73
+ const output = getOutput(instance.lastFrame());
74
+ expect(output).toContain("Switch Session");
75
+ expect(output).toContain("1. Title 1");
76
+ expect(output).toContain("2. Title 2");
77
+ expect(output).toContain("/work/dir-1");
78
+ expect(output).toContain("[1/1]");
79
+ expect(output).toContain("[0/1]");
80
+ expect(output).toContain("► 1.");
81
+ });
82
+ it("handles down arrow without immediate selection", async () => {
83
+ const sessions = [createSession(1), createSession(2), createSession(3)];
84
+ const onSelectIndex = vi.fn();
85
+ const onClose = vi.fn();
86
+ renderWithTheme(React.createElement(SessionPicker, { isOpen: true, sessions: sessions, activeIndex: 0, sessionUIStates: {}, onSelectIndex: onSelectIndex, onClose: onClose }));
87
+ expect(inputState.handler).not.toBeNull();
88
+ inputState.handler("", { downArrow: true });
89
+ await Promise.resolve();
90
+ expect(onSelectIndex).not.toHaveBeenCalled();
91
+ expect(onClose).not.toHaveBeenCalled();
92
+ });
93
+ it("selects the currently highlighted session on Enter", async () => {
94
+ const sessions = [createSession(1), createSession(2), createSession(3)];
95
+ const onSelectIndex = vi.fn();
96
+ const onClose = vi.fn();
97
+ renderWithTheme(React.createElement(SessionPicker, { isOpen: true, sessions: sessions, activeIndex: 0, sessionUIStates: {}, onSelectIndex: onSelectIndex, onClose: onClose }));
98
+ expect(inputState.handler).not.toBeNull();
99
+ inputState.handler("", { return: true });
100
+ await Promise.resolve();
101
+ expect(onSelectIndex).toHaveBeenCalledWith(0);
102
+ expect(onClose).toHaveBeenCalled();
103
+ });
104
+ it("supports direct number key selection", async () => {
105
+ const sessions = [createSession(1), createSession(2), createSession(3)];
106
+ const onSelectIndex = vi.fn();
107
+ const onClose = vi.fn();
108
+ renderWithTheme(React.createElement(SessionPicker, { isOpen: true, sessions: sessions, activeIndex: 0, sessionUIStates: {}, onSelectIndex: onSelectIndex, onClose: onClose }));
109
+ expect(inputState.handler).not.toBeNull();
110
+ inputState.handler("2", {});
111
+ await Promise.resolve();
112
+ expect(onSelectIndex).toHaveBeenCalledWith(1);
113
+ expect(onClose).toHaveBeenCalled();
114
+ });
115
+ it("calls onClose on Escape", async () => {
116
+ const onSelectIndex = vi.fn();
117
+ const onClose = vi.fn();
118
+ renderWithTheme(React.createElement(SessionPicker, { isOpen: true, sessions: [createSession(1), createSession(2)], activeIndex: 0, sessionUIStates: {}, onSelectIndex: onSelectIndex, onClose: onClose }));
119
+ expect(inputState.handler).not.toBeNull();
120
+ inputState.handler("", { escape: true });
121
+ await Promise.resolve();
122
+ expect(onClose).toHaveBeenCalledTimes(1);
123
+ expect(onSelectIndex).not.toHaveBeenCalled();
124
+ });
125
+ });
@@ -0,0 +1,101 @@
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: "session-1",
15
+ status: "pending",
16
+ timestamp: new Date().toISOString(),
17
+ callId: "call-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
+ title: "Runtime",
31
+ prompt: "Pick a runtime",
32
+ options: [{ label: "Bun" }, { label: "Node.js" }],
33
+ },
34
+ ],
35
+ };
36
+ function renderStepper(props = {}) {
37
+ return render(React.createElement(ThemeContext.Provider, { value: mockThemeValue },
38
+ React.createElement(ConfigProvider, null,
39
+ React.createElement(StepperView, { sessionId: sessionRequest.sessionId, sessionRequest: sessionRequest, ...props }))));
40
+ }
41
+ function getOutput(frame) {
42
+ return (frame ?? "").replace(/\x1b\[[0-9;]*m/g, "").replace(/\r/g, "");
43
+ }
44
+ afterEach(() => {
45
+ cleanup();
46
+ vi.restoreAllMocks();
47
+ });
48
+ describe("StepperView SessionUIState boundary", () => {
49
+ it("hydrates from initialState and starts on the hydrated question", async () => {
50
+ const initialState = {
51
+ currentQuestionIndex: 1,
52
+ answers: new Map([[0, { selectedOption: "TypeScript" }]]),
53
+ elaborateMarks: new Map([[0, "Please elaborate"]]),
54
+ focusContext: "option",
55
+ focusedOptionIndex: 1,
56
+ showReview: false,
57
+ };
58
+ const instance = renderStepper({ initialState });
59
+ await vi.waitFor(() => {
60
+ const hydratedOutput = getOutput(instance.lastFrame());
61
+ expect(hydratedOutput).toContain("Pick a framework");
62
+ });
63
+ const output = getOutput(instance.lastFrame());
64
+ expect(output).not.toContain("Pick a language");
65
+ expect(output).toContain("1/3");
66
+ });
67
+ it("uses default state when no initialState is provided", () => {
68
+ const instance = renderStepper();
69
+ const output = getOutput(instance.lastFrame());
70
+ expect(output).toContain("Pick a language");
71
+ expect(output).not.toContain("Pick a framework");
72
+ expect(output).toContain("0/3");
73
+ });
74
+ it("emits snapshot after state change and skips initial mount emission", async () => {
75
+ const onStateSnapshot = vi.fn();
76
+ const instance = renderStepper({ onStateSnapshot });
77
+ expect(onStateSnapshot).not.toHaveBeenCalled();
78
+ instance.stdin.write("\t");
79
+ await vi.waitFor(() => {
80
+ expect(onStateSnapshot).toHaveBeenCalled();
81
+ });
82
+ const [sessionId, state] = onStateSnapshot.mock.lastCall;
83
+ expect(sessionId).toBe("session-1");
84
+ expect(state.currentQuestionIndex).toBe(1);
85
+ expect(state.focusContext).toBe("option");
86
+ expect(state.showReview).toBe(false);
87
+ expect(state.answers).toBeInstanceOf(Map);
88
+ expect(state.elaborateMarks).toBeInstanceOf(Map);
89
+ });
90
+ it("emits initial flow state via onFlowStateChange", async () => {
91
+ const onFlowStateChange = vi.fn();
92
+ renderStepper({ onFlowStateChange });
93
+ await vi.waitFor(() => {
94
+ expect(onFlowStateChange).toHaveBeenCalled();
95
+ });
96
+ expect(onFlowStateChange).toHaveBeenCalledWith({
97
+ showReview: false,
98
+ showRejectionConfirm: false,
99
+ });
100
+ });
101
+ });
@@ -98,5 +98,23 @@ export const catppuccinLatteTheme = {
98
98
  codeBlockText: "#4c4f69",
99
99
  codeBlockBorder: "#ccd0da",
100
100
  },
101
+ sessionDots: {
102
+ active: "#1e66f5",
103
+ answered: "#40a02b",
104
+ inProgress: "#df8e1d",
105
+ untouched: "#acb0c0",
106
+ number: "#4c4f69",
107
+ activeNumber: "#1e66f5",
108
+ },
109
+ sessionPicker: {
110
+ border: "#1e66f5",
111
+ title: "#1e66f5",
112
+ rowText: "#4c4f69",
113
+ rowDim: "#acb0c0",
114
+ highlightBg: "#ccd0da",
115
+ highlightFg: "#40a02b",
116
+ activeMark: "#1e66f5",
117
+ progress: "#04a5e5",
118
+ },
101
119
  },
102
120
  };
@@ -98,5 +98,23 @@ export const catppuccinMochaTheme = {
98
98
  codeBlockText: "#cdd6f4",
99
99
  codeBlockBorder: "#313244",
100
100
  },
101
+ sessionDots: {
102
+ active: "#89b4fa",
103
+ answered: "#a6e3a1",
104
+ inProgress: "#f9e2af",
105
+ untouched: "#8688a0",
106
+ number: "#cdd6f4",
107
+ activeNumber: "#89b4fa",
108
+ },
109
+ sessionPicker: {
110
+ border: "#89b4fa",
111
+ title: "#89b4fa",
112
+ rowText: "#cdd6f4",
113
+ rowDim: "#8688a0",
114
+ highlightBg: "#313244",
115
+ highlightFg: "#a6e3a1",
116
+ activeMark: "#89b4fa",
117
+ progress: "#89dceb",
118
+ },
101
119
  },
102
120
  };
@@ -99,5 +99,23 @@ export const darkTheme = {
99
99
  codeBlockText: "#E7EEF5",
100
100
  codeBlockBorder: "#2A3238",
101
101
  },
102
+ sessionDots: {
103
+ active: "#46D9FF",
104
+ answered: "#5AF78E",
105
+ inProgress: "#FFD36A",
106
+ untouched: "#A0AAB4",
107
+ number: "#E7EEF5",
108
+ activeNumber: "#46D9FF",
109
+ },
110
+ sessionPicker: {
111
+ border: "#46D9FF",
112
+ title: "#46D9FF",
113
+ rowText: "#E7EEF5",
114
+ rowDim: "#A0AAB4",
115
+ highlightBg: "#0F2417",
116
+ highlightFg: "#5AF78E",
117
+ activeMark: "#46D9FF",
118
+ progress: "#46D9FF",
119
+ },
102
120
  },
103
121
  };
@@ -98,5 +98,23 @@ export const draculaTheme = {
98
98
  codeBlockText: "#f8f8f2",
99
99
  codeBlockBorder: "#44475a",
100
100
  },
101
+ sessionDots: {
102
+ active: "#bd93f9",
103
+ answered: "#50fa7b",
104
+ inProgress: "#f1fa8c",
105
+ untouched: "#7C8BBE",
106
+ number: "#f8f8f2",
107
+ activeNumber: "#bd93f9",
108
+ },
109
+ sessionPicker: {
110
+ border: "#bd93f9",
111
+ title: "#bd93f9",
112
+ rowText: "#f8f8f2",
113
+ rowDim: "#7C8BBE",
114
+ highlightBg: "#44475a",
115
+ highlightFg: "#50fa7b",
116
+ activeMark: "#bd93f9",
117
+ progress: "#8be9fd",
118
+ },
101
119
  },
102
120
  };
@@ -97,5 +97,23 @@ export const githubDarkTheme = {
97
97
  codeBlockText: "#c9d1d9",
98
98
  codeBlockBorder: "#30363d",
99
99
  },
100
+ sessionDots: {
101
+ active: "#58a6ff",
102
+ answered: "#3fb950",
103
+ inProgress: "#d29922",
104
+ untouched: "#a0a8b4",
105
+ number: "#c9d1d9",
106
+ activeNumber: "#58a6ff",
107
+ },
108
+ sessionPicker: {
109
+ border: "#58a6ff",
110
+ title: "#58a6ff",
111
+ rowText: "#c9d1d9",
112
+ rowDim: "#a0a8b4",
113
+ highlightBg: "#0d1117",
114
+ highlightFg: "#3fb950",
115
+ activeMark: "#58a6ff",
116
+ progress: "#58a6ff",
117
+ },
100
118
  },
101
119
  };
@@ -97,5 +97,23 @@ export const githubLightTheme = {
97
97
  codeBlockText: "#24292F",
98
98
  codeBlockBorder: "#D0D7DE",
99
99
  },
100
+ sessionDots: {
101
+ active: "#0969DA",
102
+ answered: "#1A7F37",
103
+ inProgress: "#9A6700",
104
+ untouched: "#6E7781",
105
+ number: "#24292F",
106
+ activeNumber: "#0969DA",
107
+ },
108
+ sessionPicker: {
109
+ border: "#0969DA",
110
+ title: "#0969DA",
111
+ rowText: "#24292F",
112
+ rowDim: "#6E7781",
113
+ highlightBg: "#DAFBE1",
114
+ highlightFg: "#1A7F37",
115
+ activeMark: "#0969DA",
116
+ progress: "#0969DA",
117
+ },
100
118
  },
101
119
  };
@@ -98,5 +98,23 @@ export const gruvboxDarkTheme = {
98
98
  codeBlockText: "#ebdbb2",
99
99
  codeBlockBorder: "#3c3836",
100
100
  },
101
+ sessionDots: {
102
+ active: "#458588",
103
+ answered: "#98971a",
104
+ inProgress: "#d79921",
105
+ untouched: "#a89984",
106
+ number: "#ebdbb2",
107
+ activeNumber: "#458588",
108
+ },
109
+ sessionPicker: {
110
+ border: "#458588",
111
+ title: "#458588",
112
+ rowText: "#ebdbb2",
113
+ rowDim: "#a89984",
114
+ highlightBg: "#3c3836",
115
+ highlightFg: "#98971a",
116
+ activeMark: "#458588",
117
+ progress: "#458588",
118
+ },
101
119
  },
102
120
  };
@@ -98,5 +98,23 @@ export const gruvboxLightTheme = {
98
98
  codeBlockText: "#3c3836",
99
99
  codeBlockBorder: "#d5c4a1",
100
100
  },
101
+ sessionDots: {
102
+ active: "#076678",
103
+ answered: "#79740e",
104
+ inProgress: "#b57614",
105
+ untouched: "#a89984",
106
+ number: "#3c3836",
107
+ activeNumber: "#076678",
108
+ },
109
+ sessionPicker: {
110
+ border: "#076678",
111
+ title: "#076678",
112
+ rowText: "#3c3836",
113
+ rowDim: "#a89984",
114
+ highlightBg: "#ebdbb2",
115
+ highlightFg: "#79740e",
116
+ activeMark: "#076678",
117
+ progress: "#076678",
118
+ },
101
119
  },
102
120
  };
@@ -98,5 +98,23 @@ export const lightTheme = {
98
98
  codeBlockText: "#24292F",
99
99
  codeBlockBorder: "#D0D7DE",
100
100
  },
101
+ sessionDots: {
102
+ active: "#007EA7",
103
+ answered: "#2DA44E",
104
+ inProgress: "#B07D00",
105
+ untouched: "#6E7781",
106
+ number: "#24292F",
107
+ activeNumber: "#007EA7",
108
+ },
109
+ sessionPicker: {
110
+ border: "#007EA7",
111
+ title: "#007EA7",
112
+ rowText: "#24292F",
113
+ rowDim: "#6E7781",
114
+ highlightBg: "#E6F9EE",
115
+ highlightFg: "#2DA44E",
116
+ activeMark: "#007EA7",
117
+ progress: "#007EA7",
118
+ },
101
119
  },
102
120
  };
@@ -99,5 +99,23 @@ export const monokaiTheme = {
99
99
  codeBlockText: "#F8F8F2",
100
100
  codeBlockBorder: "#49483E",
101
101
  },
102
+ sessionDots: {
103
+ active: "#66D9EF",
104
+ answered: "#A6E22E",
105
+ inProgress: "#E6DB74",
106
+ untouched: "#908B78",
107
+ number: "#F8F8F2",
108
+ activeNumber: "#66D9EF",
109
+ },
110
+ sessionPicker: {
111
+ border: "#66D9EF",
112
+ title: "#66D9EF",
113
+ rowText: "#F8F8F2",
114
+ rowDim: "#908B78",
115
+ highlightBg: "#383830",
116
+ highlightFg: "#A6E22E",
117
+ activeMark: "#66D9EF",
118
+ progress: "#66D9EF",
119
+ },
102
120
  },
103
121
  };