@zhushanwen/pi-ask-user 2.0.1 → 4.0.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-ask-user",
3
- "version": "2.0.1",
4
- "description": "Inline adaptive ask_user tool for Pi — single/multi-question structured input with split-pane preview, inline editor, and optional comments.",
3
+ "version": "4.0.0",
4
+ "description": "Inline adaptive ask_user tool for Pi — single/multi-question structured input with split-pane preview and inline editor.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "pi": {
@@ -26,7 +26,7 @@
26
26
  "ARCHITECTURE.md"
27
27
  ],
28
28
  "dependencies": {
29
- "@xyz-agent/extension-protocol": "^0.2.0"
29
+ "@xyz-agent/extension-protocol": "^0.3.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@earendil-works/pi-tui": "*",
@@ -38,7 +38,7 @@
38
38
  "@earendil-works/pi-coding-agent": "*",
39
39
  "@earendil-works/pi-tui": "*",
40
40
  "typebox": "*",
41
- "@zhushanwen/pi-subagent-workflow": "2.0.1"
41
+ "@zhushanwen/pi-subagent-workflow": "4.0.0"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "@earendil-works/pi-tui": {
@@ -4,23 +4,16 @@
4
4
  // 覆盖审查 S5 发现的覆盖盲区:
5
5
  // - parseAnswerParts 子串误匹配("A" 不应命中 "AB")
6
6
  // - formatAnswer 空 parts → null
7
- // - comment 分隔符边界
8
7
 
9
8
  import { describe, expect, it } from "vitest";
10
9
 
11
10
  import { formatAnswer, parseAnswerParts } from "../answer-format.js";
12
- import { ANSWER_COMMENT_SEPARATOR } from "../types.js";
13
11
 
14
12
  describe("formatAnswer", () => {
15
13
  it("returns null for empty parts (unanswered)", () => {
16
14
  expect(formatAnswer([])).toBeNull();
17
15
  });
18
16
 
19
- it("returns null for empty parts even with comment", () => {
20
- // parts 空 = 没有选中选项,即使有 comment 也不应产出有效答案行
21
- expect(formatAnswer([], "some comment")).toBeNull();
22
- });
23
-
24
17
  it("joins single part without separator", () => {
25
18
  expect(formatAnswer(["yes"])).toBe("yes");
26
19
  });
@@ -28,15 +21,6 @@ describe("formatAnswer", () => {
28
21
  it("joins multiple parts with ', '", () => {
29
22
  expect(formatAnswer(["A", "B", "C"])).toBe("A, B, C");
30
23
  });
31
-
32
- it("appends comment with ANSWER_COMMENT_SEPARATOR", () => {
33
- const result = formatAnswer(["A", "B"], "my comment");
34
- expect(result).toBe(`A, B${ANSWER_COMMENT_SEPARATOR}my comment`);
35
- });
36
-
37
- it("handles null comment (no separator appended)", () => {
38
- expect(formatAnswer(["A"], null)).toBe("A");
39
- });
40
24
  });
41
25
 
42
26
  describe("parseAnswerParts", () => {
@@ -44,7 +28,6 @@ describe("parseAnswerParts", () => {
44
28
  const labels = ["yes", "no", "maybe"];
45
29
  const result = parseAnswerParts("yes, no", labels);
46
30
  expect(result.selected).toEqual(["yes", "no"]);
47
- expect(result.comment).toBeUndefined();
48
31
  });
49
32
 
50
33
  // S5 核心:防子串误匹配——"A" 不应命中 label "AB"
@@ -69,39 +52,21 @@ describe("parseAnswerParts", () => {
69
52
  expect(result.selected).toEqual(["C", "A"]);
70
53
  });
71
54
 
72
- it("extracts comment after ANSWER_COMMENT_SEPARATOR", () => {
73
- const labels = ["yes"];
74
- const answer = `yes${ANSWER_COMMENT_SEPARATOR}because reasons`;
75
- const result = parseAnswerParts(answer, labels);
76
- expect(result.selected).toEqual(["yes"]);
77
- expect(result.comment).toBe("because reasons");
78
- });
79
-
80
55
  it("handles full-width comma (,) as separator", () => {
81
56
  const labels = ["A", "B"];
82
57
  const result = parseAnswerParts("A,B", labels);
83
58
  expect(result.selected).toEqual(["A", "B"]);
84
59
  });
85
60
 
86
- it("returns non-matching tokens as neither selected nor comment (Other free text)", () => {
61
+ it("returns non-matching tokens as neither selected (Other free text)", () => {
87
62
  const labels = ["yes", "no"];
88
63
  // "custom text" 不匹配任何 label → 是 Other 自由文本
89
64
  const result = parseAnswerParts("custom text", labels);
90
65
  expect(result.selected).toEqual([]);
91
- expect(result.comment).toBeUndefined();
92
66
  });
93
67
 
94
68
  it("handles empty answer string", () => {
95
69
  const result = parseAnswerParts("", ["A", "B"]);
96
70
  expect(result.selected).toEqual([]);
97
- expect(result.comment).toBeUndefined();
98
- });
99
-
100
- it("handles answer with only comment (no selected labels)", () => {
101
- const labels = ["A"];
102
- const answer = `${ANSWER_COMMENT_SEPARATOR}just a comment`;
103
- const result = parseAnswerParts(answer, labels);
104
- expect(result.selected).toEqual([]);
105
- expect(result.comment).toBe("just a comment");
106
71
  });
107
72
  });
@@ -6,7 +6,7 @@
6
6
  // - RPC 路径(ctx.mode === 'rpc'):转发器——handler 内部调 askUserInteract(select 通道),
7
7
  // 把 proto answers JSON.stringify 成 {value} 返回,子进程 JSON.parse(value) 正确 decode。
8
8
  // - TUI 路径(ctx.mode === 'tui'):handler 走 ctx.ui.custom(mock 成返回预设 Result),
9
- // 验证内部 Result → proto AskUserAnswers 重新编码(single/multi/Other/comment 四种答案形态)。
9
+ // 验证内部 Result → proto AskUserAnswers 重新编码(single/multi/Other 三种答案形态)。
10
10
  // - 取消(askUserInteract/custom 返回 null 或 cancelled)→ {cancelled: true}
11
11
  // - 输入校验(channelPayload 缺失/无 questions)→ {cancelled: true}
12
12
  import type { AskUserQuestion } from "@xyz-agent/extension-protocol";
@@ -75,12 +75,6 @@ const multiProto: AskUserQuestion = {
75
75
  ],
76
76
  };
77
77
 
78
- const commentProto: AskUserQuestion = {
79
- question: "Which DB?",
80
- allowComment: true,
81
- options: [{ label: "Postgres", value: "Postgres" }, { label: "SQLite", value: "SQLite" }],
82
- };
83
-
84
78
  // ── Tests ───────────────────────────────────────────────
85
79
 
86
80
  describe("createAskUserChannelHandler", () => {
@@ -103,19 +97,6 @@ describe("createAskUserChannelHandler", () => {
103
97
  expect(resp).toEqual({ value: JSON.stringify({ Tools: JSON.stringify(["A", "C"]) }) });
104
98
  });
105
99
 
106
- it("RPC: Other + comment proto answers → 透传", async () => {
107
- const protoAnswers = {
108
- "Which DB?": "Postgres",
109
- "Which DB?__other": "Custom DB",
110
- "Which DB?__comment": "prod constraint",
111
- };
112
- const handler = createAskUserChannelHandler(
113
- makeCtx({ mode: "rpc", selectResult: JSON.stringify(protoAnswers) }) as never,
114
- );
115
- const resp = await handler({ channelPayload: { questions: [commentProto] } });
116
- expect(resp).toEqual({ value: JSON.stringify(protoAnswers) });
117
- });
118
-
119
100
  it("RPC: user cancel (select undefined) → {cancelled: true}", async () => {
120
101
  const handler = createAskUserChannelHandler(
121
102
  makeCtx({ mode: "rpc", selectResult: undefined }) as never,
@@ -217,21 +198,6 @@ describe("createAskUserChannelHandler", () => {
217
198
  });
218
199
  });
219
200
 
220
- it("TUI: comment → ${key}__comment", async () => {
221
- const internalResult: Result = {
222
- questions: [],
223
- answers: { "Which DB?": "Postgres — prod constraint" },
224
- cancelled: false,
225
- };
226
- const handler = createAskUserChannelHandler(
227
- makeCtx({ mode: "tui", customResult: internalResult }) as never,
228
- );
229
- const resp = await handler({ channelPayload: { questions: [commentProto] } });
230
- expect(resp).toEqual({
231
- value: JSON.stringify({ "Which DB?": "Postgres", "Which DB?__comment": "prod constraint" }),
232
- });
233
- });
234
-
235
201
  it("TUI: user cancel (custom returns null) → {cancelled: true}", async () => {
236
202
  const handler = createAskUserChannelHandler(
237
203
  makeCtx({ mode: "tui", customResult: null }) as never,
@@ -30,7 +30,6 @@ import {
30
30
  INSERT,
31
31
  LEFT,
32
32
  make,
33
- multiQWithComment,
34
33
  OSC_BEL,
35
34
  OSC_ST,
36
35
  PAGE_DOWN,
@@ -51,14 +50,6 @@ import {
51
50
  UP,
52
51
  } from "./fixtures";
53
52
 
54
- /** Helper: 打开 comment 编辑器并返回 component(多问题避免单问题 auto-submit) */
55
- function openComment(): AskUserComponent {
56
- const { c } = make(multiQWithComment);
57
- // Q1 (allowComment): select A → enters comment mode
58
- c.handleInput(ENTER);
59
- return c;
60
- }
61
-
62
53
  /** Helper: 打开 freeform 编辑器并返回 component(不渲染,避免缓存) */
63
54
  function openFreeform(q: Question[]): AskUserComponent {
64
55
  const { c } = make(q);
@@ -264,25 +255,6 @@ describe("AskUserComponent — key leak fix (C-ARROW / C-KEYMAP)", () => {
264
255
  expect(editorLine).not.toContain("abc");
265
256
  });
266
257
 
267
- // ── C-KEYMAP-COMMENT-UP: arrow key no-op in comment editor too ──
268
- it("C-KEYMAP-COMMENT-UP: arrow keys are no-op in comment editor", () => {
269
- const c = openComment();
270
- c.handleInput("a");
271
- c.handleInput(UP);
272
- c.handleInput(RIGHT);
273
- c.handleInput("b");
274
- // comment editor renders text and cursor on separate lines;
275
- // check the text line (before cursor) contains "ab"
276
- const lines = c.render(60);
277
- const textLine = lines.find((l) => l.includes("ab"));
278
- expect(textLine).toBeDefined();
279
- expect(textLine).toContain("ab");
280
- // Ensure no leaked bracket characters from arrow escape sequences
281
- const allText = lines.join("\n");
282
- expect(allText).not.toMatch(/\[A/); // no leaked UP sequence
283
- expect(allText).not.toMatch(/\[C/); // no leaked RIGHT sequence
284
- });
285
-
286
258
  // ── C-KEYMAP-MOD: modifier key combinations matrix (18 cases) ──
287
259
  const modifierCases: Array<{ name: string; seq: string }> = [
288
260
  { name: "ctrl+up", seq: CTRL_UP },
@@ -429,18 +401,6 @@ describe("AskUserComponent — unknown control sequence leak fix (C-CSI)", () =>
429
401
  expect(editorLine).not.toContain("\x1b[A"); expect(editorLine).not.toContain("\x1b[B"); expect(editorLine).not.toContain("\x1b[C"); expect(editorLine).not.toContain("\x1b[D");
430
402
  });
431
403
 
432
- it("C-CSI-10: unknown CSI does not leak in comment editor", () => {
433
- const c = openComment();
434
- c.handleInput(UNKNOWN_CSI);
435
- c.handleInput("ab");
436
- const lines = c.render(60);
437
- const textLine = lines.find((l) => l.includes("ab"));
438
- expect(textLine).toBeDefined();
439
- expect(textLine).toContain("ab");
440
- const allText = lines.join("\n");
441
- expect(allText).not.toMatch(/\[9/);
442
- });
443
-
444
404
  it("C-CSI-R1: plain text still appended correctly", () => {
445
405
  const c = openFreeform([singleQ]);
446
406
  c.handleInput("hello");
@@ -11,11 +11,9 @@ import {
11
11
  LEFT,
12
12
  mockTui,
13
13
  multiQ,
14
- multiQWithComment,
15
14
  RIGHT,
16
15
  singleQ,
17
16
  singleQMulti,
18
- singleQWithComment,
19
17
  stubTheme,
20
18
  TAB,
21
19
  UP,
@@ -163,24 +161,20 @@ describe("AskUserComponent — multi question tab nav", () => {
163
161
  const { c, result } = make([singleQMulti]);
164
162
  // singleQMulti: [Auth, Search],光标初始在 Auth(0)
165
163
  c.handleInput(DOWN); // 光标移到 Search(1),未 toggle
166
- c.handleInput(ENTER); // Enter 应同时选中 Search + confirm + allowComment comment
167
- // 断言进入了评论模式(说明 Enter 确认了,而非 no-op)
168
- const lines = c.render(60);
169
- expect(lines.some((l) => l.toLowerCase().includes("comment"))).toBe(true);
170
- c.handleInput(ENTER); // 跳过评论 → submit(单问题)
164
+ c.handleInput(ENTER); // Enter 应同时选中 Search + confirm → submit(单问题)
171
165
  expect(result.val!.answers["Which features?"]).toBe("Search");
172
166
  });
173
167
 
174
- it("C-S3: auto-confirm(←/→ 切 tab)跳过评论输入行,仅 Enter 路径才进评论", () => {
175
- // S-3 锁定:allowComment 的问题,←/→ 切走只 auto-confirm,不进评论模式
168
+ it("C-S3: auto-confirm(←/→ 切 tab)已答问题自动确认", () => {
169
+ // S-3 锁定:←/→ 切走时已答问题 auto-confirm
176
170
  const twoQMulti: Question[] = [
177
- { question: "Q1", header: "First", options: [{ label: "A" }, { label: "B" }], multiSelect: true, allowComment: true },
171
+ { question: "Q1", header: "First", options: [{ label: "A" }, { label: "B" }], multiSelect: true },
178
172
  { question: "Q2", header: "Second", options: [{ label: "X" }, { label: "Y" }] },
179
173
  ];
180
174
  const { c, result } = make(twoQMulti);
181
175
  c.handleInput(" "); // Q1 toggle A
182
- c.handleInput(RIGHT); // → Q2,auto-confirm Q1,不进评论
183
- // 验证:当前在 Q2(非 Q1 的评论模式)。Q2 选 X → Submit
176
+ c.handleInput(RIGHT); // → Q2,auto-confirm Q1
177
+ // 验证:当前在 Q2Q2 选 X → Submit
184
178
  c.handleInput(ENTER); // Q2 select X → Submit
185
179
  c.handleInput(ENTER); // Submit
186
180
  expect(result.val!.answers["Q1"]).toBe("A"); // auto-confirm 生效
@@ -268,13 +262,14 @@ describe("AskUserComponent — multi-select toggle", () => {
268
262
  expect(lines.some((l) => l.includes("[ ]") && l.includes("Auth"))).toBe(true);
269
263
  });
270
264
 
271
- it("C-24 (AC-18): multi-select toggle does NOT trigger comment mode", () => {
272
- // singleQMulti has allowComment:true + multiSelect:true
265
+ it("C-24 (AC-18): multi-select toggle stays in options mode (no editor)", () => {
273
266
  const { c } = make([singleQMulti]);
274
- c.handleInput(" "); // toggle — should NOT enter comment mode
267
+ c.handleInput(" "); // toggle — stays in options mode
275
268
  const lines = c.render(60);
276
- // No comment prompt shown
277
- expect(lines.some((l) => l.toLowerCase().includes("comment"))).toBe(false);
269
+ // 未进入编辑器模式(无反色光标行)
270
+ expect(lines.some((l) => l.includes("\x1b[7m"))).toBe(false);
271
+ // toggle 状态可见
272
+ expect(lines.some((l) => l.includes("[✓]") && l.includes("Auth"))).toBe(true);
278
273
  });
279
274
  });
280
275
 
@@ -472,108 +467,6 @@ describe("AskUserComponent — multi-char paste in editor", () => {
472
467
  });
473
468
  });
474
469
 
475
- // ── 5f. 评论流程(FR-4.6 / FR-11 / AC-6/12/17)─────────
476
- describe("AskUserComponent — comment flow", () => {
477
- it("C-33: single-select + allowComment Enter enters comment mode", () => {
478
- const { c } = make([singleQWithComment]);
479
- c.handleInput(ENTER); // select Postgres
480
- const lines = c.render(60);
481
- expect(lines.some((l) => l.toLowerCase().includes("comment"))).toBe(true);
482
- });
483
-
484
- it("C-34 (AC-12): comment Enter empty skips and submits (single)", () => {
485
- const { c, result } = make([singleQWithComment]);
486
- c.handleInput(ENTER); // select → comment mode
487
- c.handleInput(ENTER); // empty comment → skip → submit
488
- expect(result.val).not.toBeUndefined();
489
- expect(result.val!.answers["Which DB? (with comment)"]).toBe("Postgres");
490
- });
491
-
492
- it("C-35: comment Enter with text saves comment", () => {
493
- const { c, result } = make([singleQWithComment]);
494
- c.handleInput(ENTER); // select → comment mode
495
- c.handleInput("f");
496
- c.handleInput("a");
497
- c.handleInput("s");
498
- c.handleInput("t");
499
- c.handleInput(ENTER); // save comment → submit
500
- expect(result.val!.answers["Which DB? (with comment)"]).toBe("Postgres — fast");
501
- });
502
-
503
- it("C-38: multi-select + allowComment: Enter after toggle enters comment", () => {
504
- const { c } = make([singleQMulti]);
505
- c.handleInput(" "); // toggle Auth
506
- c.handleInput(ENTER); // confirm → comment mode
507
- const lines = c.render(60);
508
- expect(lines.some((l) => l.toLowerCase().includes("comment"))).toBe(true);
509
- });
510
-
511
- it("C-39: Other + allowComment: freeText then comment", () => {
512
- const { c, result } = make([singleQWithComment]);
513
- // Navigate to Other
514
- c.handleInput(DOWN);
515
- c.handleInput(DOWN);
516
- c.handleInput(ENTER); // open freeform
517
- c.handleInput("c");
518
- c.handleInput("u");
519
- c.handleInput("s");
520
- c.handleInput("t");
521
- c.handleInput("o");
522
- c.handleInput("m");
523
- c.handleInput(ENTER); // save freeText → allowComment → comment mode
524
- c.handleInput(ENTER); // empty comment → skip → submit
525
- expect(result.val!.answers["Which DB? (with comment)"]).toBe("custom");
526
- });
527
-
528
- it("C-36 (AC-17): comment Esc skips comment and advances (single)", () => {
529
- const { c, result } = make([singleQWithComment]);
530
- c.handleInput(ENTER); // select Postgres → comment mode
531
- c.handleInput(ESC); // Esc in comment = skip comment → advance → submit
532
- expect(result.val).not.toBeUndefined();
533
- // commentValue stays null (no prior comment), answer is the selected option
534
- expect(result.val!.answers["Which DB? (with comment)"]).toBe("Postgres");
535
- });
536
-
537
- it("C-36b (AC-17): comment Esc advances to next tab (multi-question)", () => {
538
- const { c, result } = make(multiQWithComment);
539
- // Q1 (allowComment): select A → comment mode
540
- c.handleInput(ENTER); // select A → comment mode
541
- c.handleInput(ESC); // Esc in comment = skip → advance to Q2
542
- // Q2: select X → Submit
543
- c.handleInput(ENTER); // select X → Submit
544
- c.handleInput(ENTER); // Submit
545
- expect(result.val).not.toBeUndefined();
546
- expect(result.val!.answers["Q1"]).toBe("A");
547
- expect(result.val!.answers["Q2"]).toBe("X");
548
- });
549
-
550
- it("C-36c (AC-17): Esc-in-comment discards typed text (vs Enter which saves)", () => {
551
- // Contrast: typing then Enter would save commentValue and append " — keep".
552
- // Esc should discard the typed editor text and advance without attaching it.
553
- const { c, result } = make([singleQWithComment]);
554
- c.handleInput(ENTER); // select Postgres → comment mode
555
- c.handleInput("k");
556
- c.handleInput("e");
557
- c.handleInput("e");
558
- c.handleInput("p");
559
- c.handleInput(ESC); // Esc in comment = discard typed text → advance → submit
560
- expect(result.val).not.toBeUndefined();
561
- // No " — keep" suffix: Esc did not commit the typed text
562
- expect(result.val!.answers["Which DB? (with comment)"]).toBe("Postgres");
563
- });
564
-
565
- it("C-37: answer + comment combined format 'label — note'", () => {
566
- const { c, result } = make([singleQWithComment]);
567
- c.handleInput(ENTER); // select Postgres → comment mode
568
- c.handleInput("n");
569
- c.handleInput("o");
570
- c.handleInput("t");
571
- c.handleInput("e");
572
- c.handleInput(ENTER); // save comment → submit
573
- expect(result.val!.answers["Which DB? (with comment)"]).toBe("Postgres — note");
574
- });
575
- });
576
-
577
470
  // ── 5g. 防重入(FR-12)─────────────────────────────────
578
471
  describe("AskUserComponent — re-entry guard", () => {
579
472
  it("C-40: ignores input after resolution (submit)", () => {
@@ -658,12 +551,12 @@ describe("AskUserComponent — Submit tab", () => {
658
551
  });
659
552
 
660
553
  it("C-46: Submit Enter when all confirmed submits", () => {
661
- const { c, result } = make(multiQWithComment);
662
- // Q1 (allowComment): select A comment mode skip
663
- c.handleInput(ENTER); // select A
664
- c.handleInput(ENTER); // skip comment → Q2
665
- // Q2: select X
666
- c.handleInput(ENTER); // → Submit
554
+ const { c, result } = make([
555
+ { question: "Q1", header: "First", options: [{ label: "A" }, { label: "B" }] },
556
+ { question: "Q2", header: "Second", options: [{ label: "X" }, { label: "Y" }] },
557
+ ]);
558
+ c.handleInput(ENTER); // Q1 select A → Q2
559
+ c.handleInput(ENTER); // Q2 select X → Submit
667
560
  c.handleInput(ENTER); // Submit
668
561
  expect(result.val).not.toBeUndefined();
669
562
  expect(result.val!.answers["Q1"]).toBe("A");
@@ -806,7 +699,7 @@ describe("AskUserComponent — confirm-checkmark, Esc-back, Tab browsing", () =>
806
699
  // ── 5l. 新行为:←/→ 不切 tab、Other Enter 切 freeform 原生、Submit tab focus ──
807
700
  describe("AskUserComponent — new behavior (post-refactor)", () => {
808
701
  it("C-NEW-1: multi-select Other + Enter opens freeform; Other row turns into [ ] <input>█ in-place", () => {
809
- // singleQMulti: [Auth, Search],多选 + allowComment
702
+ // singleQMulti: [Auth, Search],多选
810
703
  const { c, result } = make([singleQMulti]);
811
704
  // 1) Space toggle Auth
812
705
  c.handleInput(" ");
@@ -827,14 +720,13 @@ describe("AskUserComponent — new behavior (post-refactor)", () => {
827
720
  expect(lines.some((l) => l.includes("Search"))).toBe(true);
828
721
  // [✓] 标记的 Auth 仍存在(toggle 状态保留)
829
722
  expect(lines.some((l) => l.includes("[✓]") && l.includes("Auth"))).toBe(true);
830
- // 4) 输 "redis" → Enter 保存 → allowComment → comment mode
723
+ // 4) 输 "redis" → Enter 保存 → submit(单问题)
831
724
  c.handleInput("r");
832
725
  c.handleInput("e");
833
726
  c.handleInput("d");
834
727
  c.handleInput("i");
835
728
  c.handleInput("s");
836
- c.handleInput(ENTER); // 保存 freeText → comment mode
837
- c.handleInput(ENTER); // 跳过评论 → submit(单问题)
729
+ c.handleInput(ENTER); // 保存 freeText → submit(单问题)
838
730
  // 答案含多选 toggle 项 + Other 自定义
839
731
  expect(result.val!.answers["Which features?"]).toBe("Auth, redis");
840
732
  });
@@ -877,11 +769,12 @@ describe("AskUserComponent — new behavior (post-refactor)", () => {
877
769
  });
878
770
 
879
771
  it("C-NEW-4: Submit tab Enter on Submit focus (all confirmed) submits", () => {
880
- // multiQWithComment: Q1 allowComment, Q2 plain
881
- const { c, result } = make(multiQWithComment);
772
+ const { c, result } = make([
773
+ { question: "Q1", header: "First", options: [{ label: "A" }, { label: "B" }] },
774
+ { question: "Q2", header: "Second", options: [{ label: "X" }, { label: "Y" }] },
775
+ ]);
882
776
  // 答完 Q1 + Q2
883
- c.handleInput(ENTER); // Q1 select A → comment mode
884
- c.handleInput(ENTER); // skip comment → Q2
777
+ c.handleInput(ENTER); // Q1 select A → Q2
885
778
  c.handleInput(ENTER); // Q2 select X → Submit tab(Q2 是最后一个问题,advance 到 Submit)
886
779
  // 已经在 Submit tab,focus=Submit,按 Enter 提交
887
780
  c.handleInput(ENTER);
@@ -7,8 +7,8 @@ import { describe, expect, it } from "vitest";
7
7
 
8
8
  import { makeE2E } from "./e2e-harness";
9
9
 
10
- // ── E2E-1: 单问题无评论 — 选第二项提交 ─────────────────
11
- describe("E2E-1: single question, no comment — pick 2nd option", () => {
10
+ // ── E2E-1: 单问题 — 选第二项提交 ────────────────────
11
+ describe("E2E-1: single question — pick 2nd option", () => {
12
12
  const questions = [
13
13
  {
14
14
  question: "Which DB?",
@@ -32,53 +32,6 @@ describe("E2E-1: single question, no comment — pick 2nd option", () => {
32
32
  });
33
33
  });
34
34
 
35
- // ── E2E-2: 单问题 + allowComment — 选项 + 评论拼接 ─────
36
- describe("E2E-2: single question + allowComment — option + comment joined", () => {
37
- const questions = [
38
- {
39
- question: "Which DB?",
40
- allowComment: true,
41
- options: [{ label: "Postgres" }, { label: "SQLite" }],
42
- },
43
- ];
44
-
45
- it("joins selected option with comment via ' — '", async () => {
46
- const e = makeE2E(questions);
47
- // Enter 选 Postgres → 进评论模式 → 输 "fast" → Enter 保存(allowComment 分支)
48
- e.keys(["\r", "f", "a", "s", "t", "\r"]);
49
- const result = await e.getExecuted();
50
- const details = result.details;
51
-
52
- expect(details.cancelled).toBe(false);
53
- expect(details.answers["Which DB?"]).toBe("Postgres — fast");
54
- expect(result.content[0].text).toContain("Postgres — fast");
55
- });
56
- });
57
-
58
- // ── E2E-3: 单问题 + allowComment — Enter 空评论跳过 ─────
59
- describe("E2E-3: single question + allowComment — Enter in comment skips", () => {
60
- const questions = [
61
- {
62
- question: "Which DB?",
63
- allowComment: true,
64
- options: [{ label: "Postgres" }, { label: "SQLite" }],
65
- },
66
- ];
67
-
68
- it("empty Enter in comment mode keeps option without ' — ' suffix", async () => {
69
- const e = makeE2E(questions);
70
- // Enter 选 Postgres → 直接 Enter 评论模式跳过(AC-12)
71
- e.keys(["\r", "\r"]);
72
- const result = await e.getExecuted();
73
- const details = result.details;
74
-
75
- expect(details.cancelled).toBe(false);
76
- // 不含 " — " 分隔符
77
- expect(details.answers["Which DB?"]).toBe("Postgres");
78
- expect(details.answers["Which DB?"]).not.toContain("—");
79
- });
80
- });
81
-
82
35
  // ── E2E-4: 多问题提交 — 逐题选择后 Submit tab 提交(S-11)──────
83
36
  describe("E2E-4: multi-question submit — answer each then Submit tab", () => {
84
37
  const questions = [
@@ -42,7 +42,6 @@ export const singleQ: Question = {
42
42
 
43
43
  export const singleQWithComment: Question = {
44
44
  question: "Which DB? (with comment)",
45
- allowComment: true,
46
45
  options: [
47
46
  { label: "Postgres", description: "Battle-tested" },
48
47
  { label: "SQLite", description: "Embedded" },
@@ -52,7 +51,6 @@ export const singleQWithComment: Question = {
52
51
  export const singleQMulti: Question = {
53
52
  question: "Which features?",
54
53
  multiSelect: true,
55
- allowComment: true,
56
54
  options: [
57
55
  { label: "Auth", description: "OAuth + session" },
58
56
  { label: "Search", description: "Full-text" },
@@ -71,7 +69,7 @@ export const multiQ: Question[] = [
71
69
  ];
72
70
 
73
71
  export const multiQWithComment: Question[] = [
74
- { question: "Q1", header: "First", allowComment: true, options: [{ label: "A" }, { label: "B" }] },
72
+ { question: "Q1", header: "First", options: [{ label: "A" }, { label: "B" }] },
75
73
  { question: "Q2", header: "Second", options: [{ label: "X" }, { label: "Y" }] },
76
74
  ];
77
75
 
@@ -645,31 +645,6 @@ describe("execute — RPC mode (askUserInteract via select channel)", () => {
645
645
  expect(result.details.answers["Which DB?"]).toBe("Postgres, Custom DB");
646
646
  });
647
647
 
648
- it("R-4: comment → inlined with ' — ' separator", async () => {
649
- const tool = getTool();
650
- const withComment = {
651
- questions: [
652
- {
653
- question: "Which DB?",
654
- options: [{ label: "Postgres" }, { label: "SQLite" }],
655
- allowComment: true,
656
- },
657
- ],
658
- };
659
- const protoAnswers = JSON.stringify({
660
- "Which DB?": "Postgres",
661
- "Which DB?__comment": "prod constraint",
662
- });
663
- const result = await tool.execute(
664
- "id",
665
- withComment,
666
- undefined,
667
- undefined,
668
- makeCtx({ mode: "rpc", selectResult: protoAnswers }),
669
- );
670
- expect(result.details.answers["Which DB?"]).toBe("Postgres — prod constraint");
671
- });
672
-
673
648
  it("R-5: user cancel (select returns undefined) → cancelled details", async () => {
674
649
  const tool = getTool();
675
650
  const result = await tool.execute(
@@ -721,7 +696,7 @@ describe("execute — RPC mode (askUserInteract via select channel)", () => {
721
696
  expect(result.details.answers["Which database?"]).toBe("Postgres");
722
697
  });
723
698
 
724
- it("R-8: multi-question mixed (single-select + multi-select + Other + comment)", async () => {
699
+ it("R-8: multi-question mixed (single-select + multi-select + Other)", async () => {
725
700
  const tool = getTool();
726
701
  const mixed = {
727
702
  questions: [
@@ -740,7 +715,6 @@ describe("execute — RPC mode (askUserInteract via select channel)", () => {
740
715
  question: "Which region?",
741
716
  header: "Region",
742
717
  options: [{ label: "US" }, { label: "EU" }],
743
- allowComment: true,
744
718
  },
745
719
  ],
746
720
  };
@@ -769,40 +743,4 @@ describe("execute — RPC mode (askUserInteract via select channel)", () => {
769
743
  // Q3: 无选中 → 跳过(不在 answers map 中)
770
744
  expect(result.details.answers["Which region?"]).toBeUndefined();
771
745
  });
772
-
773
- it("R-9: multi-question with comment on one question", async () => {
774
- const tool = getTool();
775
- const multiQ = {
776
- questions: [
777
- {
778
- question: "Which DB?",
779
- header: "DB",
780
- options: [{ label: "Postgres" }],
781
- },
782
- {
783
- question: "Why?",
784
- header: "Reason",
785
- options: [{ label: "Performance" }],
786
- allowComment: true,
787
- },
788
- ],
789
- };
790
- const protoAnswers = JSON.stringify({
791
- DB: "Postgres",
792
- Reason: "Performance",
793
- "Reason__comment": "benchmarked",
794
- });
795
- const result = await tool.execute(
796
- "id",
797
- multiQ,
798
- undefined,
799
- undefined,
800
- makeCtx({ mode: "rpc", selectResult: protoAnswers }),
801
- );
802
-
803
- // Q1: 无 comment
804
- expect(result.details.answers["Which DB?"]).toBe("Postgres");
805
- // Q2: 有 comment → 内联
806
- expect(result.details.answers["Why?"]).toBe("Performance — benchmarked");
807
- });
808
746
  });
@@ -252,7 +252,7 @@ describe("renderQuestionView — Other editor mode", () => {
252
252
  });
253
253
 
254
254
  it("Q-28-WIDE: freeform 模式在宽终端下用全宽渲染(不被分屏左列压窄)", () => {
255
- // 回归:freeform/comment 模式忽略分屏,编辑器用全 width。
255
+ // 回归:freeform 模式忽略分屏,编辑器用全 width。
256
256
  // 修复前:宽终端走 split.left(≈40),Other 输入被压在左半屏换行频繁。
257
257
  const width = 100;
258
258
  // getSplitPaneWidths(100) 非 null(宽终端会进分屏分支),但 freeform 应绕过它
@@ -335,35 +335,6 @@ describe("renderQuestionView — Other editor mode", () => {
335
335
  });
336
336
  });
337
337
 
338
- // ── Q-18 ~ Q-19: 评论模式 ───────────────────────────────
339
- describe("renderQuestionView — comment mode", () => {
340
- it("Q-18: comment mode renders editor with note text", () => {
341
- const lines = rv(
342
- singleQ,
343
- makeState({ mode: "comment", selectedIndex: 0 }),
344
- stubTheme,
345
- 60,
346
- true,
347
- "my note",
348
- );
349
- const t = text(lines);
350
- expect(t.toLowerCase()).toContain("comment");
351
- expect(t).toContain("my note");
352
- });
353
-
354
- it("Q-19: comment prompt includes (optional)", () => {
355
- const lines = rv(
356
- singleQ,
357
- makeState({ mode: "comment", selectedIndex: 0 }),
358
- stubTheme,
359
- 60,
360
- true,
361
- "",
362
- );
363
- expect(text(lines)).toContain("(optional)");
364
- });
365
- });
366
-
367
338
  // ── Q-20 ~ Q-23: 帮助行 ─────────────────────────────────
368
339
  describe("renderQuestionView — help line", () => {
369
340
  it("Q-20: single-select help shows 'Enter select'", () => {
@@ -124,11 +124,6 @@ describe("getAnswerText", () => {
124
124
  expect(getAnswerText(q1, s)).toBe("custom");
125
125
  });
126
126
 
127
- it("S-9: comment appended with separator", () => {
128
- const s = makeState({ confirmed: true, selectedIndex: 0, commentValue: "fast" });
129
- expect(getAnswerText(q1, s)).toBe("Postgres — fast");
130
- });
131
-
132
127
  it("S-10: unconfirmed returns null", () => {
133
128
  const s = makeState({ confirmed: false });
134
129
  expect(getAnswerText(q1, s)).toBeNull();
@@ -148,21 +143,6 @@ describe("getAnswerText", () => {
148
143
  const sEmptyMulti = makeState({ confirmed: true, selectedIndices: new Set<number>() });
149
144
  expect(getAnswerText(multiQ, sEmptyMulti)).toBeNull();
150
145
  });
151
-
152
- it("S-9b: multi-select + comment combined", () => {
153
- const multiQ: Question = {
154
- question: "Features",
155
- multiSelect: true,
156
- allowComment: true,
157
- options: [{ label: "A" }, { label: "B" }],
158
- };
159
- const s = makeState({
160
- confirmed: true,
161
- selectedIndices: new Set([0, 1]),
162
- commentValue: "nice",
163
- });
164
- expect(getAnswerText(multiQ, s)).toBe("A, B — nice");
165
- });
166
146
  });
167
147
 
168
148
  // ── S-11 ~ S-12: buildResult ─────────────────────────────
@@ -40,7 +40,6 @@ describe("types", () => {
40
40
  expect(s.selectedIndices).toBeInstanceOf(Set);
41
41
  expect(s.confirmed).toBe(false);
42
42
  expect(s.freeTextValue).toBeNull();
43
- expect(s.commentValue).toBeNull();
44
43
  expect(s.mode).toBe("options");
45
44
  });
46
45
 
@@ -82,7 +82,7 @@ describe("validateInput", () => {
82
82
  });
83
83
 
84
84
  // V-10/S3: 多 question 时 header 必须唯一——重复 header 会导致 askUserKey 碰撞
85
- // (协议 helper 用 header 作 answers 读取 key,后一个覆盖前一个的 Other/comment
85
+ // (协议 helper 用 header 作 answers 读取 key,后一个覆盖前一个的 Other)
86
86
  it("rejects duplicate headers across different questions", () => {
87
87
  const result = validateInput([
88
88
  q({ question: "Q1", header: "Same" }),
@@ -13,7 +13,6 @@ import {
13
13
  multiQ,
14
14
  RIGHT,
15
15
  singleQ,
16
- singleQWithComment,
17
16
  stubTheme,
18
17
  UP,
19
18
  } from "./fixtures";
@@ -78,15 +77,6 @@ describe("W2 — draftText migration", () => {
78
77
  expect(editorLine1).toContain("aaa");
79
78
  expect(editorLine1).not.toContain("ccc");
80
79
  });
81
-
82
- it("C-BC4C: comment flow submits comment with answer", () => {
83
- const { c, result } = make([singleQWithComment]);
84
- c.handleInput(ENTER); // select A → comment mode
85
- c.handleInput("my note");
86
- c.handleInput(ENTER); // submit
87
- expect(result.val).toBeDefined();
88
- expect(result.val!.answers["Which DB? (with comment)"]).toBe("Postgres — my note");
89
- });
90
80
  });
91
81
 
92
82
  // ── hint line: append-only UX hint ──
@@ -102,16 +92,6 @@ describe("W2 — hint line", () => {
102
92
  expect(hintLine).toContain("Enter submit");
103
93
  expect(hintLine).toContain("Esc back");
104
94
  });
105
-
106
- it("C-HINT-2: comment editor hint contains all expected hints", () => {
107
- const { c } = make([singleQWithComment]);
108
- c.handleInput(ENTER);
109
- const lines = c.render(80);
110
- const hintLine = lines.find((l: string) => l.includes("move") && l.includes("Backspace deletes"));
111
- expect(hintLine).toBeDefined();
112
- expect(hintLine).toContain("Enter submit");
113
- expect(hintLine).toContain("Esc back");
114
- });
115
95
  });
116
96
 
117
97
  // ── freeDraft 隔离:丢弃的 freeform 草稿不污染答案、不触发 auto-confirm ──
@@ -1,5 +1,5 @@
1
1
  // src/__tests__/w3-regression.test.ts
2
- // W3: Forward regression — freeform/comment/bksp edge cases.
2
+ // W3: Forward regression — freeform/bksp edge cases.
3
3
  // No-op keymap coverage moved to component-keymap.test.ts (deduplicated).
4
4
  import { describe, expect, it } from "vitest";
5
5
 
@@ -14,7 +14,6 @@ import {
14
14
  LEFT,
15
15
  mockTui,
16
16
  multiQ,
17
- multiQWithComment,
18
17
  RIGHT,
19
18
  singleQ,
20
19
  stubTheme,
@@ -72,33 +71,6 @@ describe("W3 — freeform Enter clears selectedIndex (C-BC4B)", () => {
72
71
  });
73
72
  });
74
73
 
75
- // ── C-BC4C: comment edge cases ──
76
- // C-BC4C-REEDIT (initial comment submit) removed — duplicate of w2-draft-hint C-BC4C.
77
- describe("W3 — comment re-edit (C-BC4C-CLEAR)", () => {
78
- it("C-BC4C-CLEAR: Esc in comment mode skips comment, keeps existing commentValue", () => {
79
- const { c, result } = make(multiQWithComment);
80
- // Q1: select A → comment mode
81
- c.handleInput(ENTER);
82
- c.handleInput("my note");
83
- c.handleInput(ENTER); // submit comment → advance to Q2
84
- // Q2: select X
85
- c.handleInput(ENTER);
86
- c.handleInput(ENTER); // confirm Q2 → advance to submit tab
87
- // Go back to Q1
88
- c.handleInput(LEFT);
89
- // Q1 is already confirmed, re-select A to trigger comment again
90
- c.handleInput(ENTER); // re-select A → afterConfirm → comment mode
91
- c.handleInput(ESC); // skip comment
92
- // Submit
93
- c.handleInput(RIGHT);
94
- c.handleInput(RIGHT); // navigate to submit tab
95
- c.handleInput(ENTER);
96
- expect(result.val).toBeDefined();
97
- // Comment should still be "my note" (Esc preserved commentValue)
98
- expect(result.val!.answers["Q1"]).toBe("A — my note");
99
- });
100
- });
101
-
102
74
  // ── C-BKSP-EDGE: backspace at cursorIndex=0 ──
103
75
 
104
76
  describe("W3 — backspace at cursorIndex=0 (C-BKSP-EDGE)", () => {
@@ -1,19 +1,16 @@
1
1
  // src/answer-format.ts
2
2
  // 答案文本格式的唯一权威模块。
3
3
  // TUI 路径(submit-view.ts:getAnswerText)和 RPC 路径(index.ts:protoAnswersToResult)
4
- // 都调 formatAnswer 产出 "label1, label2 — comment" 格式,确保两条路径一致。
4
+ // 都调 formatAnswer 产出 "label1, label2" 格式,确保两条路径一致。
5
5
  // renderExpandedOptions(index.ts)调 parseAnswerParts 精确反解析选中项。
6
- import { ANSWER_COMMENT_SEPARATOR } from "./types";
7
6
 
8
7
  /**
9
- * 把答案各部分拼装为最终文本格式:"part1, part2 — comment"。
8
+ * 把答案各部分拼装为最终文本格式:"part1, part2"。
10
9
  * - parts 为空 → 返回 null(未答)
11
- * - comment 有值 → 追加 ANSWER_COMMENT_SEPARATOR + comment
12
10
  */
13
- export function formatAnswer(parts: string[], comment?: string | null): string | null {
11
+ export function formatAnswer(parts: string[]): string | null {
14
12
  if (parts.length === 0) return null;
15
- const base = parts.join(", ");
16
- return comment ? `${base}${ANSWER_COMMENT_SEPARATOR}${comment}` : base;
13
+ return parts.join(", ");
17
14
  }
18
15
 
19
16
  /**
@@ -22,24 +19,15 @@ export function formatAnswer(parts: string[], comment?: string | null): string |
22
19
  *
23
20
  * @param answer 最终答案文本(formatAnswer 产出)
24
21
  * @param labels 候选 label 列表(q.options 的 label),精确匹配
25
- * @returns selected=命中的 labels(按 answer 中出现顺序),comment=评论文本(如有)
22
+ * @returns selected=命中的 labels(按 answer 中出现顺序)
26
23
  */
27
24
  export function parseAnswerParts(
28
25
  answer: string,
29
26
  labels: string[],
30
- ): { selected: string[]; comment?: string } {
31
- // 先提取 comment(ANSWER_COMMENT_SEPARATOR 之后的部分)
32
- let body = answer;
33
- let comment: string | undefined;
34
- const sepIdx = answer.indexOf(ANSWER_COMMENT_SEPARATOR);
35
- if (sepIdx >= 0) {
36
- body = answer.slice(0, sepIdx);
37
- comment = answer.slice(sepIdx + ANSWER_COMMENT_SEPARATOR.length).trim();
38
- }
39
-
40
- // body 形如 "label1, label2" → 精确匹配候选 label
27
+ ): { selected: string[] } {
28
+ // answer 形如 "label1, label2" → 精确匹配候选 label
41
29
  const labelSet = new Set(labels);
42
- const tokens = body.split(/[,,]/).map((t) => t.trim()).filter(Boolean);
30
+ const tokens = answer.split(/[,,]/).map((t) => t.trim()).filter(Boolean);
43
31
  const selected: string[] = [];
44
32
  // 剩余 tokens 不匹配任何 label → 是 Other 自由文本(不返回,调用方自行处理)
45
33
  for (const token of tokens) {
@@ -47,5 +35,5 @@ export function parseAnswerParts(
47
35
  selected.push(token);
48
36
  }
49
37
  }
50
- return { selected, comment };
38
+ return { selected };
51
39
  }
@@ -9,8 +9,8 @@
9
9
  // 不循环)。返回 {value: JSON.stringify(answers)} 让子进程 JSON.parse(value) decode。
10
10
  // - TUI:走 ctx.ui.custom + AskUserComponent。三步:(1) protoQuestions → 内部 Question[],
11
11
  // (2) ctx.ui.custom 渲染拿内部 Result,(3) 内部 Result.answers(key=question 全文,
12
- // value="label1, label2 — comment")→ 重新编码为 proto AskUserAnswers(key=header/question,
13
- // 单选=value,多选=JSON 数组,Other→__other,comment→__comment),让子进程 decode 一致。
12
+ // value="label1, label2")→ 重新编码为 proto AskUserAnswers(key=header/question,
13
+ // 单选=value,多选=JSON 数组,Other→__other),让子进程 decode 一致。
14
14
  //
15
15
  // handler 收到的 req.channelPayload = {questions: AskUserQuestion[], allowCancel}(proto 格式,
16
16
  // 由子进程 askUserInteract 编码、subagent-workflow parseChannel 解析 options[0] JSON 得到)。
@@ -23,7 +23,7 @@ import {
23
23
  } from "@xyz-agent/extension-protocol";
24
24
 
25
25
  import { AskUserComponent } from "./component";
26
- import { ANSWER_COMMENT_SEPARATOR, type Option, type Question, type Result, type ThemeLike } from "./types";
26
+ import { type Option, type Question, type Result, type ThemeLike } from "./types";
27
27
 
28
28
  /**
29
29
  * channel handler 签名——与 subagent-workflow 的 UiChannelRegistry.ChannelHandler 一致
@@ -59,7 +59,6 @@ function protoToInternalQuestions(protoQuestions: AskUserQuestion[]): Question[]
59
59
  ...(pq.context !== undefined ? { context: pq.context } : {}),
60
60
  options: opts,
61
61
  ...(pq.multiSelect !== undefined ? { multiSelect: pq.multiSelect } : {}),
62
- ...(pq.allowComment !== undefined ? { allowComment: pq.allowComment } : {}),
63
62
  };
64
63
  });
65
64
  }
@@ -67,18 +66,17 @@ function protoToInternalQuestions(protoQuestions: AskUserQuestion[]): Question[]
67
66
  /**
68
67
  * 把 TUI 路径产出的内部 Result.answers 重新编码为 proto AskUserAnswers。
69
68
  *
70
- * 内部 Result.answers:key = question 全文,value = "label1, label2 — comment"
71
- * (Other 自由文本与 selected 标签逗号拼接,comment 用 ANSWER_COMMENT_SEPARATOR 分隔)。
69
+ * 内部 Result.answers:key = question 全文,value = "label1, label2"
70
+ * (Other 自由文本与 selected 标签逗号拼接)。
72
71
  *
73
72
  * proto AskUserAnswers 契约(@xyz-agent/extension-protocol):
74
73
  * - key = question.header ?? question 全文
75
74
  * - 单选:value = 选中项 value string
76
75
  * - 多选:value = JSON.stringify(选中项 value 数组)
77
76
  * - Other 自由文本:单独 key `${header}__other`
78
- * - comment:单独 key `${header}__comment`
79
77
  *
80
78
  * 解码(无信息丢失):用 protoQuestion.options 的 label 集合精确匹配 selected;
81
- * 不匹配的 token = Other 自由文本;comment 由 ANSWER_COMMENT_SEPARATOR 切出。
79
+ * 不匹配的 token = Other 自由文本。
82
80
  */
83
81
  function encodeTuiResultToProto(
84
82
  protoQuestions: AskUserQuestion[],
@@ -97,15 +95,8 @@ function encodeTuiResultToProto(
97
95
  if (o.value !== undefined) knownLabels.add(o.value);
98
96
  }
99
97
 
100
- // 切 body / comment(comment 在 ANSWER_COMMENT_SEPARATOR 之后)
101
- const sepIdx = internalText.indexOf(ANSWER_COMMENT_SEPARATOR);
102
- const body = sepIdx >= 0 ? internalText.slice(0, sepIdx) : internalText;
103
- const comment = sepIdx >= 0
104
- ? internalText.slice(sepIdx + ANSWER_COMMENT_SEPARATOR.length).trim() || undefined
105
- : undefined;
106
-
107
98
  // body tokens:匹配 knownLabels 的为 selected,其余为 Other 自由文本
108
- const tokens = body.split(/[,,]/).map((t: string) => t.trim()).filter((t: string) => t !== "");
99
+ const tokens = internalText.split(/[,,]/).map((t: string) => t.trim()).filter((t: string) => t !== "");
109
100
  const selected: string[] = [];
110
101
  const otherTokens: string[] = [];
111
102
  for (const t of tokens) {
@@ -130,7 +121,6 @@ function encodeTuiResultToProto(
130
121
  }
131
122
 
132
123
  if (otherText) answers[`${key}__other`] = otherText;
133
- if (comment) answers[`${key}__comment`] = comment;
134
124
  }
135
125
  return answers;
136
126
  }
package/src/component.ts CHANGED
@@ -201,8 +201,8 @@ export class AskUserComponent implements Component {
201
201
  const state = this.states[this.activeTab]!;
202
202
  const q = this.questions[this.activeTab]!;
203
203
 
204
- // freeform / comment mode → editor text input
205
- if (state.mode === "freeform" || state.mode === "comment") {
204
+ // freeform mode → editor text input
205
+ if (state.mode === "freeform") {
206
206
  this.handleEditorInput(data, state, q);
207
207
  return;
208
208
  }
@@ -264,14 +264,14 @@ export class AskUserComponent implements Component {
264
264
  }
265
265
  if (matchesKey(data, "enter")) {
266
266
  state.selectedIndices.add(state.cursorIndex);
267
- this.afterConfirm(state, q);
267
+ this.afterConfirm(state);
268
268
  return;
269
269
  }
270
270
  } else if (!q.multiSelect && !onOther) {
271
271
  if (matchesKey(data, "enter")) {
272
272
  state.selectedIndex = state.cursorIndex;
273
273
  state.freeTextValue = null;
274
- this.afterConfirm(state, q);
274
+ this.afterConfirm(state);
275
275
  return;
276
276
  }
277
277
  }
@@ -364,16 +364,8 @@ export class AskUserComponent implements Component {
364
364
  // 其他 special key(功能键/modifier 组合)→ no-op(不泄漏)
365
365
  }
366
366
 
367
- /** Esc:comment 跳过评论并 advance;freeform 存 freeDraft 草稿后回 options。 */
367
+ /** Esc:freeform 存 freeDraft 草稿后回 options。 */
368
368
  private handleEditorEsc(state: QuestionState): void {
369
- if (state.mode === "comment") {
370
- // comment Esc: skip comment, advance (keep existing commentValue)
371
- state.mode = "options";
372
- state.draftText = "";
373
- state.cursorIndex = state.savedOptionsCursorIndex;
374
- this.advance();
375
- return;
376
- }
377
369
  // freeform Esc: save draft to freeDraft (separate from submitted freeTextValue)
378
370
  // so discarded drafts don't pollute the answer or trigger auto-confirm.
379
371
  state.freeDraft = state.draftText || null;
@@ -383,34 +375,26 @@ export class AskUserComponent implements Component {
383
375
  this.rerender();
384
376
  }
385
377
 
386
- /** Enter:freeform 有文本→提交,空文本→回退;comment→保存评论并 advance。 */
378
+ /** Enter:freeform 有文本→提交,空文本→回退。 */
387
379
  private handleEditorEnter(state: QuestionState, q: Question): void {
388
380
  const text = state.draftText.trim();
389
- if (state.mode === "freeform") {
390
- state.cursorIndex = state.savedOptionsCursorIndex;
391
- if (text) {
392
- state.freeTextValue = text;
393
- state.selectedIndex = null;
394
- state.mode = "options";
395
- state.draftText = "";
396
- this.afterConfirm(state, q);
397
- } else {
398
- state.freeTextValue = null;
399
- state.mode = "options";
400
- state.draftText = "";
401
- // freeTextValue 刚清空;confirmed 仅在无其他选择时置 false(允许重新作答)
402
- if (q.multiSelect ? state.selectedIndices.size === 0 : state.selectedIndex === null) {
403
- state.confirmed = false;
404
- }
405
- this.rerender();
381
+ state.cursorIndex = state.savedOptionsCursorIndex;
382
+ if (text) {
383
+ state.freeTextValue = text;
384
+ state.selectedIndex = null;
385
+ state.mode = "options";
386
+ state.draftText = "";
387
+ this.afterConfirm(state);
388
+ } else {
389
+ state.freeTextValue = null;
390
+ state.mode = "options";
391
+ state.draftText = "";
392
+ // freeTextValue 刚清空;confirmed 仅在无其他选择时置 false(允许重新作答)
393
+ if (q.multiSelect ? state.selectedIndices.size === 0 : state.selectedIndex === null) {
394
+ state.confirmed = false;
406
395
  }
407
- return;
396
+ this.rerender();
408
397
  }
409
- state.commentValue = text || null;
410
- state.mode = "options";
411
- state.draftText = "";
412
- state.cursorIndex = state.savedOptionsCursorIndex;
413
- this.advance();
414
398
  }
415
399
 
416
400
  private toggleIndex(state: QuestionState, index: number): void {
@@ -450,17 +434,9 @@ export class AskUserComponent implements Component {
450
434
  this.rerender();
451
435
  }
452
436
 
453
- /** 选中确认后的处理:若 allowComment,进入评论模式(可重入编辑/清除已有评论);否则前进。 */
454
- private afterConfirm(state: QuestionState, q: Question): void {
437
+ /** 选中确认后的处理:直接前进到下一题(或单问题提交)。 */
438
+ private afterConfirm(state: QuestionState): void {
455
439
  state.confirmed = true;
456
- if (q.allowComment && state.mode !== "comment") {
457
- state.savedOptionsCursorIndex = state.cursorIndex;
458
- state.mode = "comment";
459
- state.draftText = state.commentValue ?? "";
460
- state.cursorIndex = state.draftText.length;
461
- this.rerender();
462
- return;
463
- }
464
440
  this.advance();
465
441
  }
466
442
 
package/src/index.ts CHANGED
@@ -7,7 +7,6 @@ import {
7
7
  askUserInteract,
8
8
  type AskUserQuestion,
9
9
  getAskUserAnswer,
10
- getAskUserComment,
11
10
  getAskUserOther,
12
11
  } from "@xyz-agent/extension-protocol";
13
12
 
@@ -128,15 +127,14 @@ function toProtoQuestions(questions: Question[]): AskUserQuestion[] {
128
127
  })),
129
128
  multiSelect: q.multiSelect,
130
129
  allowOther: true,
131
- allowComment: q.allowComment ?? false,
132
130
  }));
133
131
  }
134
132
 
135
133
  /**
136
134
  * 把协议包 AskUserAnswers 转换为 ask-user 内部 Result.answers。
137
135
  *
138
- * 协议格式:key=header/question, 单选=string, 多选=JSON数组, Other=__other, comment=__comment
139
- * ask-user 格式:key=question 全文, value=逗号分隔 label + Other, comment 内联(` — `)
136
+ * 协议格式:key=header/question, 单选=string, 多选=JSON数组, Other=__other
137
+ * ask-user 格式:key=question 全文, value=逗号分隔 label + Other
140
138
  *
141
139
  * 拼装逻辑复用 formatAnswer(与 TUI 版 getAnswerText 共享同一格式函数),
142
140
  * 确保 RPC 和 TUI 两条路径产出的 Result.answers 格式一致。
@@ -152,7 +150,6 @@ function protoAnswersToResult(
152
150
  const iq = protoQuestions[i]!;
153
151
  const selected = getAskUserAnswer(answers, iq);
154
152
  const other = getAskUserOther(answers, iq);
155
- const comment = getAskUserComment(answers, iq);
156
153
 
157
154
  const parts: string[] = [];
158
155
  if (Array.isArray(selected)) {
@@ -170,7 +167,7 @@ function protoAnswersToResult(
170
167
  parts.push(selected);
171
168
  }
172
169
  if (other) parts.push(other);
173
- const formatted = formatAnswer(parts, comment);
170
+ const formatted = formatAnswer(parts);
174
171
  if (formatted !== null) out[q.question] = formatted;
175
172
  }
176
173
  return out;
@@ -136,7 +136,7 @@ function buildOptionLines(
136
136
  for (let i = 0; i < opts.length; i++) {
137
137
  const opt = opts[i]!;
138
138
  // 编辑器模式下用 savedOptionsCursorIndex 判断选项高亮,cursorIndex 此时是文本光标
139
- const activeOptionCursor = (state.mode === "freeform" || state.mode === "comment") ? state.savedOptionsCursorIndex : state.cursorIndex;
139
+ const activeOptionCursor = state.mode === "freeform" ? state.savedOptionsCursorIndex : state.cursorIndex;
140
140
  const isSelected = i === activeOptionCursor;
141
141
  const isOther = opt.isOther === true;
142
142
  const prefix = isSelected ? t.fg("accent", ">") : " ";
@@ -224,33 +224,6 @@ function buildPreviewLines(
224
224
  return lines;
225
225
  }
226
226
 
227
- /**
228
- * freeform 模式:editor 已在 buildOptionLines 中原地渲染([ ] <input> 反色光标 行),
229
- * buildEditorBlock 在此模式下不重复输出,**仅留出与正常 help 行同位置的视觉空隙**。
230
- * comment 模式:保留独立编辑块(与 normal help 行解耦:comment 行有更长的 prompt)。
231
- */
232
- function buildEditorBlock(
233
- ctx: RenderContext,
234
- ): string[] {
235
- const { state, theme: t, width } = ctx;
236
- if (state.mode === "freeform") {
237
- return [""];
238
- }
239
- const lines: string[] = [];
240
- const add = (s: string): void => {
241
- lines.push(truncateToWidth(s, width));
242
- };
243
- add("");
244
- const prompt = t.fg("muted", " Your comment (optional):");
245
- add(prompt);
246
- // 渲染当前编辑器文本,光标用反色高亮当前字符(surrogate pair 安全)
247
- const cursorText = renderCursorText(state.draftText, state.cursorIndex);
248
- add(` ${t.fg("text", cursorText)}`);
249
- add("");
250
- add(t.fg("dim", EDITOR_HINT));
251
- return lines;
252
- }
253
-
254
227
  /** 渲染分屏模式下的左右双列(选项列表 + 详情预览)。 */
255
228
  function buildSplitPane(
256
229
  ctx: RenderContext,
@@ -302,21 +275,20 @@ export function renderQuestionView(ctx: RenderContext): string[] {
302
275
 
303
276
  // 选项模式下:question/context 与 options 之间加分割线(三段式)
304
277
  // 编辑器模式不加(编辑器块自带视觉边界)
305
- if (state.mode !== "freeform" && state.mode !== "comment") {
278
+ if (state.mode !== "freeform") {
306
279
  divider();
307
280
  }
308
281
 
309
- // 编辑器/评论模式:选项列表 + 编辑器块(freeform 模式下编辑器块为空,由 buildOptionLines 原地渲染)。
282
+ // 编辑器模式:选项列表 + 原地编辑器行(freeform 模式下 buildOptionLines 原地渲染)。
310
283
  // 编辑器模式一律用全 width 单列渲染——分屏左列仅约 42% 宽,Other 自由输入会被压窄换行,
311
284
  // 且右侧详情预览在输入自定义内容时无意义。隐藏 descriptions 以避免行数爆炸。
312
- if (state.mode === "freeform" || state.mode === "comment") {
285
+ if (state.mode === "freeform") {
313
286
  add("");
314
287
  for (const line of buildOptionLines(ctx, false)) add(line);
315
- lines.push(...buildEditorBlock(ctx));
316
- if (state.mode === "freeform") {
317
- // freeform 模式 help 行:光标锁在 Other 上,正在输入
318
- add(t.fg("dim", EDITOR_HINT));
319
- }
288
+ // 选项列表与 help 行之间的视觉空隙(旧 buildEditorBlock freeform 分支返回 [""] 提供)
289
+ add("");
290
+ // freeform 模式 help 行:光标锁在 Other 上,正在输入
291
+ add(t.fg("dim", EDITOR_HINT));
320
292
  return lines;
321
293
  }
322
294
 
@@ -44,7 +44,7 @@ export function getAnswerText(q: Question, s: QuestionState): string | null {
44
44
  if (label) parts.push(label);
45
45
  }
46
46
  if (s.freeTextValue !== null) parts.push(s.freeTextValue);
47
- return formatAnswer(parts, s.commentValue);
47
+ return formatAnswer(parts);
48
48
  }
49
49
 
50
50
  /**
package/src/types.ts CHANGED
@@ -10,7 +10,6 @@ export const SPLIT_PANE_MIN_WIDTH = 84;
10
10
  export const SPLIT_PANE_SEPARATOR = " │ ";
11
11
  export const SPLIT_PANE_LEFT_MIN = 32;
12
12
  export const SPLIT_PANE_RIGHT_MIN = 28;
13
- export const ANSWER_COMMENT_SEPARATOR = " — ";
14
13
 
15
14
  // ── Input schema(LLM 调用参数) ─────────────────────
16
15
  // description 用英文:这些字符串会进 LLM 的 tool schema,英文更利于模型理解。
@@ -47,9 +46,6 @@ export const QuestionSchema = Type.Object({
47
46
  multiSelect: Type.Optional(
48
47
  Type.Boolean({ description: "Default false. Set true only when more than one option can validly apply simultaneously; otherwise leave false for a single best answer." }),
49
48
  ),
50
- allowComment: Type.Optional(
51
- Type.Boolean({ description: "Default false. Set true to let the user append a short free-text comment after selecting (e.g. to note a constraint)." }),
52
- ),
53
49
  });
54
50
 
55
51
  /**
@@ -123,7 +119,7 @@ export interface ThemeLike {
123
119
  }
124
120
 
125
121
  /** 单问题的交互模式 */
126
- export type QuestionMode = "options" | "freeform" | "comment";
122
+ export type QuestionMode = "options" | "freeform";
127
123
 
128
124
  /** 单问题的交互状态(每问题一个实例) */
129
125
  export interface QuestionState {
@@ -140,8 +136,6 @@ export interface QuestionState {
140
136
  /** freeform Esc 保存的未提交草稿;null=无草稿。
141
137
  * 与 freeTextValue(已提交答案)分离,避免放弃的草稿污染答案、触发 auto-confirm。 */
142
138
  freeDraft: string | null;
143
- /** 可选评论;null=未输入 */
144
- commentValue: string | null;
145
139
  /** 当前交互模式 */
146
140
  mode: QuestionMode;
147
141
  /** 编辑器草稿文本(每问题独立持有,进编辑器时预填、退出时清空) */
@@ -159,7 +153,6 @@ export function createQuestionState(): QuestionState {
159
153
  confirmed: false,
160
154
  freeTextValue: null,
161
155
  freeDraft: null,
162
- commentValue: null,
163
156
  mode: "options",
164
157
  draftText: "",
165
158
  savedOptionsCursorIndex: 0,
package/src/validate.ts CHANGED
@@ -72,12 +72,12 @@ export function validateInput(questions: InputQuestion[]): string | null {
72
72
  }
73
73
 
74
74
  // S3: 多问题时 header 唯一——重复 header 会导致 askUserKey 碰撞,
75
- // 后一个 question 的 __other/__comment 覆盖前一个(协议 helper 用 header 作 answers 读取 key)。
75
+ // 后一个 question 的 __other 覆盖前一个(协议 helper 用 header 作 answers 读取 key)。
76
76
  const seenHeaders = new Set<string>();
77
77
  for (const q of questions) {
78
78
  const h = q.header!.trim();
79
79
  if (seenHeaders.has(h)) {
80
- return `Duplicate header "${h}" in questions. Headers must be unique in multi-question mode — shared headers cause answer key collisions (one question's Other/comment overwrites another's). Rephrase one header to differ.`;
80
+ return `Duplicate header "${h}" in questions. Headers must be unique in multi-question mode — shared headers cause answer key collisions (one question's Other overwrites another's). Rephrase one header to differ.`;
81
81
  }
82
82
  seenHeaders.add(h);
83
83
  }