auq-mcp-server 3.2.5 → 3.2.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auq-mcp-server",
3
- "version": "3.2.5",
3
+ "version": "3.2.6",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "auq": "bin/auq"
@@ -33,8 +33,8 @@ export const Footer = ({ focusContext, multiSelect, isReviewScreen = false, show
33
33
  return [
34
34
  { key: KEY_LABELS.NAVIGATE_OPTIONS, action: t("footer.options") },
35
35
  { key: KEY_LABELS.CURSOR, action: t("footer.cursor") },
36
- { key: KEY_LABELS.NAVIGATE_QUESTIONS_TAB, action: t("footer.questions") },
37
36
  { key: KEY_LABELS.NEWLINE, action: t("footer.newline") },
37
+ { key: KEY_LABELS.ADVANCE_INPUT, action: t("footer.next") },
38
38
  { key: KEY_LABELS.REJECT, action: t("footer.reject") },
39
39
  ];
40
40
  }
@@ -43,7 +43,8 @@ export const Footer = ({ focusContext, multiSelect, isReviewScreen = false, show
43
43
  return [
44
44
  { key: KEY_LABELS.NAVIGATE_OPTIONS, action: t("footer.options") },
45
45
  { key: KEY_LABELS.CURSOR, action: t("footer.cursor") },
46
- { key: "Enter/Tab", action: t("footer.next") },
46
+ { key: KEY_LABELS.NEWLINE, action: t("footer.newline") },
47
+ { key: KEY_LABELS.ADVANCE_INPUT, action: t("footer.next") },
47
48
  { key: KEY_LABELS.REJECT, action: t("footer.reject") },
48
49
  ];
49
50
  }
@@ -5,9 +5,9 @@ import { useTheme } from "../ThemeContext.js";
5
5
  /**
6
6
  * Multi-line text input component for Ink with cursor positioning
7
7
  * Supports left/right arrow keys for cursor movement
8
- * Enter for newlines, Tab to submit (portable across terminals)
8
+ * Enter to submit/advance, Shift+Enter for newlines, Tab also submits
9
9
  */
10
- export const MultiLineTextInput = ({ isFocused = true, onChange, onSubmit, placeholder = t("input.multiLinePlaceholder"), value, enterSubmits = false, }) => {
10
+ export const MultiLineTextInput = ({ isFocused = true, onChange, onSubmit, placeholder = t("input.multiLinePlaceholder"), value, enterSubmits: _enterSubmits = false, }) => {
11
11
  const { theme } = useTheme();
12
12
  // Initialize cursor at end of text (using character count for CJK support)
13
13
  const [cursorPosition, setCursorPosition] = useState([...value].length);
@@ -45,18 +45,20 @@ export const MultiLineTextInput = ({ isFocused = true, onChange, onSubmit, place
45
45
  if (key.tab && key.shift) {
46
46
  return;
47
47
  }
48
- // Enter: Submit if enterSubmits mode, otherwise add newline
48
+ // Enter: Submit/advance; Shift+Enter inserts newline
49
49
  if (input === "\r" || input === "\n" || key.return) {
50
- if (enterSubmits) {
51
- onSubmit?.();
50
+ if (key.shift) {
51
+ // Shift+Enter: Insert newline at cursor position
52
+ const chars = [...currentValue];
53
+ const before = chars.slice(0, currentCursor).join("");
54
+ const after = chars.slice(currentCursor).join("");
55
+ const newValue = before + "\n" + after;
56
+ onChange(newValue);
57
+ setCursorPosition(currentCursor + 1);
52
58
  return;
53
59
  }
54
- const chars = [...currentValue];
55
- const before = chars.slice(0, currentCursor).join("");
56
- const after = chars.slice(currentCursor).join("");
57
- const newValue = before + "\n" + after;
58
- onChange(newValue);
59
- setCursorPosition(currentCursor + 1);
60
+ // Enter (without Shift): Submit/advance
61
+ onSubmit?.();
60
62
  return;
61
63
  }
62
64
  // Left arrow: Move cursor left (accounting for CJK characters)
@@ -77,24 +77,37 @@ export const StepperView = ({ onComplete, onProgress, hasMultipleSessions, initi
77
77
  }, [currentQuestionIndex]);
78
78
  // Handle option selection (single-select mode)
79
79
  const handleSelectOption = (label) => {
80
+ const existing = answers.get(currentQuestionIndex) || {};
81
+ const isDeselecting = existing.selectedOption === label;
80
82
  setAnswers((prev) => {
81
83
  const newAnswers = new Map(prev);
82
- const existing = newAnswers.get(currentQuestionIndex) || {};
83
- newAnswers.set(currentQuestionIndex, {
84
- ...existing,
85
- selectedOption: label,
86
- });
87
- return newAnswers;
88
- });
89
- // Clear elaborate mark when selecting a regular option (single-select behavior)
90
- setElaborateMarks((prev) => {
91
- if (prev.has(currentQuestionIndex)) {
92
- const newMarks = new Map(prev);
93
- newMarks.delete(currentQuestionIndex);
94
- return newMarks;
84
+ const existingAnswer = newAnswers.get(currentQuestionIndex) || {};
85
+ if (isDeselecting) {
86
+ newAnswers.set(currentQuestionIndex, {
87
+ ...existingAnswer,
88
+ selectedOption: undefined,
89
+ });
95
90
  }
96
- return prev;
91
+ else {
92
+ newAnswers.set(currentQuestionIndex, {
93
+ ...existingAnswer,
94
+ selectedOption: label,
95
+ customText: undefined,
96
+ });
97
+ }
98
+ return newAnswers;
97
99
  });
100
+ // Clear elaborate mark only when SELECTING (not deselecting)
101
+ if (!isDeselecting) {
102
+ setElaborateMarks((prev) => {
103
+ if (prev.has(currentQuestionIndex)) {
104
+ const newMarks = new Map(prev);
105
+ newMarks.delete(currentQuestionIndex);
106
+ return newMarks;
107
+ }
108
+ return prev;
109
+ });
110
+ }
98
111
  };
99
112
  const handleToggleOption = (label) => {
100
113
  setAnswers((prev) => {
@@ -125,13 +138,17 @@ export const StepperView = ({ onComplete, onProgress, hasMultipleSessions, initi
125
138
  }
126
139
  };
127
140
  // Handle custom answer text
141
+ // Handle custom answer text
128
142
  const handleChangeCustomAnswer = (text) => {
129
143
  setAnswers((prev) => {
130
144
  const newAnswers = new Map(prev);
131
145
  const existing = newAnswers.get(currentQuestionIndex) || {};
146
+ const question = sessionRequest.questions[currentQuestionIndex];
132
147
  newAnswers.set(currentQuestionIndex, {
133
148
  ...existing,
134
149
  customText: text,
150
+ // Single-choice: clear selectedOption when typing custom text
151
+ ...(text.trim().length > 0 && !question?.multiSelect ? { selectedOption: undefined } : {}),
135
152
  });
136
153
  return newAnswers;
137
154
  });
@@ -331,14 +348,16 @@ export const StepperView = ({ onComplete, onProgress, hasMultipleSessions, initi
331
348
  return newMarks;
332
349
  });
333
350
  // In single-select mode, clear selected option when marking elaborate
351
+ // In single-select mode, clear selected option and customText when marking elaborate
334
352
  if (isMarking && !currentQuestion.multiSelect) {
335
353
  setAnswers((prev) => {
336
354
  const existing = prev.get(currentQuestionIndex);
337
- if (existing?.selectedOption) {
355
+ if (existing?.selectedOption || existing?.customText) {
338
356
  const newAnswers = new Map(prev);
339
357
  newAnswers.set(currentQuestionIndex, {
340
358
  ...existing,
341
359
  selectedOption: undefined,
360
+ customText: undefined,
342
361
  });
343
362
  return newAnswers;
344
363
  }
@@ -42,8 +42,8 @@ describe("Footer keybinding labels", () => {
42
42
  const output = getOutput(instance.lastFrame());
43
43
  expect(output).toContain(KEY_LABELS.NAVIGATE_OPTIONS); // "↑↓"
44
44
  expect(output).toContain(KEY_LABELS.CURSOR); // "←→"
45
- expect(output).toContain(KEY_LABELS.NAVIGATE_QUESTIONS_TAB); // "Tab/S+Tab"
46
- expect(output).toContain(KEY_LABELS.NEWLINE); // "Enter"
45
+ expect(output).toContain(KEY_LABELS.NEWLINE); // "Shift+Enter"
46
+ expect(output).toContain(KEY_LABELS.ADVANCE_INPUT); // "Enter"
47
47
  expect(output).toContain(KEY_LABELS.REJECT); // "Esc"
48
48
  });
49
49
  it("elaborate-input context shows correct keybindings", () => {
@@ -51,7 +51,8 @@ describe("Footer keybinding labels", () => {
51
51
  const output = getOutput(instance.lastFrame());
52
52
  expect(output).toContain(KEY_LABELS.NAVIGATE_OPTIONS); // "↑↓"
53
53
  expect(output).toContain(KEY_LABELS.CURSOR); // "←→"
54
- expect(output).toContain("Enter/Tab");
54
+ expect(output).toContain(KEY_LABELS.NEWLINE); // "Shift+Enter"
55
+ expect(output).toContain(KEY_LABELS.ADVANCE_INPUT); // "Enter"
55
56
  expect(output).toContain(KEY_LABELS.REJECT); // "Esc"
56
57
  });
57
58
  it("review screen shows Submit and Back labels", () => {
@@ -35,7 +35,8 @@ export const KEY_LABELS = {
35
35
  SELECT_NEXT: "Enter",
36
36
  NEXT: "Enter",
37
37
  CURSOR: "←→",
38
- NEWLINE: "Enter",
38
+ NEWLINE: "Shift+Enter",
39
+ ADVANCE_INPUT: "Enter",
39
40
  REJECT: "Esc",
40
41
  BACK: "n",
41
42
  SUBMIT: "Enter",
@@ -33,7 +33,8 @@ export const Footer = ({ focusContext, multiSelect, isReviewScreen = false, show
33
33
  return [
34
34
  { key: KEY_LABELS.NAVIGATE_OPTIONS, action: t("footer.options") },
35
35
  { key: KEY_LABELS.CURSOR, action: t("footer.cursor") },
36
- { key: "Enter/Tab", action: t("footer.next") },
36
+ { key: KEY_LABELS.NEWLINE, action: t("footer.newline") },
37
+ { key: KEY_LABELS.ADVANCE_INPUT, action: t("footer.next") },
37
38
  { key: KEY_LABELS.REJECT, action: t("footer.reject") },
38
39
  ];
39
40
  }
@@ -42,7 +43,8 @@ export const Footer = ({ focusContext, multiSelect, isReviewScreen = false, show
42
43
  return [
43
44
  { key: KEY_LABELS.NAVIGATE_OPTIONS, action: t("footer.options") },
44
45
  { key: KEY_LABELS.CURSOR, action: t("footer.cursor") },
45
- { key: "Enter/Tab", action: t("footer.next") },
46
+ { key: KEY_LABELS.NEWLINE, action: t("footer.newline") },
47
+ { key: KEY_LABELS.ADVANCE_INPUT, action: t("footer.next") },
46
48
  { key: KEY_LABELS.REJECT, action: t("footer.reject") },
47
49
  ];
48
50
  }
@@ -82,7 +82,7 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
82
82
  setFocusedIndex(newIndex);
83
83
  return;
84
84
  }
85
- // When custom input is focused, only handle escape and tab to exit
85
+ // When custom input is focused, handle all keyboard input here
86
86
  if (isCustomInputFocused) {
87
87
  if (key.name === "escape") {
88
88
  setFocusedIndex(Math.max(0, options.length - 1));
@@ -90,9 +90,32 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
90
90
  else if (key.name === "tab" && !key.shift) {
91
91
  onAdvance?.();
92
92
  }
93
+ else if (key.name === "return") {
94
+ if (key.shift) {
95
+ // Shift+Enter: insert newline
96
+ onCustomChange?.(customValue + "\n");
97
+ }
98
+ else {
99
+ // Enter: advance to next question
100
+ onAdvance?.();
101
+ }
102
+ }
103
+ else if (key.name === "backspace" || key.name === "delete") {
104
+ if (customValue.length > 0) {
105
+ onCustomChange?.(customValue.slice(0, -1));
106
+ }
107
+ }
108
+ else if (key.sequence && !key.ctrl && !key.meta && key.name !== "up" && key.name !== "down") {
109
+ const sanitized = key.sequence
110
+ .replace(/\x1b?\[<[\d;]*[Mm]/g, '')
111
+ .replace(/\[?[OI]/g, '');
112
+ if (sanitized.length > 0) {
113
+ onCustomChange?.(customValue + sanitized);
114
+ }
115
+ }
93
116
  return;
94
117
  }
95
- // When elaborate input is focused, only handle escape and tab to exit
118
+ // When elaborate input is focused, handle all keyboard input here
96
119
  if (isElaborateFocused) {
97
120
  if (key.name === "escape") {
98
121
  setFocusedIndex(customInputIndex);
@@ -100,6 +123,32 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
100
123
  else if (key.name === "tab" && !key.shift) {
101
124
  onAdvance?.();
102
125
  }
126
+ else if (key.name === "return") {
127
+ if (key.shift) {
128
+ // Shift+Enter: insert newline
129
+ onElaborateTextChange?.(elaborateText + "\n");
130
+ }
131
+ else {
132
+ // Enter: advance to next question
133
+ if (!elaborateText.trim()) {
134
+ onElaborateSelect?.();
135
+ }
136
+ onAdvance?.();
137
+ }
138
+ }
139
+ else if (key.name === "backspace" || key.name === "delete") {
140
+ if (elaborateText.length > 0) {
141
+ onElaborateTextChange?.(elaborateText.slice(0, -1));
142
+ }
143
+ }
144
+ else if (key.sequence && !key.ctrl && !key.meta && key.name !== "up" && key.name !== "down") {
145
+ const sanitized = key.sequence
146
+ .replace(/\x1b?\[<[\d;]*[Mm]/g, '')
147
+ .replace(/\[?[OI]/g, '');
148
+ if (sanitized.length > 0) {
149
+ onElaborateTextChange?.(elaborateText + sanitized);
150
+ }
151
+ }
103
152
  return;
104
153
  }
105
154
  // Spacebar: Select/toggle WITHOUT advancing (works for both modes)
@@ -109,6 +158,8 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
109
158
  onToggle?.(options[focusedIndex].label);
110
159
  }
111
160
  else {
161
+ // Single-select: toggle (deselect if already selected)
162
+ // Parent handleSelectOption handles the toggle logic
112
163
  onSelect(options[focusedIndex].label);
113
164
  }
114
165
  }
@@ -203,15 +254,7 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
203
254
  marginTop: 0,
204
255
  paddingX: 1,
205
256
  paddingY: 0,
206
- }, children: _jsx("input", { placeholder: t("input.placeholder"), value: customValue, focused: true, onInput: (val) => {
207
- // Filter SGR mouse escape sequences that leak through stdin parser
208
- const sanitized = val
209
- .replace(/\x1b?\[<[\d;]*[Mm]/g, '')
210
- .replace(/\[?[OI]/g, '');
211
- if (sanitized !== val && sanitized.length === 0)
212
- return;
213
- onCustomChange?.(sanitized);
214
- }, onSubmit: () => onAdvance?.() }) })), !isCustomInputFocused && customValue && (_jsx("box", { style: { marginLeft: 2, marginTop: 0 }, children: _jsxs("text", { style: { fg: theme.components.options.hint, attributes: TextAttributes.DIM }, children: [" ", customLines.slice(0, 3).join("\n "), customLines.length > 3 ? "\n \u2026" : ""] }) }))] }) })), showCustomInput && (_jsx("box", { style: { marginTop: 0 }, children: _jsxs("box", { style: { flexDirection: "column" }, children: [(() => {
257
+ }, children: customValue ? (_jsx("text", { style: { fg: theme.components.options.focused }, children: customValue + "▌" })) : (_jsx("text", { style: { fg: theme.components.options.hint, attributes: TextAttributes.DIM }, children: t("input.placeholder") + "▌" })) })), !isCustomInputFocused && customValue && (_jsx("box", { style: { marginLeft: 2, marginTop: 0 }, children: _jsxs("text", { style: { fg: theme.components.options.hint, attributes: TextAttributes.DIM }, children: [" ", customLines.slice(0, 3).join("\n "), customLines.length > 3 ? "\n \u2026" : ""] }) }))] }) })), showCustomInput && (_jsx("box", { style: { marginTop: 0 }, children: _jsxs("box", { style: { flexDirection: "column" }, children: [(() => {
215
258
  const rowBg = isElaborateFocused
216
259
  ? theme.components.options.focusedBg
217
260
  : isElaborateMarked
@@ -240,19 +283,5 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
240
283
  marginTop: 0,
241
284
  paddingX: 1,
242
285
  paddingY: 0,
243
- }, children: _jsx("input", { placeholder: t("input.elaboratePlaceholder"), value: elaborateText, focused: true, onInput: (val) => {
244
- // Filter SGR mouse escape sequences that leak through stdin parser
245
- const sanitized = val
246
- .replace(/\x1b?\[<[\d;]*[Mm]/g, '')
247
- .replace(/\[?[OI]/g, '');
248
- if (sanitized !== val && sanitized.length === 0)
249
- return;
250
- onElaborateTextChange?.(sanitized);
251
- }, onSubmit: () => {
252
- // Enter submits and advance
253
- if (!elaborateText.trim()) {
254
- onElaborateSelect?.();
255
- }
256
- onAdvance?.();
257
- } }) })), !isElaborateFocused && elaborateText && (_jsx("box", { style: { marginLeft: 2, marginTop: 0 }, children: _jsxs("text", { style: { fg: theme.components.options.hint, attributes: TextAttributes.DIM }, children: [" ", elaborateLines.slice(0, 3).join("\n "), elaborateLines.length > 3 ? "\n \u2026" : ""] }) }))] }) }))] }));
286
+ }, children: elaborateText ? (_jsx("text", { style: { fg: theme.components.options.focused }, children: elaborateText + "▌" })) : (_jsx("text", { style: { fg: theme.components.options.hint, attributes: TextAttributes.DIM }, children: t("input.elaboratePlaceholder") + "▌" })) })), !isElaborateFocused && elaborateText && (_jsx("box", { style: { marginLeft: 2, marginTop: 0 }, children: _jsxs("text", { style: { fg: theme.components.options.hint, attributes: TextAttributes.DIM }, children: [" ", elaborateLines.slice(0, 3).join("\n "), elaborateLines.length > 3 ? "\n \u2026" : ""] }) }))] }) }))] }));
258
287
  };
