poly-weaver 0.11.3 → 0.11.5

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.
Files changed (152) hide show
  1. package/dist/agents/reviewers/handler.d.ts.map +1 -1
  2. package/dist/agents/reviewers/handler.js +6 -1
  3. package/dist/agents/reviewers/handler.js.map +1 -1
  4. package/dist/agents/reviewers/sanity-inject.d.ts +7 -0
  5. package/dist/agents/reviewers/sanity-inject.d.ts.map +1 -0
  6. package/dist/agents/reviewers/sanity-inject.js +22 -0
  7. package/dist/agents/reviewers/sanity-inject.js.map +1 -0
  8. package/dist/auto-update/tui.d.ts +1 -1
  9. package/dist/auto-update/tui.d.ts.map +1 -1
  10. package/dist/auto-update/tui.js +3 -2
  11. package/dist/auto-update/tui.js.map +1 -1
  12. package/dist/cli.js +3 -0
  13. package/dist/cli.js.map +1 -1
  14. package/dist/flow/executor.d.ts +2 -2
  15. package/dist/flow/executor.d.ts.map +1 -1
  16. package/dist/flow/executor.js +30 -7
  17. package/dist/flow/executor.js.map +1 -1
  18. package/dist/flow-editor/tui.d.ts +3 -0
  19. package/dist/flow-editor/tui.d.ts.map +1 -1
  20. package/dist/flow-editor/tui.js +90 -12
  21. package/dist/flow-editor/tui.js.map +1 -1
  22. package/dist/markdown.d.ts.map +1 -1
  23. package/dist/markdown.js +386 -30
  24. package/dist/markdown.js.map +1 -1
  25. package/dist/offscreen/artifact-pane.d.ts +20 -0
  26. package/dist/offscreen/artifact-pane.d.ts.map +1 -0
  27. package/dist/offscreen/artifact-pane.js +72 -0
  28. package/dist/offscreen/artifact-pane.js.map +1 -0
  29. package/dist/offscreen/controller.d.ts +16 -3
  30. package/dist/offscreen/controller.d.ts.map +1 -1
  31. package/dist/offscreen/controller.js +84 -75
  32. package/dist/offscreen/controller.js.map +1 -1
  33. package/dist/offscreen/inquiry-pane.d.ts +14 -0
  34. package/dist/offscreen/inquiry-pane.d.ts.map +1 -0
  35. package/dist/offscreen/inquiry-pane.js +101 -0
  36. package/dist/offscreen/inquiry-pane.js.map +1 -0
  37. package/dist/offscreen/panes.d.ts +14 -0
  38. package/dist/offscreen/panes.d.ts.map +1 -0
  39. package/dist/offscreen/panes.js +2 -0
  40. package/dist/offscreen/panes.js.map +1 -0
  41. package/dist/offscreen/split-inquiry.d.ts +28 -0
  42. package/dist/offscreen/split-inquiry.d.ts.map +1 -0
  43. package/dist/offscreen/split-inquiry.js +88 -0
  44. package/dist/offscreen/split-inquiry.js.map +1 -0
  45. package/dist/orchestrator.d.ts +3 -0
  46. package/dist/orchestrator.d.ts.map +1 -1
  47. package/dist/orchestrator.js +18 -2
  48. package/dist/orchestrator.js.map +1 -1
  49. package/dist/paste-image/attachment-manager.d.ts +20 -0
  50. package/dist/paste-image/attachment-manager.d.ts.map +1 -0
  51. package/dist/paste-image/attachment-manager.js +88 -0
  52. package/dist/paste-image/attachment-manager.js.map +1 -0
  53. package/dist/paste-image/bracketed-paste.d.ts +13 -0
  54. package/dist/paste-image/bracketed-paste.d.ts.map +1 -0
  55. package/dist/paste-image/bracketed-paste.js +82 -0
  56. package/dist/paste-image/bracketed-paste.js.map +1 -0
  57. package/dist/paste-image/clipboard-ffi.d.ts +9 -0
  58. package/dist/paste-image/clipboard-ffi.d.ts.map +1 -0
  59. package/dist/paste-image/clipboard-ffi.js +149 -0
  60. package/dist/paste-image/clipboard-ffi.js.map +1 -0
  61. package/dist/paste-image/clipboard.d.ts +38 -0
  62. package/dist/paste-image/clipboard.d.ts.map +1 -0
  63. package/dist/paste-image/clipboard.js +454 -0
  64. package/dist/paste-image/clipboard.js.map +1 -0
  65. package/dist/paste-image/image-format.d.ts +7 -0
  66. package/dist/paste-image/image-format.d.ts.map +1 -0
  67. package/dist/paste-image/image-format.js +61 -0
  68. package/dist/paste-image/image-format.js.map +1 -0
  69. package/dist/paste-image/intents.d.ts +11 -0
  70. package/dist/paste-image/intents.d.ts.map +1 -0
  71. package/dist/paste-image/intents.js +66 -0
  72. package/dist/paste-image/intents.js.map +1 -0
  73. package/dist/paste-image/persistence.d.ts +2 -0
  74. package/dist/paste-image/persistence.d.ts.map +1 -0
  75. package/dist/paste-image/persistence.js +8 -0
  76. package/dist/paste-image/persistence.js.map +1 -0
  77. package/dist/paste-image/staging.d.ts +8 -0
  78. package/dist/paste-image/staging.d.ts.map +1 -0
  79. package/dist/paste-image/staging.js +23 -0
  80. package/dist/paste-image/staging.js.map +1 -0
  81. package/dist/paste-image/token.d.ts +14 -0
  82. package/dist/paste-image/token.d.ts.map +1 -0
  83. package/dist/paste-image/token.js +26 -0
  84. package/dist/paste-image/token.js.map +1 -0
  85. package/dist/providers/claude/completion-plan-mode.d.ts +3 -2
  86. package/dist/providers/claude/completion-plan-mode.d.ts.map +1 -1
  87. package/dist/providers/claude/completion-plan-mode.js +13 -11
  88. package/dist/providers/claude/completion-plan-mode.js.map +1 -1
  89. package/dist/providers/claude/plan-mode-pty-monitor.d.ts +9 -6
  90. package/dist/providers/claude/plan-mode-pty-monitor.d.ts.map +1 -1
  91. package/dist/providers/claude/plan-mode-pty-monitor.js +15 -6
  92. package/dist/providers/claude/plan-mode-pty-monitor.js.map +1 -1
  93. package/dist/providers/option-catalog.d.ts.map +1 -1
  94. package/dist/providers/option-catalog.js +0 -1
  95. package/dist/providers/option-catalog.js.map +1 -1
  96. package/dist/resume-tui.d.ts +1 -1
  97. package/dist/resume-tui.d.ts.map +1 -1
  98. package/dist/resume-tui.js +3 -1
  99. package/dist/resume-tui.js.map +1 -1
  100. package/dist/session/manifest.d.ts +3 -1
  101. package/dist/session/manifest.d.ts.map +1 -1
  102. package/dist/session/restore.d.ts.map +1 -1
  103. package/dist/session/restore.js +2 -1
  104. package/dist/session/restore.js.map +1 -1
  105. package/dist/session/session-store.d.ts +8 -0
  106. package/dist/session/session-store.d.ts.map +1 -1
  107. package/dist/session/session-store.js +18 -1
  108. package/dist/session/session-store.js.map +1 -1
  109. package/dist/startup-tui.d.ts +8 -2
  110. package/dist/startup-tui.d.ts.map +1 -1
  111. package/dist/startup-tui.js +77 -6
  112. package/dist/startup-tui.js.map +1 -1
  113. package/dist/terminal/input-router.d.ts +9 -0
  114. package/dist/terminal/input-router.d.ts.map +1 -1
  115. package/dist/terminal/input-router.js +14 -0
  116. package/dist/terminal/input-router.js.map +1 -1
  117. package/dist/text-editing/emacs-input.d.ts +11 -0
  118. package/dist/text-editing/emacs-input.d.ts.map +1 -1
  119. package/dist/text-editing/emacs-input.js +254 -17
  120. package/dist/text-editing/emacs-input.js.map +1 -1
  121. package/dist/user/curate-handler.d.ts.map +1 -1
  122. package/dist/user/curate-handler.js +4 -1
  123. package/dist/user/curate-handler.js.map +1 -1
  124. package/dist/user/curate-prompt.d.ts +2 -0
  125. package/dist/user/curate-prompt.d.ts.map +1 -1
  126. package/dist/user/curate-prompt.js.map +1 -1
  127. package/dist/user/feedback-editing.d.ts +28 -0
  128. package/dist/user/feedback-editing.d.ts.map +1 -0
  129. package/dist/user/feedback-editing.js +72 -0
  130. package/dist/user/feedback-editing.js.map +1 -0
  131. package/dist/user/handler.d.ts.map +1 -1
  132. package/dist/user/handler.js +5 -2
  133. package/dist/user/handler.js.map +1 -1
  134. package/dist/user/host-curate-prompt.d.ts +5 -6
  135. package/dist/user/host-curate-prompt.d.ts.map +1 -1
  136. package/dist/user/host-curate-prompt.js +149 -81
  137. package/dist/user/host-curate-prompt.js.map +1 -1
  138. package/dist/user/host-prompt.d.ts +4 -13
  139. package/dist/user/host-prompt.d.ts.map +1 -1
  140. package/dist/user/host-prompt.js +444 -264
  141. package/dist/user/host-prompt.js.map +1 -1
  142. package/dist/user/prompt.d.ts +3 -1
  143. package/dist/user/prompt.d.ts.map +1 -1
  144. package/dist/user/shortcut-hints.d.ts +1 -0
  145. package/dist/user/shortcut-hints.d.ts.map +1 -1
  146. package/dist/user/shortcut-hints.js +1 -1
  147. package/dist/user/shortcut-hints.js.map +1 -1
  148. package/dist/user/split-surface.d.ts +5 -1
  149. package/dist/user/split-surface.d.ts.map +1 -1
  150. package/dist/user/split-surface.js +15 -8
  151. package/dist/user/split-surface.js.map +1 -1
  152. package/package.json +1 -2
