@selesai/code 0.5.26 → 0.5.28

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.
@@ -1144,6 +1144,42 @@ test("terminal split keyboard scroll supports Pi page aliases and preserves app
1144
1144
  compositor.dispose();
1145
1145
  });
1146
1146
 
1147
+ test("terminal split leaves PgUp/PgDn to a focused custom view", () => {
1148
+ const terminal = new FakeTerminal();
1149
+ let ownsKeyboardScroll = true;
1150
+ let inputListener: ((data: string) => { consume?: boolean; data?: string } | undefined) | null = null;
1151
+ const tui = {
1152
+ terminal,
1153
+ addInputListener(listener: (data: string) => { consume?: boolean; data?: string } | undefined) {
1154
+ inputListener = listener;
1155
+ return () => {
1156
+ inputListener = null;
1157
+ };
1158
+ },
1159
+ requestRender() {},
1160
+ render() {
1161
+ return Array.from({ length: 30 }, (_, index) => `line-${index}`);
1162
+ },
1163
+ };
1164
+
1165
+ const compositor = new TerminalSplitCompositor({
1166
+ tui,
1167
+ terminal,
1168
+ shouldCaptureKeyboardScroll: () => ownsKeyboardScroll,
1169
+ renderCluster: () => ({ lines: ["cluster-a", "cluster-b"], cursor: null }),
1170
+ });
1171
+
1172
+ compositor.install();
1173
+ tui.render();
1174
+
1175
+ assert.deepEqual(inputListener?.("\x1b[5~"), { consume: true });
1176
+ ownsKeyboardScroll = false;
1177
+ assert.equal(inputListener?.("\x1b[5~"), undefined);
1178
+ assert.equal(inputListener?.("\x1b[6~"), undefined);
1179
+
1180
+ compositor.dispose();
1181
+ });
1182
+
1147
1183
  test("terminal split keyboard scroll accepts configured shortcuts", () => {
1148
1184
  const terminal = new FakeTerminal();
1149
1185
  const renderRequests: Array<boolean | undefined> = [];
@@ -92,6 +92,7 @@ class QuestionComponent extends Container implements Component {
92
92
  private theme: Theme,
93
93
  private keybindings: KeybindingsManager,
94
94
  private onCancel: () => void,
95
+ private onCommit: () => void,
95
96
  initialResponse?: QuestionResponse,
96
97
  ) {
97
98
  super();
@@ -177,6 +178,7 @@ class QuestionComponent extends Container implements Component {
177
178
  if (!multi) this.otherDraft = "";
178
179
  this.updateAnswerText();
179
180
  this.tui.requestRender();
181
+ if (this.getResponse()) this.onCommit();
180
182
  };
181
183
  list.onEnterFreeform = () => this.showOther();
182
184
  list.onCancel = this.onCancel;
@@ -195,6 +197,7 @@ class QuestionComponent extends Container implements Component {
195
197
  editor.setText(text.trim());
196
198
  this.updateAnswerText();
197
199
  this.tui.requestRender();
200
+ if (this.getResponse()) this.onCommit();
198
201
  };
199
202
  this.editor = editor;
200
203
  return editor;
@@ -430,7 +433,7 @@ class BatchQuestionComponent extends Container implements Component {
430
433
  } else {
431
434
  const question = this.questions[this.currentPage]!;
432
435
  const state = this.states.get(question.id);
433
- this.questionComponent = new QuestionComponent(question, this.tui, this.theme, this.keybindings, this.cancel, state?.response);
436
+ this.questionComponent = new QuestionComponent(question, this.tui, this.theme, this.keybindings, this.cancel, this.commitCurrent, state?.response);
434
437
  this.questionComponent.focused = this.focusedState;
435
438
  this.content.addChild(this.questionComponent);
436
439
  }
@@ -452,6 +455,16 @@ class BatchQuestionComponent extends Container implements Component {
452
455
  this.showPage();
453
456
  }
454
457
 
458
+ private commitCurrent = (): void => {
459
+ this.saveCurrent(true);
460
+ if (this.questions.length === 1) {
461
+ this.onDone({ status: "submitted", answers: buildQuestionAnswers(this.questions, this.states) });
462
+ return;
463
+ }
464
+ if (this.currentPage < this.questions.length) this.currentPage++;
465
+ this.showPage();
466
+ };
467
+
455
468
  private goPrevious(): void {
456
469
  this.saveCurrent(false);
457
470
  if (this.currentPage > 0) this.currentPage--;
@@ -52,15 +52,13 @@ test("wizard submits typed selections and text without review comments", async (
52
52
  { id: "note", type: "text", question: "Explain" },
53
53
  ],
54
54
  });
55
- component().handleInput(ENTER);
56
- component().handleInput(TAB);
55
+ component().handleInput(ENTER); // choice commits and advances
57
56
  component().handleInput(" ");
58
57
  component().handleInput(DOWN);
59
58
  component().handleInput(" ");
60
- component().handleInput(TAB);
59
+ component().handleInput(ENTER); // multi-select commits and advances
61
60
  type(component(), "details");
62
- component().handleInput(ESC);
63
- component().handleInput(TAB);
61
+ component().handleInput(ENTER); // text commits and advances to review
64
62
  assert.doesNotMatch(component().render(120).join("\n"), /Optional comment/);
65
63
  component().handleInput(ENTER);
66
64
 
@@ -85,14 +83,19 @@ test("wizard preserves skipped, Other, and cancelled outcomes", async () => {
85
83
  other.component().handleInput(DOWN);
86
84
  other.component().handleInput(ENTER);
87
85
  type(other.component(), "custom");
88
- other.component().handleInput(ESC);
89
- other.component().handleInput(TAB);
90
- other.component().handleInput(ENTER);
86
+ other.component().handleInput(ENTER); // one answered question submits immediately
91
87
  assert.deepEqual((await other.execution).details, {
92
88
  status: "submitted",
93
89
  answers: [{ id: "q1", status: "answered", response: { kind: "selection", values: [], otherText: "custom" } }],
94
90
  });
95
91
 
92
+ const selected = await runWizard({ questions: [{ type: "select", question: "Select", options: [{ value: "x", label: "X" }] }] });
93
+ selected.component().handleInput(ENTER);
94
+ assert.deepEqual((await selected.execution).details, {
95
+ status: "submitted",
96
+ answers: [{ id: "q1", status: "answered", response: { kind: "selection", values: ["x"] } }],
97
+ });
98
+
96
99
  const cancelled = await runWizard({ questions: [{ type: "select", question: "Cancel", options: [{ value: "x", label: "X" }] }] });
97
100
  cancelled.component().handleInput(ESC);
98
101
  assert.deepEqual((await cancelled.execution).details, { status: "cancelled", reason: "user" });
@@ -8,7 +8,9 @@ Interview the user relentlessly until you reach a shared understanding. Map this
8
8
 
9
9
  Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled — the questions you can ask *now* without guessing at answers you haven't heard yet.
10
10
 
11
- For every non-empty frontier, call the `question` tool **exactly once** with its `questions` array; never print frontier questions as plain chat text. Give every item a stable `id`, short `label`, concise `question`, and `context` containing the recommended answer and why. Provide options when known and set `allowFreeform: true` when the user may need another answer. The batch UI lets the user revisit pages before pressing Enter on its final review page to submit all answers. Wait for that tool result, map returned `answers` by `id`, then recompute the next frontier.
11
+ Before asking a frontier, normalize it by intent. Remove semantic duplicates and merge items that ask for the same decision into one concise parent question; when compatible, combine their choices into one `select` or `multiselect`. Different wording, examples, or context do not make a new decision. Keep questions separate only when the user could reasonably answer them differently and each answer must be recorded independently.
12
+
13
+ For every normalized, non-empty frontier, call the `question` tool **exactly once** with its `questions` array; never print frontier questions as plain chat text. Give every item a stable `id`, short `label`, one-sentence `question`, and `context` containing the recommended answer and why in at most two short sentences. Do not restate the question or option labels in `context`. Provide options when known and set `allowOther: true` when the user may need another answer. The batch UI lets the user revisit pages before pressing Enter on its final review page to submit all answers. Wait for that tool result, map returned `answers` by `id`, then recompute the next frontier.
12
14
 
13
15
  Each round the user answers reshapes the tree — settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a *later* round, not this one.
14
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selesai/code",
3
- "version": "0.5.26",
3
+ "version": "0.5.28",
4
4
  "description": "Maintained, extension-first Pi coding agent with built-in workflows, subagents, web research, questions, skills, and an enhanced terminal UI.",
5
5
  "type": "module",
6
6
  "repository": {