@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 +3 -1
- package/src/__tests__/component.test.ts +58 -57
- package/src/__tests__/question-view.test.ts +52 -1
- package/src/component.ts +23 -22
- package/src/question-view.ts +9 -5
- package/src/submit-view.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-ask-user",
|
|
3
|
-
"version": "0.0.
|
|
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(
|
|
121
|
-
c.handleInput(
|
|
122
|
-
c.handleInput(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
218
|
-
c.handleInput(
|
|
219
|
-
c.handleInput(
|
|
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(
|
|
517
|
-
c.handleInput(
|
|
518
|
-
c.handleInput(
|
|
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
|
-
//
|
|
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(
|
|
545
|
-
c.handleInput(
|
|
546
|
-
c.handleInput(
|
|
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 上按
|
|
551
|
-
c.handleInput(
|
|
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(
|
|
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:
|
|
645
|
+
it("C-E5: ←/→ navigates question tabs (→ next, ← previous)", () => {
|
|
646
646
|
const { c } = make(multiQ);
|
|
647
|
-
c.handleInput(
|
|
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
|
-
//
|
|
651
|
-
c.handleInput(
|
|
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:
|
|
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(
|
|
660
|
-
c.handleInput(
|
|
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 上
|
|
663
|
-
c.handleInput(
|
|
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:
|
|
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 →
|
|
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
|
-
//
|
|
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
|
|
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(
|
|
727
|
-
c.handleInput(
|
|
728
|
-
c.handleInput(
|
|
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
|
-
// 按
|
|
734
|
-
c.handleInput(
|
|
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
|
-
// 再按
|
|
739
|
-
c.handleInput(
|
|
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 后,按
|
|
766
|
-
c.handleInput(
|
|
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(
|
|
778
|
-
c.handleInput(
|
|
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
|
-
//
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
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, "
|
|
231
|
-
this.gotoTab((this.activeTab - 1
|
|
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
|
-
* - ← / → →
|
|
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
|
-
* -
|
|
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
|
-
// ← / →
|
|
299
|
-
if (matchesKey(data, "left")
|
|
300
|
-
this.
|
|
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
|
-
|
|
306
|
-
|
|
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.
|
|
315
|
+
this.submitTabFocus = this.submitTabFocus === "submit" ? "cancel" : "submit";
|
|
315
316
|
this.invalidate();
|
|
316
317
|
this.tui.requestRender();
|
|
317
318
|
return;
|
package/src/question-view.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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 ? "" : " ·
|
|
312
|
+
const tabHint = isSingle ? "" : " · ←/→ switch tabs";
|
|
309
313
|
const actionHint = onOther
|
|
310
314
|
? "Enter open editor"
|
|
311
315
|
: q.multiSelect
|
package/src/submit-view.ts
CHANGED
|
@@ -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 ·
|
|
97
|
+
add(t.fg("dim", " ←/→ navigate · Tab toggle Submit/Cancel · Enter confirm · Esc back"));
|
|
98
98
|
|
|
99
99
|
return lines;
|
|
100
100
|
}
|