@selesai/code 0.5.25 → 0.5.27

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.
@@ -21,6 +21,7 @@ interface TerminalSplitCompositorOptions {
21
21
  getShowHardwareCursor?: () => boolean;
22
22
  mouseScroll?: boolean;
23
23
  keyboardScrollShortcuts?: KeyboardScrollShortcuts;
24
+ shouldCaptureKeyboardScroll?: () => boolean;
24
25
  onCopySelection?: (text: string) => void;
25
26
  }
26
27
 
@@ -318,6 +319,7 @@ export class TerminalSplitCompositor {
318
319
  private readonly getShowHardwareCursor: () => boolean;
319
320
  private readonly mouseScroll: boolean;
320
321
  private readonly keyboardScrollShortcuts: KeyboardScrollShortcuts;
322
+ private readonly shouldCaptureKeyboardScroll: () => boolean;
321
323
  private readonly onCopySelection: ((text: string) => void) | null;
322
324
  private extendedKeyboardMode: ExtendedKeyboardMode | null = null;
323
325
  private readonly rowsDescriptor: PropertyDescriptor | undefined;
@@ -360,6 +362,7 @@ export class TerminalSplitCompositor {
360
362
  this.getShowHardwareCursor = options.getShowHardwareCursor ?? (() => false);
361
363
  this.mouseScroll = options.mouseScroll !== false;
362
364
  this.keyboardScrollShortcuts = options.keyboardScrollShortcuts ?? DEFAULT_KEYBOARD_SCROLL_SHORTCUTS;
365
+ this.shouldCaptureKeyboardScroll = options.shouldCaptureKeyboardScroll ?? (() => true);
363
366
  this.onCopySelection = options.onCopySelection ?? null;
364
367
  this.rowsDescriptor = descriptorForRows(options.terminal);
365
368
  this.originalWrite = options.terminal.write.bind(options.terminal);
@@ -602,7 +605,7 @@ export class TerminalSplitCompositor {
602
605
  }
603
606
 
604
607
  private handleInput(data: string): { consume?: boolean; data?: string } | undefined {
605
- if (this.disposed || this.hasVisibleOverlay()) return undefined;
608
+ if (this.disposed || this.hasVisibleOverlay() || !this.shouldCaptureKeyboardScroll()) return undefined;
606
609
 
607
610
  const mousePackets = this.mouseScroll ? parseSgrMousePackets(data) : null;
608
611
  if (mousePackets) {
@@ -2386,6 +2386,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
2386
2386
  up: resolvedShortcuts.scrollChatUp,
2387
2387
  down: resolvedShortcuts.scrollChatDown,
2388
2388
  },
2389
+ shouldCaptureKeyboardScroll: () => Reflect.get(tui, "focusedComponent") === currentEditor,
2389
2390
  onCopySelection: (text) => copyTextToClipboard(ctx, text),
2390
2391
  getShowHardwareCursor: () => typeof tui.getShowHardwareCursor === "function" && tui.getShowHardwareCursor(),
2391
2392
  renderCluster: (width, terminalRows) => {
@@ -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> = [];
@@ -299,8 +299,8 @@ async function resumeController(
299
299
  }
300
300
  const ls = controller.loopState;
301
301
  continueAgent(pi, ctx, ls.stage === "reviewing"
302
- ? `Resume the loop at review round ${ls.reviewRound + 1}: call the subagent tool now with { agent: "commentator", task: "..." } to review the builder's uncommitted diff against the original goal and parent conversation context. End the review with WORKFLOW_REVIEW_STATUS: clean or WORKFLOW_REVIEW_STATUS: blocking.`
303
- : `Resume the loop at review round ${ls.reviewRound}: call the subagent tool now with { agent: "builder", task: "..." } to address the feedback in ${sm.snapshot.artifactDir}/${ls.reviewPath ?? "loop-review-<round>.md"}.`);
302
+ ? `Resume the loop at review round ${ls.reviewRound + 1}: call the subagent tool now with { agent: "commentator", task: "..." }. Have the commentator read ${sm.snapshot.artifactDir}/handoff.md and inspect the current uncommitted diff against the handoff acceptance criteria. End the review with WORKFLOW_REVIEW_STATUS: clean or WORKFLOW_REVIEW_STATUS: blocking.`
303
+ : `Resume the loop at review round ${ls.reviewRound}: call the subagent tool now with { agent: "builder", task: "..." } to address the feedback in ${sm.snapshot.artifactDir}/${ls.reviewPath ?? "loop-review-<round>.md"}, using ${sm.snapshot.artifactDir}/handoff.md as the source of truth for the goal and acceptance criteria.`);
304
304
  } else {
305
305
  applyControllerEffect(controller, ctx, current);
306
306
  }
@@ -4,24 +4,37 @@ import type {
4
4
  WorkflowConfig,
5
5
  WorkflowModeRegistration,
6
6
  } from "../state-machine.ts";
7
- import { loopCompleteValidator } from "../validators.ts";
7
+ import { handoffValidator, loopCompleteValidator } from "../validators.ts";
8
8
 
9
- // Direct build↔review mode for work whose plan already exists in the parent
10
- // conversation. The engine persists reviewer feedback and completion; no
11
- // planning or handoff artifact is needed.
12
- const phases: Phase[] = ["loop"];
9
+ // Direct build↔review mode with a parent-persisted handoff artifact. The handoff
10
+ // lets fresh builder/commentator subagents work from a self-contained document
11
+ // instead of relying on the parent conversation.
12
+ const phases: Phase[] = ["handoff", "loop"];
13
13
 
14
14
  const prompts: Partial<Record<Phase, (ctx: PromptContext) => string>> = {
15
+ handoff: ({ artifactDir, userPrompt }) =>
16
+ `You are in the HANDOFF phase of a LOOP workflow.
17
+
18
+ Original goal:
19
+ ${userPrompt}
20
+
21
+ Compile a self-contained handoff document so loop-phase sub-agents understand the goal, constraints, acceptance criteria, and relevant workspace context without re-grilling or re-planning.
22
+
23
+ Call the subagent tool with { agent: "recapper", task: "...", output: false } (do NOT pass a model parameter). Point it at the original goal above and instruct it to use the inherited conversation to produce a concise, self-contained handoff for fresh agents. It must return the complete handoff inline; do not tell it to write any artifact.
24
+
25
+ Inspect that result, verify it ends with exactly one machine-readable line on its own:
26
+ WORKFLOW_HANDOFF_STATUS: ready
27
+ Then immediately call write_workflow_artifact with the complete validated handoff as content. Only that parent tool call writes ${artifactDir}/handoff.md and advances the workflow.`,
15
28
  loop: ({ artifactDir, userPrompt, loopMaxIterations }) =>
16
- `You are in the LOOP phase of a LOOP workflow. The user and parent already discussed the plan; do not grill, research, create a plan, or create a handoff artifact.
29
+ `You are in the LOOP phase of a LOOP workflow. The workflow ENGINE owns build→review rounds and persists review feedback.
17
30
 
18
31
  Original goal:
19
32
  ${userPrompt}
20
33
 
21
- The workflow ENGINE owns build→review rounds and persists review feedback. Fresh subagents cannot see this conversation, so synthesize the agreed plan, constraints, acceptance criteria, and relevant workspace context into EVERY delegation prompt.
34
+ Use read to inspect ${artifactDir}/handoff.md. Using that handoff context — not the parent conversation — generate your own delegation prompt.
22
35
 
23
- 1. Call the subagent tool with { agent: "builder", task: "...", output: false } (do NOT pass a model parameter). Direct it to implement the agreed work in the workspace only, run relevant checks, and return an inline completion summary with checks run.
24
- 2. Call the subagent tool with { agent: "commentator", task: "...", output: false }. Give it the same agreed context. Require independent validation: inspect the uncommitted diff, verify agreed acceptance criteria and correctness, run relevant checks where feasible, and report evidence. Its review must end with exactly one machine-readable line:
36
+ 1. Call the subagent tool with { agent: "builder", task: "...", output: false } (do NOT pass a model parameter). Give it the handoff context. Instruct it to implement the agreed work in the workspace only, run relevant checks, and return an inline completion summary with checks run. If a previous blocking review exists (e.g. ${artifactDir}/loop-review-1.md), direct the builder to read the persisted review and address every issue before returning.
37
+ 2. Call the subagent tool with { agent: "commentator", task: "...", output: false }. Give it the handoff context. Require independent validation: inspect the current uncommitted diff, verify the handoff acceptance criteria and correctness, run relevant checks where feasible, and report evidence. Its review must end with exactly one machine-readable line:
25
38
  WORKFLOW_REVIEW_STATUS: clean
26
39
  OR
27
40
  WORKFLOW_REVIEW_STATUS: blocking
@@ -33,10 +46,16 @@ When review is clean, the engine writes ${artifactDir}/loop-complete.md and make
33
46
  const config: WorkflowConfig = {
34
47
  mode: "loop",
35
48
  phases,
36
- phaseArtifacts: { loop: "loop-complete.md" },
49
+ phaseArtifacts: {
50
+ handoff: "handoff.md",
51
+ loop: "loop-complete.md",
52
+ },
37
53
  prompts,
38
54
  skipRules: [],
39
- artifactValidators: { loop: loopCompleteValidator },
55
+ artifactValidators: {
56
+ handoff: handoffValidator,
57
+ loop: loopCompleteValidator,
58
+ },
40
59
  closeValidators: { "loop-complete.md": loopCompleteValidator },
41
60
  closeArtifacts: ["loop-complete.md"],
42
61
  loopMaxIterations: 3,
package/docs/workflows.md CHANGED
@@ -133,11 +133,11 @@ Lifecycle: `plan → reuse → handoff → loop (build ↔ review) → terminal-
133
133
 
134
134
  ### `loop` — direct build/review loop
135
135
 
136
- Use this after the plan was already agreed in the current conversation. It has one engine-owned `loop` phase: builder changes workspace code, commentator independently validates the diff and relevant checks, then blocking feedback returns to the builder (max 3 blocking rounds). A clean review writes `loop-complete.md`, makes the run terminal-ready, and requires explicit completion.
136
+ Use this after the plan was already agreed in the current conversation. It captures the agreed context into a parent-owned `handoff.md` artifact, then runs an engine-owned `loop` phase: builder changes workspace code, commentator independently validates the diff and relevant checks, then blocking feedback returns to the builder (max 3 blocking rounds). A clean review writes `loop-complete.md`, makes the run terminal-ready, and requires explicit completion.
137
137
 
138
- Fresh subagents do not inherit the parent conversation. Before each delegation, the parent synthesizes the agreed plan, constraints, acceptance criteria, and relevant workspace context into the builder and reviewer prompts.
138
+ Fresh subagents do not inherit the parent conversation. The parent forks a `recapper` subagent once to synthesize a concise, self-contained handoff document directly from the inherited conversation. The parent validates the handoff marker and writes `handoff.md` via `write_workflow_artifact`. After that, every builder and commentator call reads `handoff.md` instead of relying on the parent conversation. Persisted `loop-review-N.md` files feed blocking fixes back to the builder.
139
139
 
140
- Lifecycle: `loop (build ↔ review) → terminal-ready → end_workflow({ mode: "loop" })`
140
+ Lifecycle: `handoff → loop (build ↔ review) → terminal-ready → end_workflow({ mode: "loop" })`
141
141
 
142
142
  - `/workflow-loop <goal>` — start a direct build/review run
143
143
  - `/workflow-loop resume` / `/workflow-loop resume <id-or-artifact-dir-or-workflow.json>` — list or resume a run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selesai/code",
3
- "version": "0.5.25",
3
+ "version": "0.5.27",
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": {