@@ -1,32 +1,28 @@
1
- import { bold, dim, green, invert, reset, visibleLength, } from "../ansi.js";
1
+ import { bold, dim, green, reset, visibleLength, yellow, } from "../ansi.js";
2
2
  import { AgentInterruptedError } from "../agents/runner.js";
3
3
  import { EmacsInputParser } from "./prompt.js";
4
- import { clipToWidth } from "./curate-prompt.js";
4
+ import { clipToWidth, wrapToWidth } from "./curate-prompt.js";
5
5
  import { buildPlanPane } from "./plan-pane.js";
6
6
  import { renderShortcutHints } from "./shortcut-hints.js";
7
- import { applyTextBufferEvent, isTextBufferEvent, } from "../text-editing/emacs-input.js";
8
- import { cursorToVisual, renderWrappedLine, softWrapLine } from "../text-editing/soft-wrap.js";
9
- import { OffscreenController } from "../offscreen/controller.js";
10
- /** Maximum visible rows for the multiline feedback editor. */
11
- const FEEDBACK_EDITOR_MAX_ROWS = 5;
7
+ import { aliasSelectionNavEvent, isTextBufferEvent, } from "../text-editing/emacs-input.js";
8
+ import { OffscreenController, } from "../offscreen/controller.js";
9
+ import { AttachmentManager } from "../paste-image/attachment-manager.js";
10
+ import { ALT_V_PASTE_IMAGE_HINT, handleBracketedPaste, handleImagePasteIntent, PASTING_IMAGE_HINT, } from "../paste-image/intents.js";
11
+ import { buildSplitSurface, computeSplitMetrics, SPLIT_COLLAPSE_AT_COLS, } from "./split-surface.js";
12
+ import { applyEditingBufferEvent, applyEditInsertedText, bufferFromText, emptyBuffer, finalizeEditText, renderMultilineEditor, } from "./feedback-editing.js";
13
+ // Max rows allotted to the scrollable feedback-list block (Approve/item/+Add
14
+ // rows only; the separator + hint are fixed chrome rendered outside this
15
+ // budget). This same constant is both the stacked-layout scroll budget and the
16
+ // wide-terminal split trigger.
17
+ export const FEEDBACK_AREA_MAX_ROWS = 10;
12
18
  /** Lines of overlap when paging the plan (PageUp/PageDown). */
13
19
  const PLAN_PAGE_OVERLAP = 1;
