@zhushanwen/pi-ask-user 0.0.1 → 0.0.2

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-ask-user",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Inline adaptive ask_user tool for Pi — single/multi-question structured input with split-pane preview, inline editor, and optional comments.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -25,6 +25,8 @@
25
25
  "README.md"
26
26
  ],
27
27
  "devDependencies": {
28
+ "@earendil-works/pi-tui": "*",
29
+ "@sinclair/typebox": "*",
28
30
  "@types/node": "^24.0.0",
29
31
  "vitest": "^4.1.8"
30
32
  },
@@ -117,21 +117,21 @@ describe("AskUserComponent — multi question tab nav", () => {
117
117
 
118
118
  it("C-9: Submit tab blocks when not all confirmed", () => {
119
119
  const { c, result } = make(multiQ);
120
- c.handleInput(TAB); // -> Q2
121
- c.handleInput(TAB); // -> Q3
122
- c.handleInput(TAB); // -> Submit
120
+ c.handleInput(RIGHT); // -> Q2
121
+ c.handleInput(RIGHT); // -> Q3
122
+ c.handleInput(RIGHT); // -> Submit
123
123
  c.handleInput(ENTER); // should NOT submit
124
124
  expect(result.val).toBeUndefined();
125
125
  });
126
126
 
127
- // C-10 / C-11 已废弃:←/→ 不再切 tab(C-E5/C-E6 覆盖 Tab/Shift+Tab 行为)
127
+ // C-10 / C-11 已废弃:Tab/Shift+Tab 不再切问题 tab(C-E5/C-E6 覆盖 ←/→ 切问题 tab 行为)
128
128
 
129
129
  it("C-16 (AC-16): can re-edit confirmed answer", () => {
130
130
  const { c, result } = make(multiQ);
131
131
  // Q1: select A
132
132
  c.handleInput(ENTER); // → Q2
133
- // Go back to Q1
134
- c.handleInput("\x1b[Z"); // Q1
133
+ // Go back to Q1(问题 tab 间用 ← 回退)
134
+ c.handleInput(LEFT); // Q1
135
135
  // Select B instead
136
136
  c.handleInput(DOWN);
137
137
  c.handleInput(ENTER); // → Q2
@@ -152,7 +152,7 @@ describe("AskUserComponent — multi question tab nav", () => {
152
152
  const { c, result } = make(twoQ);
153
153
  c.handleInput(ENTER); // Q1 select A → Q2
154
154
  c.handleInput(" "); // Q2 toggle X (no Enter-confirm)
155
- c.handleInput(TAB); // → Submit, should auto-confirm Q2
155
+ c.handleInput(RIGHT); // → Submit, should auto-confirm Q2
156
156
  c.handleInput(ENTER); // Submit (all confirmed)
157
157
  expect(result.val!.answers["Q1"]).toBe("A");
158
158
  expect(result.val!.answers["Q2"]).toBe("X");
@@ -179,7 +179,7 @@ describe("AskUserComponent — multi question tab nav", () => {
179
179
  ];
180
180
  const { c, result } = make(twoQMulti);
181
181
  c.handleInput(" "); // Q1 toggle A
182
- c.handleInput(TAB); // → Q2,auto-confirm Q1,不进评论
182
+ c.handleInput(RIGHT); // → Q2,auto-confirm Q1,不进评论
183
183
  // 验证:当前在 Q2(非 Q1 的评论模式)。Q2 选 X → Submit
184
184
  c.handleInput(ENTER); // Q2 select X → Submit
185
185
  c.handleInput(ENTER); // Submit
@@ -201,8 +201,8 @@ describe("AskUserComponent — multi question tab nav", () => {
201
201
  c.handleInput("o");
202
202
  c.handleInput("m");
203
203
  c.handleInput(ENTER); // 保存 freeText → confirmed=true → advance to Q2
204
- // 切回 Q1,重进 Other 编辑器,清空后空 Enter
205
- c.handleInput("\x1b[Z"); // Shift+Tab → Q1
204
+ // 切回 Q1,重进 Other 编辑器,清空后空 Enter(问题 tab 间用 ← 回退)
205
+ c.handleInput(LEFT); // → Q1
206
206
  c.handleInput(DOWN); // idempotent: cursor stays on Other
207
207
  c.handleInput(DOWN);
208
208
  c.handleInput(ENTER); // 重开 freeform,editorText 预填 "custom"
@@ -214,9 +214,9 @@ describe("AskUserComponent — multi question tab nav", () => {
214
214
  c.handleInput(BKSP);
215
215
  c.handleInput(ENTER); // 空 Enter → freeTextValue 清空,confirmed 应重置 false
216
216
  // 导航到 Submit 并尝试提交 → 应被阻塞(Q1 回到未答)
217
- c.handleInput(TAB); // → Q2
218
- c.handleInput(TAB); // → Q3
219
- c.handleInput(TAB); // → Submit
217
+ c.handleInput(RIGHT); // → Q2
218
+ c.handleInput(RIGHT); // → Q3
219
+ c.handleInput(RIGHT); // → Submit
220
220
  c.handleInput(ENTER); // 应被阻塞
221
221
  expect(result.val).toBeUndefined();
222
222
  });
@@ -513,9 +513,9 @@ describe("AskUserComponent — render cache", () => {
513
513
  describe("AskUserComponent — Submit tab", () => {
514
514
  it("C-47: Submit Esc backs to last question (no longer cancels)", () => {
515
515
  const { c, result } = make(multiQ);
516
- c.handleInput(TAB); // Q2
517
- c.handleInput(TAB); // Q3
518
- c.handleInput(TAB); // Submit
516
+ c.handleInput(RIGHT); // Q2
517
+ c.handleInput(RIGHT); // Q3
518
+ c.handleInput(RIGHT); // Submit
519
519
  c.handleInput(ESC); // 回退到最后一个问题 Q3(不取消)
520
520
  expect(result.val).toBeUndefined();
521
521
  const lines = c.render(80);
@@ -538,17 +538,17 @@ describe("AskUserComponent — Submit tab", () => {
538
538
  });
539
539
 
540
540
  it("C-S12: Submit tab Left navigates to last question tab", () => {
541
- // S-12 锁定:Submit tab 上按 Left → activeTab = questions.length - 1(最后一个问题)
541
+ // 锁定:Submit tab 上按 → activeTab = questions.length - 1(最后一个问题)
542
542
  const { c } = make(multiQ); // 3 questions → tabs 0,1,2,3=Submit
543
- // 导航到 Submit
544
- c.handleInput(TAB); // Q1 → Q2
545
- c.handleInput(TAB); // Q2 → Q3
546
- c.handleInput(TAB); // Q3 → Submit
543
+ // 导航到 Submit(问题 tab 间用 →)
544
+ c.handleInput(RIGHT); // Q1 → Q2
545
+ c.handleInput(RIGHT); // Q2 → Q3
546
+ c.handleInput(RIGHT); // Q3 → Submit
547
547
  // 确认当前在 Submit(渲染 Submit 视图)
548
548
  let lines = c.render(80);
549
549
  expect(lines.some((l) => l.includes("Ready") || l.includes("Unanswered"))).toBe(true);
550
- // 在 Submit 上按 Left → 应回到最后一个问题 Q3
551
- c.handleInput("\x1b[Z");
550
+ // 在 Submit 上按 → 应回到最后一个问题 Q3(←/→ 在所有 tab 上都是导航)
551
+ c.handleInput(LEFT);
552
552
  lines = c.render(80);
553
553
  // Q3 不再是 Submit 视图(无 Ready/Unanswered),且渲染了 Q3 的选项 M
554
554
  expect(lines.some((l) => l.includes("Ready") || l.includes("Unanswered"))).toBe(false);
@@ -607,8 +607,8 @@ describe("AskUserComponent — confirm-checkmark, Esc-back, Tab browsing", () =>
607
607
  it("C-E1: confirmed tab shows green ✓ marker", () => {
608
608
  const { c } = make(multiQ);
609
609
  c.handleInput(ENTER); // Q1 确认 → Q2
610
- // 回到 Q1 看 tab 栏:Q1 已确认应有 ✓ 标识
611
- c.handleInput("\x1b[Z"); // Q2 → Q1
610
+ // 回到 Q1 看 tab 栏:Q1 已确认应有 ✓ 标识(问题 tab 间用 ← 回退)
611
+ c.handleInput(LEFT); // Q2 → Q1
612
612
  const lines = c.render(80);
613
613
  expect(lines.some((l) => l.includes("✓") && l.includes("First"))).toBe(true);
614
614
  });
@@ -642,29 +642,31 @@ describe("AskUserComponent — confirm-checkmark, Esc-back, Tab browsing", () =>
642
642
  expect(c.render(80).some((l) => l.includes("Cancel all"))).toBe(false);
643
643
  });
644
644
 
645
- it("C-E5: Tab navigates to next tab; Shift+Tab to previous", () => {
645
+ it("C-E5: ←/→ navigates question tabs (→ next, previous)", () => {
646
646
  const { c } = make(multiQ);
647
- c.handleInput(TAB); // Q1 → Q2
647
+ c.handleInput(RIGHT); // Q1 → Q2
648
648
  // 确认在 Q2:渲染含 Q2 的选项 X
649
649
  expect(c.render(80).some((l) => l.includes("X"))).toBe(true);
650
- // Shift+Tab(xterm 序列 ESC[Z)回退到 Q1
651
- c.handleInput("\x1b[Z");
650
+ // 回退到 Q1
651
+ c.handleInput(LEFT);
652
652
  expect(c.render(80).some((l) => l.includes("Q1") || l.includes("First"))).toBe(true);
653
653
  });
654
654
 
655
- it("C-E6: Tab wraps Submit Q1, Shift+Tab wraps Q1 Submit", () => {
655
+ it("C-E6: at last question enters Submit; at first question stays (no wrap)", () => {
656
656
  const { c } = make(multiQ);
657
657
  // 到 Submit:Q1→Q2→Q3→Submit
658
658
  c.handleInput(ENTER); // Q1→Q2
659
- c.handleInput(TAB); // Q2→Q3
660
- c.handleInput(TAB); // Q3→Submit
659
+ c.handleInput(RIGHT); // Q2→Q3
660
+ c.handleInput(RIGHT); // Q3→Submit
661
661
  expect(c.render(80).some((l) => l.includes("Ready") || l.includes("Unanswered"))).toBe(true);
662
- // Submit 上 Tab环绕到 Q1
663
- c.handleInput(TAB);
662
+ // Submit 上 回退到 Q3(←/→ 在所有 tab 上都是导航)
663
+ c.handleInput(LEFT);
664
+ expect(c.render(80).some((l) => l.includes("M"))).toBe(true);
665
+ // Q1 上 ← 不环绕(停在首个问题)
666
+ c.handleInput(LEFT); // Q3→Q2
667
+ c.handleInput(LEFT); // Q2→Q1
668
+ c.handleInput(LEFT); // Q1 上 ← → 停留 Q1
664
669
  expect(c.render(80).some((l) => l.includes("Q1") || l.includes("First"))).toBe(true);
665
- // Q1 上 Shift+Tab → 环绕回 Submit
666
- c.handleInput("\x1b[Z");
667
- expect(c.render(80).some((l) => l.includes("Ready") || l.includes("Unanswered"))).toBe(true);
668
670
  });
669
671
  });
670
672
 
@@ -704,39 +706,39 @@ describe("AskUserComponent — new behavior (post-refactor)", () => {
704
706
  expect(result.val!.answers["Which features?"]).toBe("Auth, redis");
705
707
  });
706
708
 
707
- it("C-NEW-2: Left/Right on question tab do NOT switch tabs (only Tab/Shift+Tab do)", () => {
709
+ it("C-NEW-2: ←/→ on question tab switches tabs ( next, ← previous; ← no wrap at first)", () => {
708
710
  // multiQ: 3 questions → tabs 0,1,2,3=Submit
709
711
  const { c } = make(multiQ);
710
712
  c.render(80);
711
- // 1) Q1 上按 Right → 应仍在 Q1(不切 tab)
713
+ // 1) Q1 上按 Right → 应切到 Q2
712
714
  c.handleInput(RIGHT);
715
+ expect(c.render(80).some((l) => l.includes("Q2") || l.includes("Second"))).toBe(true);
716
+ // 2) Left 回到 Q1
717
+ c.handleInput(LEFT);
713
718
  expect(c.render(80).some((l) => l.includes("Q1") || l.includes("First"))).toBe(true);
714
- // 2) Left 也不切
719
+ // 3) Q1 上 Left 不环绕(仍停 Q1)
715
720
  c.handleInput(LEFT);
716
721
  expect(c.render(80).some((l) => l.includes("Q1") || l.includes("First"))).toBe(true);
717
- // 3) Tab 仍然可以切到 Q2
718
- c.handleInput(TAB);
719
- expect(c.render(80).some((l) => l.includes("Q2") || l.includes("Second"))).toBe(true);
720
722
  });
721
723
 
722
- it("C-NEW-3: Submit tab Left/Right toggles submitTabFocus (Submit ↔ Cancel)", () => {
724
+ it("C-NEW-3: Submit tab Tab toggles submitTabFocus (Submit ↔ Cancel)", () => {
723
725
  // multiQ: 3 questions → tabs 0,1,2,3=Submit
724
726
  const { c } = make(multiQ);
725
- // 导航到 Submit
726
- c.handleInput(TAB); // Q1→Q2
727
- c.handleInput(TAB); // Q2→Q3
728
- c.handleInput(TAB); // Q3→Submit
727
+ // 导航到 Submit(问题 tab 间用 →)
728
+ c.handleInput(RIGHT); // Q1→Q2
729
+ c.handleInput(RIGHT); // Q2→Q3
730
+ c.handleInput(RIGHT); // Q3→Submit
729
731
  // Submit tab 默认 focus=Submit。验证渲染中 Submit 高亮(accent)
730
732
  let lines = c.render(80);
731
733
  const focusedLineInitial = lines.find((l) => l.match(/[\[\(]\s*Submit\s*[\]\)]/));
732
734
  expect(focusedLineInitial).toBeDefined();
733
- // 按 Right → focus 切到 Cancel
734
- c.handleInput(RIGHT);
735
+ // 按 Tab → focus 切到 Cancel(单键双向循环)
736
+ c.handleInput(TAB);
735
737
  lines = c.render(80);
736
738
  const focusedLineAfter = lines.find((l) => l.match(/[\[\(]\s*Cancel\s*[\]\)]/));
737
739
  expect(focusedLineAfter).toBeDefined();
738
- // 再按 Left → focus 回 Submit
739
- c.handleInput(LEFT);
740
+ // 再按 Tab → focus 回 Submit
741
+ c.handleInput(TAB);
740
742
  lines = c.render(80);
741
743
  expect(lines.find((l) => l.match(/[\[\(]\s*Submit\s*[\]\)]/))).toBeDefined();
742
744
  });
@@ -762,8 +764,8 @@ describe("AskUserComponent — new behavior (post-refactor)", () => {
762
764
  c.handleInput(ENTER); // Q1 → Q2
763
765
  c.handleInput(ENTER); // Q2 → Q3
764
766
  c.handleInput(ENTER); // Q3 → Submit tab
765
- // 切到 Submit 后,按 Right 把 focus 切到 Cancel
766
- c.handleInput(RIGHT);
767
+ // 切到 Submit 后,按 Tab 把 focus 切到 Cancel
768
+ c.handleInput(TAB);
767
769
  // Enter → 直接 cancel()(Submit tab 上无二次确认)
768
770
  c.handleInput(ENTER);
769
771
  expect(result.val).toBeNull();
@@ -774,9 +776,8 @@ describe("AskUserComponent — new behavior (post-refactor)", () => {
774
776
  // 只答 Q1
775
777
  c.handleInput(ENTER); // Q1 → Q2
776
778
  // 切到 Submit(Q2 还未答)
777
- c.handleInput(TAB); // Q2
778
- c.handleInput(TAB); // Q3
779
- c.handleInput(TAB); // Submit
779
+ c.handleInput(RIGHT); // Q2 → Q3
780
+ c.handleInput(RIGHT); // Q3 → Submit
780
781
  // focus=Submit(默认),按 Enter → 不提交(Q2/Q3 未答)
781
782
  c.handleInput(ENTER);
782
783
  expect(result.val).toBeUndefined();
@@ -138,7 +138,7 @@ describe("renderQuestionView — split pane", () => {
138
138
 
139
139
  // ── Q-15 ~ Q-17: Other 编辑器模式 ───────────────────────
140
140
  describe("renderQuestionView — Other editor mode", () => {
141
- it("Q-15: freeform mode renders Other row in-place with draft + cursor", () => {
141
+ it("Q-15: freeform mode renders Other row in-place with draft + cursor + number prefix", () => {
142
142
  const lines = renderQuestionView(
143
143
  singleQ,
144
144
  makeState({ mode: "freeform", cursorIndex: 2 }),
@@ -153,6 +153,32 @@ describe("renderQuestionView — Other editor mode", () => {
153
153
  expect(t).toContain("█");
154
154
  // 不再独立 "Your answer" 提示行
155
155
  expect(t).not.toContain("Your answer");
156
+ // 需求4:Other 在 freeform 态也有编号前缀(与其他选项一致)。
157
+ // singleQ 有 2 个选项 → Other 是第 3 项,lead 应含 "3. "
158
+ expect(t).toContain("3. ");
159
+ });
160
+
161
+ it("Q-15-NUM: multi-select freeform Other row shows [ ] + number prefix", () => {
162
+ // Auth(1), Search(2), Other(3),多选 freeform 应渲染 "> [ ] 3. <input>█"
163
+ const multiQ: Question = {
164
+ question: "Which features?",
165
+ multiSelect: true,
166
+ options: [{ label: "Auth" }, { label: "Search" }],
167
+ };
168
+ const lines = renderQuestionView(
169
+ multiQ,
170
+ makeState({ mode: "freeform", cursorIndex: 2 }),
171
+ stubTheme,
172
+ 60,
173
+ true,
174
+ "custom",
175
+ );
176
+ const t = text(lines);
177
+ // 多选 box [ ] + 编号 3. 都在编辑行上
178
+ expect(t).toContain("[ ]");
179
+ expect(t).toContain("3. ");
180
+ expect(t).toContain("custom");
181
+ expect(t).toContain("█");
156
182
  });
157
183
 
158
184
  it("Q-16: Other with saved free-text shows checkmark + preview", () => {
@@ -208,6 +234,31 @@ describe("renderQuestionView — Other editor mode", () => {
208
234
  }
209
235
  });
210
236
 
237
+ it("Q-28-WIDE: freeform 模式在宽终端下用全宽渲染(不被分屏左列压窄)", () => {
238
+ // 回归:freeform/comment 模式忽略分屏,编辑器用全 width。
239
+ // 修复前:宽终端走 split.left(≈40),Other 输入被压在左半屏换行频繁。
240
+ const width = 100;
241
+ // getSplitPaneWidths(100) 非 null(宽终端会进分屏分支),但 freeform 应绕过它
242
+ expect(getSplitPaneWidths(width)).not.toBeNull();
243
+ // 单选 lead visLen=5 → avail = width - 5 = 95。用 60 字符(< 95)应单行装下。
244
+ // 修复前若用 split.left≈40,avail≈35 → 60 字符会换 2 行。
245
+ const input = "a".repeat(60);
246
+ const lines = renderQuestionView(
247
+ singleQ,
248
+ makeState({ mode: "freeform", cursorIndex: 2 }),
249
+ stubTheme,
250
+ width,
251
+ true,
252
+ input,
253
+ );
254
+ // 用 █ 定位编辑器行(help 行不含 █),排除 “submit” 含 a 的干扰
255
+ const editorLines = lines.filter((l) => l.includes("█"));
256
+ expect(editorLines.length).toBe(1);
257
+ // 全部 60 个 a 都在这一行(未被换行拆分)
258
+ const aCount = editorLines[0]!.split("").filter((c) => c === "a").length;
259
+ expect(aCount).toBe(60);
260
+ });
261
+
211
262
  it("Q-29: freeform 输入超 5 行时截断到 5 行并加省略号", () => {
212
263
  const width = 30;
213
264
  // avail=24,200 字符 → 9 行,超过 5 行上限
package/src/component.ts CHANGED
@@ -221,14 +221,15 @@ export class AskUserComponent implements Component {
221
221
  return;
222
222
  }
223
223
 
224
- // Tab / Shift+Tab 切换 tab(多问题,options 模式)。←/→ 故意不切 tab——
225
- // 把左右键留给 Submit tab 上的 Submit/Cancel 焦点切换,避免在问题列表上意外跳走。
226
- if (!this.isSingle && matchesKey(data, "tab")) {
227
- this.gotoTab((this.activeTab + 1) % this.totalTabs);
224
+ // / 切换问题 tab(多问题,options 模式):末尾 进入 Submit tab
225
+ // 首个 停在首个问题(不环绕)。Tab/Shift+Tab 不在问题 tab tab——
226
+ // 留给 Submit tab 上的 Submit/Cancel 焦点切换。
227
+ if (!this.isSingle && matchesKey(data, "right")) {
228
+ this.gotoTab(Math.min(this.activeTab + 1, this.questions.length));
228
229
  return;
229
230
  }
230
- if (!this.isSingle && matchesKey(data, "shift+tab")) {
231
- this.gotoTab((this.activeTab - 1 + this.totalTabs) % this.totalTabs);
231
+ if (!this.isSingle && matchesKey(data, "left")) {
232
+ this.gotoTab(Math.max(this.activeTab - 1, 0));
232
233
  return;
233
234
  }
234
235
 
@@ -285,33 +286,33 @@ export class AskUserComponent implements Component {
285
286
  }
286
287
 
287
288
  /**
288
- * Submit tab 输入路由:
289
- * - ← / → → 切换 submitTabFocus(Submit Cancel 视觉高亮)
289
+ * Submit tab 输入路由(键位语义与问题 tab 全局一致):
290
+ * - ← / → → tab 导航(← 回到最后一个问题;→ 环绕到首个问题)
291
+ * - Tab → 在 Submit ↔ Cancel 间循环切焦点(单键双向,不依赖 shift+tab,
292
+ * 规避 Pi 全局 app.thinking.cycle 对 shift+tab 的拦截)
290
293
  * - Enter → 触发当前 focus 项:Submit=allConfirmed 才提交;Cancel=直接取消
291
- * - Tab / 环绕到首个问题
292
- * - Esc / Shift+Tab → 回退到最后一个问题
293
- *
294
- * 注意:Submit tab 上 ←/→ 不再切回问题区(与问题 tab 上的"←/→ 不切 tab"对称,
295
- * 避免在 Submit 视图内意外跳走)。
294
+ * - Esc回退到最后一个问题(与问题 tab 的 Esc-back 语义一致)
296
295
  */
297
296
  private handleSubmitTabInput(data: string): void {
298
- // ← / → 切换 focus(Submit Cancel)。Tab 单独处理(→ 行为)
299
- if (matchesKey(data, "left") || matchesKey(data, "right")) {
300
- this.submitTabFocus = this.submitTabFocus === "submit" ? "cancel" : "submit";
301
- this.invalidate();
302
- this.tui.requestRender();
297
+ // ← / → tab 导航(与问题 tab 一致:方向键管 tab 间移动)
298
+ if (matchesKey(data, "left")) {
299
+ this.gotoTab(this.questions.length - 1);
303
300
  return;
304
301
  }
305
- // Esc / Shift+Tab → 回退到最后一个问题
306
- if (matchesKey(data, "escape") || matchesKey(data, "shift+tab")) {
302
+ if (matchesKey(data, "right")) {
303
+ this.gotoTab(0);
304
+ return;
305
+ }
306
+ // Esc → 回退到最后一个问题
307
+ if (matchesKey(data, "escape")) {
307
308
  this.activeTab = this.questions.length - 1;
308
309
  this.invalidate();
309
310
  this.tui.requestRender();
310
311
  return;
311
312
  }
312
- // Tab → 环绕到首个问题
313
+ // Tab → Submit ↔ Cancel 循环切焦点(单键双向)
313
314
  if (matchesKey(data, "tab")) {
314
- this.activeTab = 0;
315
+ this.submitTabFocus = this.submitTabFocus === "submit" ? "cancel" : "submit";
315
316
  this.invalidate();
316
317
  this.tui.requestRender();
317
318
  return;
@@ -107,9 +107,11 @@ function buildOptionLines(
107
107
 
108
108
  if (isOther) {
109
109
  if (state.mode === "freeform") {
110
- // 原地编辑:与多选普通选项的 [ ] 框视觉对齐(单选无勾选语义则留空)
110
+ // 原地编辑:与普通选项的 [ ] 框 + 编号视觉对齐(单选无勾选语义则留空)。
111
+ // lead = "> [ ] N. ",输入文本跟在编号后,与其他选项整体一致。
111
112
  const box = q.multiSelect ? t.fg("dim", "[ ]") : " ";
112
- const lead = `${prefix} ${box} `;
113
+ const num = i + 1;
114
+ const lead = `${prefix} ${box} ${t.fg("muted", `${num}. `)}`;
113
115
  const avail = Math.max(1, width - visibleWidth(lead));
114
116
  // 文本 + 末尾光标 █ 整体软换行(空 input 时仅光标,wrapTextWithAnsi("█") 单行)
115
117
  const styled = `${t.fg("text", editorText)}${t.fg("accent", "█")}`;
@@ -277,10 +279,12 @@ export function renderQuestionView(
277
279
  divider();
278
280
  }
279
281
 
280
- // 编辑器/评论模式:选项列表 + 编辑器块(freeform 模式下编辑器块为空,由 buildOptionLines 原地渲染)
282
+ // 编辑器/评论模式:选项列表 + 编辑器块(freeform 模式下编辑器块为空,由 buildOptionLines 原地渲染)。
283
+ // 编辑器模式一律用全 width 单列渲染——分屏左列仅约 42% 宽,Other 自由输入会被压窄换行,
284
+ // 且右侧详情预览在输入自定义内容时无意义。隐藏 descriptions 以避免行数爆炸。
281
285
  if (state.mode === "freeform" || state.mode === "comment") {
282
286
  add("");
283
- const optionLines = buildOptionLines(q, state, theme, split ? split.left : width, !!split, editorText);
287
+ const optionLines = buildOptionLines(q, state, theme, width, false, editorText);
284
288
  for (const line of optionLines) add(line);
285
289
  const editorBlock = buildEditorBlock(theme, width, state.mode, editorText);
286
290
  lines.push(...editorBlock);
@@ -305,7 +309,7 @@ export function renderQuestionView(
305
309
  // 帮助行(上下文相关)
306
310
  const opts = allOptions(q);
307
311
  const onOther = state.cursorIndex === opts.length - 1;
308
- const tabHint = isSingle ? "" : " · Tab switch tabs";
312
+ const tabHint = isSingle ? "" : " · ←/→ switch tabs";
309
313
  const actionHint = onOther
310
314
  ? "Enter open editor"
311
315
  : q.multiSelect
@@ -35,8 +35,8 @@ export function getAnswerText(q: Question, s: QuestionState): string | null {
35
35
 
36
36
  /**
37
37
  * 渲染 Submit tab 视图。
38
- * focus: 当前在 [Submit]/[Cancel] 上的焦点(←/→ 切换)。
39
- * 渲染内嵌按钮栏高亮 focus;help 行更新为 "←/→ toggle · Enter confirm"。
38
+ * focus: 当前在 [Submit]/[Cancel] 上的焦点(Tab 切换)。
39
+ * 渲染内嵌按钮栏高亮 focus;help 行更新为 "←/→ navigate · Tab toggle · Enter confirm"。
40
40
  */
41
41
  export function renderSubmitView(
42
42
  questions: Question[],
@@ -94,7 +94,7 @@ export function renderSubmitView(
94
94
 
95
95
  // Submit tab 帮助行
96
96
  add("");
97
- add(t.fg("dim", " ←/→ toggle · Enter confirm · Tab back to first question"));
97
+ add(t.fg("dim", " ←/→ navigate · Tab toggle Submit/Cancel · Enter confirm · Esc back"));
98
98
 
99
99
  return lines;
100
100
  }