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