14
- const ACTIVE_PREFIX = " ❯";
20
+ const ACTIVE_SELECTOR = "❯";
15
21
  const INACTIVE_PREFIX = " ";
16
22
  /**
17
- * Host-backed user-review prompt with a top-down stack layout.
18
- *
19
- * Plan on top (header rule + scrollable body), full-width dim ─ separator, and
20
- * a controls block (Approve + bounded feedback editor + compact hint row).
21
- *
22
- * Key handling:
23
- * - ↑/↓ toggles between Approve and the editor (does NOT scroll the plan).
24
- * - PageUp/PageDown/Home/End/wheel scrolls the plan.
25
- * - Ctrl+D submits feedback (or accepts when on Approve).
26
- * - Ctrl+C quits poly-weaver (exit code 130). When the feedback editor has
27
- * typed text, the first Ctrl+C clears the buffer instead; a second Ctrl+C
28
- * on an empty buffer quits.
29
- * - Ctrl+] is consumed by the host's `InputRouter` for dump capture.
23
+ * Host-backed user-review prompt. The review controls use the same multi-item
24
+ * feedback model as verdict curation: add, edit, delete, then submit the list
25
+ * as discrete feedback items.
30
26
  */
31
27
  export class HostBackedUserPrompt {
32
28
  host;
@@ -37,20 +33,21 @@ export class HostBackedUserPrompt {
37
33
  if (!this.host.isInteractive) {
38
34
  return { accepted: true };
39
35
  }
36
+ const state = {
37
+ cursor: 1,
38
+ userAdded: [],
39
+ editing: undefined,
40
+ planScrollTop: 0,
41
+ planAtEnd: false,
42
+ };
43
+ const parser = new EmacsInputParser();
40
44
  return new Promise((resolve, reject) => {
41
- const state = {
42
- mode: "input",
43
- lines: [""],
44
- cursorLine: 0,
45
- cursorCol: 0,
46
- feedbackScrollTop: 0,
47
- planScrollTop: 0,
48
- planAtEnd: false,
49
- };
50
- const parser = new EmacsInputParser();
51
45
  let detach;
52
46
  let offscreenController;
47
+ let active = true;
48
+ let settlingEdit = false;
53
49
  const cleanup = () => {
50
+ active = false;
54
51
  offscreenController?.dispose();
55
52
  offscreenController = undefined;
56
53
  detach?.();
@@ -64,291 +61,457 @@ export class HostBackedUserPrompt {
64
61
  cleanup();
65
62
  reject(err);
66
63
  };
67
- const baseMode = {
68
- getSurface: () => {
69
- const layout = this.host.getLayout();
70
- const cols = Math.max(20, layout.columns);
71
- const rows = Math.max(1, layout.childRows);
72
- return buildSurface(plan, context, state, cols, rows);
73
- },
74
- onEvents: (keys) => {
75
- const events = parser.mapKeys(keys);
76
- let dirty = false;
77
- for (const event of events) {
78
- if (event.type === "ctrl-c") {
79
- const bufferEmpty = state.lines.length === 1 && state.lines[0] === "";
80
- if (bufferEmpty) {
81
- failWith(new AgentInterruptedError("Interrupted"));
82
- return true;
83
- }
84
- state.lines = [""];
85
- state.cursorLine = 0;
86
- state.cursorCol = 0;
87
- state.feedbackScrollTop = 0;
88
- state.mode = "input";
64
+ const submitFeedback = () => {
65
+ finish({ accepted: false, feedback: [...state.userAdded] });
66
+ };
67
+ const cancelEdit = () => {
68
+ if (settlingEdit || !state.editing)
69
+ return;
70
+ state.editing = undefined;
71
+ clampCursor(state);
72
+ };
73
+ const applySubmittedEdit = (editing, text) => {
74
+ state.editing = undefined;
75
+ if (editing.target.kind === "user-edit") {
76
+ const index = editing.target.index;
77
+ if (text.length > 0) {
78
+ state.userAdded[index] = text;
79
+ }
80
+ else {
81
+ state.userAdded.splice(index, 1);
82
+ }
83
+ }
84
+ else if (text.length > 0) {
85
+ state.userAdded.push(text);
86
+ }
87
+ state.cursor = addRowCursorIndex(state);
88
+ clampCursor(state);
89
+ };
90
+ const submitEdit = () => {
91
+ const editing = state.editing;
92
+ if (!editing || settlingEdit)
93
+ return;
94
+ const text = editing.buffer.lines.join("\n").trim();
95
+ if (editing.attachments.snapshot().length === 0) {
96
+ applySubmittedEdit(editing, text);
97
+ return;
98
+ }
99
+ settlingEdit = true;
100
+ void finalizeEditText(editing, text, context.persistentDir).then((finalText) => {
101
+ if (!active || state.editing !== editing)
102
+ return;
103
+ applySubmittedEdit(editing, finalText);
104
+ settlingEdit = false;
105
+ this.host.scheduleParentUiRender();
106
+ }).catch((err) => {
107
+ settlingEdit = false;
108
+ failWith(err instanceof Error ? err : new Error(String(err)));
109
+ });
110
+ };
111
+ const openUserAddEditor = () => {
112
+ state.cursor = addRowCursorIndex(state);
113
+ state.editing = {
114
+ target: { kind: "user-add" },
115
+ buffer: emptyBuffer(),
116
+ attachments: new AttachmentManager(),
117
+ transientHint: null,
118
+ };
119
+ };
120
+ const openUserEditEditor = (index) => {
121
+ state.cursor = index;
122
+ state.editing = {
123
+ target: { kind: "user-edit", index },
124
+ buffer: bufferFromText(state.userAdded[index] ?? ""),
125
+ attachments: new AttachmentManager(),
126
+ transientHint: null,
127
+ };
128
+ };
129
+ const applyEditPasteOutcome = (editing, outcome) => {
130
+ if (!active || settlingEdit || state.editing !== editing)
131
+ return;
132
+ editing.transientHint = outcome.hint ?? null;
133
+ if (outcome.inserted !== undefined)
134
+ applyEditInsertedText(editing, outcome.inserted);
135
+ this.host.scheduleParentUiRender();
136
+ };
137
+ const launchEditPasteIntent = (editing, task) => {
138
+ void task.then((outcome) => applyEditPasteOutcome(editing, outcome)).catch((err) => {
139
+ if (!active || settlingEdit || state.editing !== editing)
140
+ return;
141
+ editing.transientHint = err instanceof Error ? err.message : String(err);
142
+ this.host.scheduleParentUiRender();
143
+ });
144
+ };
145
+ const processEvents = (events) => {
146
+ let dirty = false;
147
+ let finished = false;
148
+ for (const rawEvent of events) {
149
+ if (finished)
150
+ break;
151
+ const event = state.editing
152
+ ? rawEvent
153
+ : aliasSelectionNavEvent(rawEvent);
154
+ if (state.editing) {
155
+ if (settlingEdit) {
89
156
  dirty = true;
90
157
  continue;
91
158
  }
92
- if (event.type === "ctrl-d") {
93
- if (state.mode === "approve") {
94
- finish({ accepted: true });
95
- return true;
96
- }
97
- const text = state.lines.join("\n").trim();
98
- if (text.length > 0) {
99
- finish({ accepted: false, comments: text });
100
- return true;
101
- }
159
+ if (state.editing.transientHint) {
160
+ state.editing.transientHint = null;
161
+ dirty = true;
162
+ }
163
+ if (event.type === "paste") {
164
+ const editing = state.editing;
165
+ launchEditPasteIntent(editing, handleBracketedPaste(event.content, editing.attachments));
102
166
  continue;
103
167
  }
104
- if (event.type === "enter") {
105
- if (state.mode === "approve") {
106
- finish({ accepted: true });
107
- return true;
108
- }
109
- dirty = applyFeedbackTextBufferEvent(state, { type: "newline" }) || dirty;
168
+ if (event.type === "image-paste") {
169
+ const editing = state.editing;
170
+ launchEditPasteIntent(editing, handleImagePasteIntent(editing.attachments, () => {
171
+ if (!active || settlingEdit || state.editing !== editing)
172
+ return;
173
+ editing.transientHint = PASTING_IMAGE_HINT;
174
+ this.host.scheduleParentUiRender();
175
+ }));
176
+ continue;
177
+ }
178
+ if (event.type === "ctrl-c" || event.type === "escape") {
179
+ cancelEdit();
180
+ dirty = true;
181
+ continue;
182
+ }
183
+ if (event.type === "ctrl-d" || event.type === "enter") {
184
+ submitEdit();
185
+ dirty = true;
110
186
  continue;
111
187
  }
112
188
  if (event.type === "ctrl-j") {
113
- if (state.mode === "input") {
114
- dirty = applyFeedbackTextBufferEvent(state, { type: "newline" }) || dirty;
115
- }
189
+ dirty = applyEditingBufferEvent(state.editing, { type: "newline" }) || dirty;
116
190
  continue;
117
191
  }
118
192
  if (event.type === "arrow") {
119
- if (event.direction === "up" && state.mode === "input") {
120
- state.mode = "approve";
121
- dirty = true;
122
- }
123
- else if (event.direction === "down" && state.mode === "approve") {
124
- state.mode = "input";
125
- dirty = true;
126
- }
127
- else if (state.mode === "input" &&
128
- (event.direction === "left" || event.direction === "right")) {
129
- dirty = applyFeedbackTextBufferEvent(state, {
193
+ if (event.direction === "left" || event.direction === "right") {
194
+ dirty = applyEditingBufferEvent(state.editing, {
130
195
  type: event.direction === "left" ? "ctrl-b" : "ctrl-f",
131
196
  }) || dirty;
132
197
  }
133
198
  continue;
134
199
  }
135
- if (event.type === "scroll-page") {
136
- const { planHeight, planLines } = planMetrics(plan, context, state, this.host);
137
- const step = Math.max(1, planHeight - PLAN_PAGE_OVERLAP);
138
- const max = Math.max(0, planLines - planHeight);
139
- if (event.direction === "up") {
140
- state.planScrollTop = Math.max(0, state.planScrollTop - step);
141
- }
142
- else {
143
- state.planScrollTop = Math.min(max, state.planScrollTop + step);
144
- }
145
- state.planAtEnd = state.planScrollTop >= max;
146
- dirty = true;
200
+ if (isTextBufferEvent(event)) {
201
+ dirty = applyEditingBufferEvent(state.editing, event) || dirty;
147
202
  continue;
148
203
  }
149
- if (event.type === "scroll-home") {
150
- state.planScrollTop = 0;
151
- state.planAtEnd = false;
204
+ continue;
205
+ }
206
+ if (event.type === "ctrl-c") {
207
+ failWith(new AgentInterruptedError("Interrupted"));
208
+ finished = true;
209
+ continue;
210
+ }
211
+ if (event.type === "ctrl-d") {
212
+ if (state.userAdded.length === 0) {
213
+ finish({ accepted: true });
214
+ }
215
+ else {
216
+ submitFeedback();
217
+ }
218
+ finished = true;
219
+ continue;
220
+ }
221
+ if (event.type === "arrow") {
222
+ if (event.direction === "up" || event.direction === "down") {
223
+ const total = navigableRows(state).length;
224
+ state.cursor = event.direction === "up"
225
+ ? (state.cursor - 1 + total) % total
226
+ : (state.cursor + 1) % total;
227
+ dirty = true;
228
+ }
229
+ continue;
230
+ }
231
+ if (event.type === "scroll-page") {
232
+ const { planHeight, planLines } = planMetrics(plan, context, state, this.host);
233
+ const step = Math.max(1, planHeight - PLAN_PAGE_OVERLAP);
234
+ const max = Math.max(0, planLines - planHeight);
235
+ if (event.direction === "up") {
236
+ state.planScrollTop = Math.max(0, state.planScrollTop - step);
237
+ }
238
+ else {
239
+ state.planScrollTop = Math.min(max, state.planScrollTop + step);
240
+ }
241
+ state.planAtEnd = state.planScrollTop >= max;
242
+ dirty = true;
243
+ continue;
244
+ }
245
+ if (event.type === "scroll-home") {
246
+ state.planScrollTop = 0;
247
+ state.planAtEnd = false;
248
+ dirty = true;
249
+ continue;
250
+ }
251
+ if (event.type === "scroll-end") {
252
+ const { planHeight, planLines } = planMetrics(plan, context, state, this.host);
253
+ state.planScrollTop = Math.max(0, planLines - planHeight);
254
+ state.planAtEnd = true;
255
+ dirty = true;
256
+ continue;
257
+ }
258
+ if (event.type === "scroll-lines") {
259
+ const { planHeight, planLines } = planMetrics(plan, context, state, this.host);
260
+ const max = Math.max(0, planLines - planHeight);
261
+ state.planScrollTop = Math.min(max, Math.max(0, state.planScrollTop + event.delta));
262
+ state.planAtEnd = state.planScrollTop >= max;
263
+ dirty = true;
264
+ continue;
265
+ }
266
+ if (event.type === "enter") {
267
+ const row = currentRow(state);
268
+ if (row.kind === "approve") {
269
+ finish({ accepted: true });
270
+ finished = true;
271
+ }
272
+ else if (row.kind === "item") {
273
+ openUserEditEditor(row.index);
152
274
  dirty = true;
153
- continue;
154
275
  }
155
- if (event.type === "scroll-end") {
156
- const { planHeight, planLines } = planMetrics(plan, context, state, this.host);
157
- state.planScrollTop = Math.max(0, planLines - planHeight);
158
- state.planAtEnd = true;
276
+ else {
277
+ openUserAddEditor();
159
278
  dirty = true;
160
- continue;
161
279
  }
162
- if (event.type === "scroll-lines") {
163
- const { planHeight, planLines } = planMetrics(plan, context, state, this.host);
164
- const max = Math.max(0, planLines - planHeight);
165
- state.planScrollTop = Math.min(max, Math.max(0, state.planScrollTop + event.delta));
166
- state.planAtEnd = state.planScrollTop >= max;
280
+ continue;
281
+ }
282
+ if (event.type === "insert") {
283
+ const ch = event.text.toLowerCase();
284
+ if (ch === "a") {
285
+ openUserAddEditor();
167
286
  dirty = true;
168
287
  continue;
169
288
  }
170
- if (isTextBufferEvent(event)) {
171
- if (state.mode === "approve") {
172
- if (event.type === "insert") {
173
- state.mode = "input";
174
- applyFeedbackTextBufferEvent(state, event);
175
- dirty = true;
176
- }
177
- else if (event.type === "backspace") {
178
- state.mode = "input";
179
- dirty = true;
180
- }
181
- continue;
289
+ if (ch === "d") {
290
+ const row = currentRow(state);
291
+ if (row.kind === "item") {
292
+ state.userAdded.splice(row.index, 1);
293
+ clampCursor(state);
294
+ state.cursor = addRowCursorIndex(state);
295
+ dirty = true;
182
296
  }
183
- dirty = applyFeedbackTextBufferEvent(state, event) || dirty;
184
- continue;
185
297
  }
186
- // tab/escape/dump ignored — dump is consumed by the host router.
187
298
  }
188
- if (dirty)
189
- this.host.scheduleParentUiRender();
299
+ }
300
+ if (dirty && !finished)
301
+ this.host.scheduleParentUiRender();
302
+ return finished;
303
+ };
304
+ const baseMode = {
305
+ getSurface: () => {
306
+ const layout = this.host.getLayout();
307
+ const cols = Math.max(20, layout.columns);
308
+ const rows = Math.max(1, layout.childRows);
309
+ return buildSurface(plan, context, state, cols, rows);
310
+ },
311
+ onEvents: (keys) => {
312
+ processEvents(parser.mapKeys(keys));
190
313
  return true;
191
314
  },
192
315
  };
193
316
  const mode = context.offscreen
194
- ? (offscreenController = new OffscreenController(this.host, context.offscreen.workdir, context.offscreen.slots)).wrap(baseMode)
317
+ ? (offscreenController = new OffscreenController(this.host, context.offscreen.workdir, context.offscreen.slots, {
318
+ primary: reviewArtifactPaneConfig(plan, context),
319
+ })).wrap(baseMode)
195
320
  : baseMode;
196
321
  detach = this.host.attachParentUi(mode);
197
322
  });
198
323
  }
199
324
  }
200
- function normalizeFeedbackCursor(state) {
201
- if (state.lines.length === 0)
202
- state.lines = [""];
203
- state.cursorLine = Math.max(0, Math.min(state.cursorLine, state.lines.length - 1));
204
- state.cursorCol = Math.max(0, Math.min(state.cursorCol, state.lines[state.cursorLine].length));
325
+ function navigableRows(state) {
326
+ if (state.userAdded.length === 0) {
327
+ return [{ kind: "approve" }, { kind: "add" }];
328
+ }
329
+ return [
330
+ ...state.userAdded.map((_, index) => ({ kind: "item", index })),
331
+ { kind: "add" },
332
+ ];
205
333
  }
206
- function applyFeedbackTextBufferEvent(state, event) {
207
- normalizeFeedbackCursor(state);
208
- const buffer = {
209
- lines: state.lines,
210
- cursorRow: state.cursorLine,
211
- cursorCol: state.cursorCol,
212
- };
213
- const changed = applyTextBufferEvent(buffer, event);
214
- state.lines = buffer.lines;
215
- state.cursorLine = buffer.cursorRow;
216
- state.cursorCol = buffer.cursorCol;
217
- normalizeFeedbackCursor(state);
218
- return changed;
334
+ function currentRow(state) {
335
+ clampCursor(state);
336
+ return navigableRows(state)[state.cursor] ?? { kind: "approve" };
219
337
  }
220
- /**
221
- * Build the natural-height controls block (no padding):
222
- * ❯ Approve (Enter) — when state.mode === "approve"
223
- * _ — feedback editor (1..5 rows; placeholder when empty)
224
- * Ctrl+D Submit — hint, only when input mode and editor non-empty
225
- *
226
- * Returns its natural height (1..7 rows). Cursor is pane-local.
227
- */
228
- function buildControlsBlock(state, width, offscreenHint) {
229
- const activePrefixWidth = visibleLength(ACTIVE_PREFIX);
230
- const inactivePrefixWidth = visibleLength(INACTIVE_PREFIX);
231
- const bodyWidth = Math.max(1, width - inactivePrefixWidth);
232
- const approveRow = state.mode === "approve"
233
- ? `${ACTIVE_PREFIX}${bold}${green}Approve (Enter)${reset}`
234
- : `${INACTIVE_PREFIX}${dim}Approve${reset}`;
235
- const showPlaceholder = state.lines.length === 1 && state.lines[0].length === 0;
236
- const hasFeedback = state.lines.some((line) => line.length > 0);
237
- const visualRows = [];
238
- let cursorVisualRow = 0;
239
- let cursorVisCol = 0;
240
- if (!showPlaceholder) {
241
- const cursorLine = Math.min(Math.max(0, state.cursorLine), state.lines.length - 1);
242
- for (let logIdx = 0; logIdx < state.lines.length; logIdx++) {
243
- const line = state.lines[logIdx] ?? "";
244
- const segments = softWrapLine(line, bodyWidth);
245
- if (logIdx < cursorLine) {
246
- cursorVisualRow += segments.length;
247
- }
248
- else if (logIdx === cursorLine) {
249
- const cursor = cursorToVisual(segments, state.cursorCol, line.length, bodyWidth);
250
- cursorVisualRow += cursor.subRow;
251
- cursorVisCol = cursor.visCol;
252
- }
253
- const rendered = renderWrappedLine(line, bodyWidth, state.mode === "input" && logIdx === cursorLine ? state.cursorCol : null);
254
- for (let subRow = 0; subRow < rendered.length; subRow++) {
255
- visualRows.push({ logIdx, subRow, body: rendered[subRow] });
256
- }
257
- }
258
- }
259
- const unboundedEditor = showPlaceholder ? 1 : visualRows.length;
260
- const editorHeight = Math.min(FEEDBACK_EDITOR_MAX_ROWS, Math.max(1, unboundedEditor));
261
- if (showPlaceholder) {
262
- state.feedbackScrollTop = 0;
338
+ function addRowCursorIndex(state) {
339
+ return state.userAdded.length === 0 ? 1 : state.userAdded.length;
340
+ }
341
+ function clampCursor(state) {
342
+ const max = Math.max(0, navigableRows(state).length - 1);
343
+ state.cursor = Math.max(0, Math.min(state.cursor, max));
344
+ }
345
+ // Natural (unbounded) height of the feedback-list block at the given render
346
+ // width - the dim/active Approve row + every item row (incl. inline editor) +
347
+ // the +Add row. Excludes the separator/hint chrome. Used by `planMetrics` to
348
+ // size the plan area on scroll events.
349
+ function measureFeedbackListRows(state, width) {
350
+ return buildFeedbackListRows(state, width).lines.length;
351
+ }
352
+ function buildFeedbackListRows(state, width) {
353
+ clampCursor(state);
354
+ const lines = [];
355
+ const rowStarts = [];
356
+ const rowEnds = [];
357
+ let editorRow;
358
+ let editorCol = 0;
359
+ if (state.userAdded.length === 0) {
360
+ const active = state.cursor === 0;
361
+ rowStarts[0] = lines.length;
362
+ lines.push(clipToWidth(active
363
+ ? `${ACTIVE_SELECTOR}${bold}${green}✓ Approve (Enter)${reset}`
364
+ : ` ${dim}✓ Approve${reset}`, width));
365
+ rowEnds[0] = lines.length;
263
366
  }
264
367
  else {
265
- if (cursorVisualRow < state.feedbackScrollTop) {
266
- state.feedbackScrollTop = cursorVisualRow;
368
+ lines.push(clipToWidth(` ${dim}✓ Approve${reset}`, width));
369
+ }
370
+ for (let i = 0; i < state.userAdded.length; i++) {
371
+ const isCursor = state.cursor === i;
372
+ const leadChar = isCursor ? ACTIVE_SELECTOR : " ";
373
+ const head = `${leadChar} ${i + 1}. `;
374
+ const headWidth = visibleLength(head);
375
+ const indent = " ".repeat(headWidth);
376
+ const avail = Math.max(1, width - headWidth);
377
+ rowStarts[i] = lines.length;
378
+ if (state.editing?.target.kind === "user-edit" &&
379
+ state.editing.target.index === i) {
380
+ const renderedEditor = renderMultilineEditor(state.editing.buffer, width, head, indent);
381
+ editorRow = lines.length + renderedEditor.cursorRowOffset;
382
+ editorCol = renderedEditor.cursorCol;
383
+ lines.push(...renderedEditor.lines);
267
384
  }
268
- else if (cursorVisualRow >= state.feedbackScrollTop + editorHeight) {
269
- state.feedbackScrollTop = cursorVisualRow - editorHeight + 1;
385
+ else {
386
+ const wrapped = wrapToWidth(state.userAdded[i] ?? "", avail);
387
+ lines.push(clipToWidth(`${head}${wrapped[0] ?? ""}`, width));
388
+ for (let j = 1; j < wrapped.length; j++) {
389
+ lines.push(clipToWidth(`${indent}${wrapped[j]}`, width));
390
+ }
270
391
  }
271
- const maxScroll = Math.max(0, visualRows.length - editorHeight);
272
- if (state.feedbackScrollTop > maxScroll)
273
- state.feedbackScrollTop = maxScroll;
274
- if (state.feedbackScrollTop < 0)
275
- state.feedbackScrollTop = 0;
392
+ rowEnds[i] = lines.length;
276
393
  }
277
- const placeholder = "Type feedback";
278
- const editorBody = [];
279
- if (showPlaceholder) {
280
- let first;
281
- if (state.mode === "input") {
282
- const head = placeholder[0] ?? " ";
283
- const tail = placeholder.slice(1);
284
- first = `${ACTIVE_PREFIX}${dim}${invert}${head}${reset}${dim}${tail}${reset}`;
394
+ {
395
+ const rowIndex = addRowCursorIndex(state);
396
+ const isCursor = state.cursor === rowIndex;
397
+ rowStarts[rowIndex] = lines.length;
398
+ if (state.editing?.target.kind === "user-add") {
399
+ const head = `${ACTIVE_SELECTOR} ${state.userAdded.length + 1}. `;
400
+ const headWidth = visibleLength(head);
401
+ const renderedEditor = renderMultilineEditor(state.editing.buffer, width, head, " ".repeat(headWidth));
402
+ editorRow = lines.length + renderedEditor.cursorRowOffset;
403
+ editorCol = renderedEditor.cursorCol;
404
+ lines.push(...renderedEditor.lines);
285
405
  }
286
406
  else {
287
- first = `${INACTIVE_PREFIX}${dim}${placeholder}${reset}`;
407
+ lines.push(clipToWidth(isCursor
408
+ ? `${ACTIVE_SELECTOR}${bold}${green}+ Add feedback (Enter)${reset}`
409
+ : ` ${dim}+ Add feedback${reset}`, width));
288
410
  }
289
- editorBody.push(first);
290
- while (editorBody.length < editorHeight)
291
- editorBody.push("");
411
+ rowEnds[rowIndex] = lines.length;
292
412
  }
293
- else {
294
- for (let i = 0; i < editorHeight; i++) {
295
- const visual = visualRows[state.feedbackScrollTop + i];
296
- if (!visual) {
297
- editorBody.push("");
298
- continue;
413
+ return {
414
+ lines,
415
+ rowStarts,
416
+ rowEnds,
417
+ editorRow,
418
+ editorCol,
419
+ };
420
+ }
421
+ function buildFeedbackPane(state, rendered, width, listRows, hintTokens) {
422
+ const scrollRows = Math.max(0, listRows);
423
+ const activeStart = rendered.rowStarts[state.cursor] ?? 0;
424
+ const activeEndExclusive = rendered.rowEnds[state.cursor] ?? rendered.lines.length;
425
+ let scrollTop = 0;
426
+ const total = rendered.lines.length;
427
+ if (scrollRows > 0 && total > scrollRows) {
428
+ scrollTop = activeStart;
429
+ if (activeEndExclusive - scrollTop < scrollRows) {
430
+ scrollTop = Math.max(0, total - scrollRows);
431
+ if (scrollTop > activeStart)
432
+ scrollTop = activeStart;
433
+ }
434
+ if (rendered.editorRow !== undefined) {
435
+ if (rendered.editorRow < scrollTop)
436
+ scrollTop = rendered.editorRow;
437
+ if (rendered.editorRow >= scrollTop + scrollRows) {
438
+ scrollTop = rendered.editorRow - scrollRows + 1;
299
439
  }
300
- const isFirstUnderlying = visual.logIdx === 0 && visual.subRow === 0;
301
- const prefix = isFirstUnderlying && state.mode === "input"
302
- ? ACTIVE_PREFIX
303
- : INACTIVE_PREFIX;
304
- editorBody.push(`${prefix}${visual.body}`);
305
440
  }
441
+ if (scrollTop < 0)
442
+ scrollTop = 0;
443
+ if (scrollTop + scrollRows > total)
444
+ scrollTop = total - scrollRows;
306
445
  }
307
- const tokens = [];
308
- if (state.mode === "input")
309
- tokens.push("Ctrl+J Newline");
310
- if (hasFeedback)
311
- tokens.push("Ctrl+D Submit");
312
- if (offscreenHint)
313
- tokens.push(offscreenHint);
314
- const lines = [
315
- approveRow,
316
- ...editorBody,
317
- `${dim}${"─".repeat(width)}${reset}`,
318
- `${INACTIVE_PREFIX}${renderShortcutHints(tokens)}`,
319
- ];
320
- const approveRowInBlock = 0;
321
- const editorStart = 1;
446
+ const listLines = scrollRows === 0
447
+ ? []
448
+ : rendered.lines.slice(scrollTop, scrollTop + scrollRows);
449
+ while (listLines.length < scrollRows)
450
+ listLines.push("");
322
451
  let cursorRow;
323
452
  let cursorCol;
324
- if (state.mode === "approve") {
325
- cursorRow = approveRowInBlock;
326
- cursorCol = activePrefixWidth;
327
- }
328
- else if (showPlaceholder) {
329
- cursorRow = editorStart;
330
- cursorCol = activePrefixWidth;
453
+ if (rendered.editorRow !== undefined) {
454
+ cursorRow = rendered.editorRow - scrollTop;
455
+ cursorCol = rendered.editorCol;
331
456
  }
332
457
  else {
333
- const offset = Math.min(Math.max(0, cursorVisualRow - state.feedbackScrollTop), editorHeight - 1);
334
- cursorRow = editorStart + offset;
335
- const cursorLine = Math.min(Math.max(0, state.cursorLine), state.lines.length - 1);
336
- const isFirstUnderlying = cursorLine === 0 && cursorVisualRow === 0;
337
- const prefixLen = isFirstUnderlying ? activePrefixWidth : inactivePrefixWidth;
338
- cursorCol = prefixLen + cursorVisCol;
458
+ cursorRow = activeStart - scrollTop;
459
+ cursorCol = currentRow(state).kind === "approve" ? visibleLength(ACTIVE_SELECTOR) : 0;
339
460
  }
340
- if (cursorRow < 0)
461
+ if (scrollRows === 0) {
341
462
  cursorRow = 0;
342
- if (cursorCol < 0)
343
463
  cursorCol = 0;
464
+ }
465
+ else {
466
+ if (cursorRow < 0)
467
+ cursorRow = 0;
468
+ if (cursorRow >= scrollRows)
469
+ cursorRow = scrollRows - 1;
470
+ if (cursorCol < 0)
471
+ cursorCol = 0;
472
+ if (cursorCol >= width)
473
+ cursorCol = Math.max(0, width - 1);
474
+ }
475
+ const lines = [
476
+ ...listLines,
477
+ `${dim}${"─".repeat(width)}${reset}`,
478
+ `${INACTIVE_PREFIX}${renderShortcutHints(hintTokens)}`,
479
+ ];
344
480
  return {
345
- lines: lines.map((l) => clipToWidth(l, width)),
481
+ lines: lines.map((line) => clipToWidth(line, width)),
346
482
  cursor: { row: cursorRow, col: cursorCol },
347
483
  };
348
484
  }
485
+ function promptHelpTokens(state, offscreenHint) {
486
+ if (state.editing) {
487
+ const tokens = ["Ctrl+J Newline", "Enter Submit", "Esc Cancel", ALT_V_PASTE_IMAGE_HINT];
488
+ if (state.editing.transientHint) {
489
+ tokens.push(`${yellow}${state.editing.transientHint}${reset}`);
490
+ }
491
+ if (offscreenHint)
492
+ tokens.push(offscreenHint);
493
+ return tokens;
494
+ }
495
+ const row = currentRow(state);
496
+ const tokens = [];
497
+ if (row.kind === "item") {
498
+ tokens.push("Enter Edit", "D Delete", "A Add");
499
+ }
500
+ else if (row.kind === "add") {
501
+ tokens.push("Enter/A Add");
502
+ }
503
+ else {
504
+ tokens.push("Enter Approve", "A Add");
505
+ }
506
+ tokens.push(state.userAdded.length === 0 ? "Ctrl+D Approve" : "Ctrl+D Submit");
507
+ tokens.push("Ctrl+C Quit");
508
+ if (offscreenHint)
509
+ tokens.push(offscreenHint);
510
+ return tokens;
511
+ }
349
512
  /**
350
- * Compute the plan area's available height given total `rows` and the natural
351
- * controls block height. Reserves 1 row for the dim ─ separator.
513
+ * Compute the plan area's available height given total `rows` and the controls
514
+ * block height. Reserves 1 row for the dim separator between plan and controls.
352
515
  */
353
516
  function computePlanRows(controlsHeight, rows) {
354
517
  return Math.max(1, rows - controlsHeight - 1);
@@ -357,8 +520,6 @@ function buildPlanLines(plan, context, state, width, planRows) {
357
520
  const pane = buildPlanPane(plan, context.phaseLabel, width, context.artifactKind ?? "plan");
358
521
  const total = pane.lines.length;
359
522
  const maxScroll = Math.max(0, total - planRows);
360
- // Re-pin to the new bottom when the user last scrolled to End and the plan
361
- // window has since shrunk (e.g. the controls block grew while typing).
362
523
  if (state.planAtEnd)
363
524
  state.planScrollTop = maxScroll;
364
525
  if (state.planScrollTop > maxScroll)
@@ -369,12 +530,24 @@ function buildPlanLines(plan, context, state, width, planRows) {
369
530
  while (sliced.length < planRows)
370
531
  sliced.push("");
371
532
  return {
372
- lines: sliced.map((l) => clipToWidth(l, width)),
533
+ lines: sliced.map((line) => clipToWidth(line, width)),
373
534
  total,
374
535
  };
375
536
  }
376
537
  function buildSurface(plan, context, state, cols, rows) {
377
- const controls = buildControlsBlock(state, cols, primaryOffscreenHint(context));
538
+ const split = computeSplitMetrics({ cols, rows, collapseAt: SPLIT_COLLAPSE_AT_COLS });
539
+ const natural = buildFeedbackListRows(state, cols);
540
+ const naturalRows = natural.lines.length;
541
+ const offscreenHint = primaryOffscreenHint(context);
542
+ const hintTokens = promptHelpTokens(state, offscreenHint);
543
+ if (!split.collapsed && naturalRows > FEEDBACK_AREA_MAX_ROWS) {
544
+ const leftRender = buildFeedbackListRows(state, split.leftWidth);
545
+ const left = buildFeedbackPane(state, leftRender, split.leftWidth, Math.min(leftRender.lines.length, Math.max(0, rows - 2)), hintTokens);
546
+ const { lines: rightLines } = buildPlanLines(plan, context, state, split.rightWidth, rows);
547
+ return buildSplitSurface({ lines: left.lines, cursor: left.cursor }, { lines: rightLines }, { cols, rows, collapseAt: SPLIT_COLLAPSE_AT_COLS }, "left");
548
+ }
549
+ const listRows = Math.min(naturalRows, FEEDBACK_AREA_MAX_ROWS);
550
+ const controls = buildFeedbackPane(state, natural, cols, listRows, hintTokens);
378
551
  const planRows = computePlanRows(controls.lines.length, rows);
379
552
  const { lines: planLines } = buildPlanLines(plan, context, state, cols, planRows);
380
553
  const separator = `${dim}${"─".repeat(cols)}${reset}`;
@@ -383,16 +556,11 @@ function buildSurface(plan, context, state, cols, rows) {
383
556
  separator,
384
557
  ...controls.lines,
385
558
  ];
386
- // Pad / clip to exactly `rows` rows. The plan area is the only flex region;
387
- // controls + separator + margins remain anchored to the bottom.
388
559
  while (composed.length < rows)
389
560
  composed.push("");
390
561
  if (composed.length > rows) {
391
- // Clip from the top of the plan area; never clip controls.
392
562
  composed.splice(0, composed.length - rows);
393
563
  }
394
- // Cursor row sits in the controls block: planRows + 1 (separator) +
395
- // controls.cursor.row.
396
564
  let cursorRow = planRows + 1 + controls.cursor.row;
397
565
  if (cursorRow < 0)
398
566
  cursorRow = 0;
@@ -407,12 +575,24 @@ function planMetrics(plan, context, state, host) {
407
575
  const layout = host.getLayout();
408
576
  const cols = Math.max(20, layout.columns);
409
577
  const rows = Math.max(1, layout.childRows);
410
- const controls = buildControlsBlock(state, cols, primaryOffscreenHint(context));
411
- const planHeight = computePlanRows(controls.lines.length, rows);
578
+ const split = computeSplitMetrics({ cols, rows, collapseAt: SPLIT_COLLAPSE_AT_COLS });
579
+ const naturalRows = measureFeedbackListRows(state, cols);
580
+ if (!split.collapsed && naturalRows > FEEDBACK_AREA_MAX_ROWS) {
581
+ const pane = buildPlanPane(plan, context.phaseLabel, split.rightWidth, context.artifactKind ?? "plan");
582
+ return { planHeight: rows, planLines: pane.lines.length };
583
+ }
584
+ const controlsHeight = Math.min(naturalRows, FEEDBACK_AREA_MAX_ROWS) + 2;
585
+ const planHeight = computePlanRows(controlsHeight, rows);
412
586
  const pane = buildPlanPane(plan, context.phaseLabel, cols, context.artifactKind ?? "plan");
413
587
  return { planHeight, planLines: pane.lines.length };
414
588
  }
415
589
  function primaryOffscreenHint(context) {
416
590
  return context.offscreen?.slots.find((slot) => slot.binding === "primary")?.hint;
417
591
  }
592
+ function reviewArtifactPaneConfig(plan, context) {
593
+ return {
594
+ inquirySide: "left",
595
+ renderArtifact: (width) => buildPlanPane(plan, context.phaseLabel, width, context.artifactKind ?? "plan").lines,
596
+ };
597
+ }
418
598
  //# sourceMappingURL=host-prompt.js.map