@@ -73,24 +73,37 @@ export const StepperView = ({ onComplete, onProgress, hasMultipleSessions, initi
73
73
  }, [currentQuestionIndex]);
74
74
  // Handle option selection (single-select mode)
75
75
  const handleSelectOption = (label) => {
76
+ const existing = answers.get(currentQuestionIndex) || {};
77
+ const isDeselecting = existing.selectedOption === label;
76
78
  setAnswers((prev) => {
77
79
  const newAnswers = new Map(prev);
78
- const existing = newAnswers.get(currentQuestionIndex) || {};
79
- newAnswers.set(currentQuestionIndex, {
80
- ...existing,
81
- selectedOption: label,
82
- });
83
- return newAnswers;
84
- });
85
- // Clear elaborate mark when selecting a regular option (single-select behavior)
86
- setElaborateMarks((prev) => {
87
- if (prev.has(currentQuestionIndex)) {
88
- const newMarks = new Map(prev);
89
- newMarks.delete(currentQuestionIndex);
90
- return newMarks;
80
+ const existingAnswer = newAnswers.get(currentQuestionIndex) || {};
81
+ if (isDeselecting) {
82
+ newAnswers.set(currentQuestionIndex, {
83
+ ...existingAnswer,
84
+ selectedOption: undefined,
85
+ });
86
+ }
87
+ else {
88
+ newAnswers.set(currentQuestionIndex, {
89
+ ...existingAnswer,
90
+ selectedOption: label,
91
+ customText: undefined,
92
+ });
91
93
  }
92
- return prev;
94
+ return newAnswers;
93
95
  });
96
+ // Clear elaborate mark only when SELECTING (not deselecting) a regular option
97
+ if (!isDeselecting) {
98
+ setElaborateMarks((prev) => {
99
+ if (prev.has(currentQuestionIndex)) {
100
+ const newMarks = new Map(prev);
101
+ newMarks.delete(currentQuestionIndex);
102
+ return newMarks;
103
+ }
104
+ return prev;
105
+ });
106
+ }
94
107
  };
95
108
  const handleToggleOption = (label) => {
96
109
  setAnswers((prev) => {
@@ -123,9 +136,12 @@ export const StepperView = ({ onComplete, onProgress, hasMultipleSessions, initi
123
136
  setAnswers((prev) => {
124
137
  const newAnswers = new Map(prev);
125
138
  const existing = newAnswers.get(currentQuestionIndex) || {};
139
+ const question = sessionRequest.questions[currentQuestionIndex];
126
140
  newAnswers.set(currentQuestionIndex, {
127
141
  ...existing,
128
142
  customText: text,
143
+ // Single-choice: clear selectedOption when typing custom text
144
+ ...(text.trim().length > 0 && !question?.multiSelect ? { selectedOption: undefined } : {}),
129
145
  });
130
146
  return newAnswers;
131
147
  });
@@ -327,11 +343,12 @@ export const StepperView = ({ onComplete, onProgress, hasMultipleSessions, initi
327
343
  if (isMarking && !currentQuestion.multiSelect) {
328
344
  setAnswers((prev) => {
329
345
  const existing = prev.get(currentQuestionIndex);
330
- if (existing?.selectedOption) {
346
+ if (existing?.selectedOption || existing?.customText) {
331
347
  const newAnswers = new Map(prev);
332
348
  newAnswers.set(currentQuestionIndex, {
333
349
  ...existing,
334
350
  selectedOption: undefined,
351
+ customText: undefined,
335
352
  });
336
353
  return newAnswers;
337
354
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auq-mcp-server",
3
- "version": "3.2.5",
3
+ "version": "3.2.6",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "auq": "bin/auq"