@hobin/developer 0.1.3 → 0.1.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.
- package/README.md +160 -38
- package/extensions/developer.ts +483 -68
- package/extensions/machine.ts +344 -0
- package/extensions/state.ts +128 -98
- package/extensions/tool-policy.ts +54 -25
- package/extensions/tui.ts +407 -202
- package/package.json +7 -2
- package/skills/abstraction-review/SKILL.md +6 -5
- package/skills/adversarial-eval/SKILL.md +7 -3
- package/skills/model/SKILL.md +13 -4
- package/skills/naming-judgment/SKILL.md +9 -3
- package/skills/schedule/SKILL.md +6 -3
- package/skills/signal/SKILL.md +6 -3
- package/skills/sketch/SKILL.md +29 -10
- package/skills/specify/SKILL.md +11 -2
- package/skills/verify/SKILL.md +10 -4
- package/skills/visualize/SKILL.md +5 -3
package/extensions/tui.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
DynamicBorder,
|
|
3
2
|
type ExtensionCommandContext,
|
|
3
|
+
type KeybindingsManager,
|
|
4
4
|
type Theme,
|
|
5
5
|
type ThemeColor,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import {
|
|
8
|
-
Box,
|
|
9
8
|
Container,
|
|
10
9
|
matchesKey,
|
|
11
10
|
type SelectItem,
|
|
12
|
-
|
|
11
|
+
type SizeValue,
|
|
13
12
|
Text,
|
|
14
13
|
truncateToWidth,
|
|
15
14
|
visibleWidth,
|
|
@@ -20,6 +19,7 @@ import {
|
|
|
20
19
|
protocolState,
|
|
21
20
|
type DeveloperMode,
|
|
22
21
|
type DeveloperState,
|
|
22
|
+
type JudgmentStatus,
|
|
23
23
|
type PendingQuestion,
|
|
24
24
|
type ProtocolState,
|
|
25
25
|
} from "./state.ts";
|
|
@@ -33,7 +33,12 @@ function modeName(mode: DeveloperMode): string {
|
|
|
33
33
|
|
|
34
34
|
function protocolColor(value: ProtocolState): ThemeColor {
|
|
35
35
|
if (value === "blocked") return "error";
|
|
36
|
-
if (
|
|
36
|
+
if (
|
|
37
|
+
value === "needs-evidence" ||
|
|
38
|
+
value === "needs-answer" ||
|
|
39
|
+
value === "needs-routing" ||
|
|
40
|
+
value === "needs-verification"
|
|
41
|
+
) return "warning";
|
|
37
42
|
if (value === "needs-judgment") return "accent";
|
|
38
43
|
return "dim";
|
|
39
44
|
}
|
|
@@ -44,6 +49,22 @@ function modeColor(mode: DeveloperMode): ThemeColor {
|
|
|
44
49
|
return "dim";
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
function judgmentColor(status: JudgmentStatus): ThemeColor {
|
|
53
|
+
if (status === "blocked") return "error";
|
|
54
|
+
if (status === "needs-evidence") return "warning";
|
|
55
|
+
if (status === "resolved") return "success";
|
|
56
|
+
return "muted";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function judgmentSummary(result: string): string {
|
|
60
|
+
return (
|
|
61
|
+
result
|
|
62
|
+
.split(/\r?\n/)
|
|
63
|
+
.map((line) => line.trim())
|
|
64
|
+
.find((line) => line && !line.startsWith("```")) ?? result
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
47
68
|
export function renderDeveloperFooter(state: DeveloperState, theme: Theme): string {
|
|
48
69
|
const currentProtocol = protocolState(state);
|
|
49
70
|
const target = state.activeRoute?.owner ?? "none";
|
|
@@ -82,7 +103,7 @@ export function developerActionItems(state: DeveloperState): SelectItem[] {
|
|
|
82
103
|
{
|
|
83
104
|
value: "strict",
|
|
84
105
|
label: state.mode === "strict" ? "Strict mode (active)" : "Strict mode",
|
|
85
|
-
description: "
|
|
106
|
+
description: "Allow bash on judgment routes; require direct for Pi built-in edit/write",
|
|
86
107
|
},
|
|
87
108
|
{
|
|
88
109
|
value: "off",
|
|
@@ -93,146 +114,272 @@ export function developerActionItems(state: DeveloperState): SelectItem[] {
|
|
|
93
114
|
return items;
|
|
94
115
|
}
|
|
95
116
|
|
|
117
|
+
function questionAction(owner: PendingQuestion["resolutionOwner"]): string {
|
|
118
|
+
if (owner === "user") return "enter to provide the required answer";
|
|
119
|
+
if (owner === "environment") return "enter to provide access or external evidence";
|
|
120
|
+
if (owner === "agent") return "enter to ask Pi to investigate";
|
|
121
|
+
return "enter to classify and resolve this legacy question";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolutionRequestLabel(owner: PendingQuestion["resolutionOwner"]): string {
|
|
125
|
+
if (owner === "user") return "Required answer or product decision:";
|
|
126
|
+
if (owner === "environment") return "External access, observation, or environment evidence:";
|
|
127
|
+
return "Evidence or investigation request for Pi:";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const ENABLE_MOUSE_SCROLL = "\u001b[?1000h\u001b[?1006h";
|
|
131
|
+
const DISABLE_MOUSE_SCROLL = "\u001b[?1006l\u001b[?1000l";
|
|
132
|
+
|
|
133
|
+
interface WritableTerminal {
|
|
134
|
+
write(data: string): void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function enableMouseScroll(tui: { terminal?: WritableTerminal }): WritableTerminal | undefined {
|
|
138
|
+
const terminal = tui.terminal;
|
|
139
|
+
if (!terminal || typeof terminal.write !== "function") return undefined;
|
|
140
|
+
terminal.write(ENABLE_MOUSE_SCROLL);
|
|
141
|
+
return terminal;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function disableMouseScroll(terminal: WritableTerminal | undefined): void {
|
|
145
|
+
terminal?.write(DISABLE_MOUSE_SCROLL);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function mouseWheelDirection(data: string): -1 | 1 | undefined {
|
|
149
|
+
const match = /^\u001b\[<(\d+);\d+;\d+M$/.exec(data);
|
|
150
|
+
if (!match?.[1]) return undefined;
|
|
151
|
+
const button = Number(match[1]);
|
|
152
|
+
if ((button & 64) === 0 || (button & 3) > 1) return undefined;
|
|
153
|
+
return (button & 1) === 0 ? -1 : 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
96
156
|
export function pendingQuestionItems(questions: PendingQuestion[]): SelectItem[] {
|
|
97
|
-
return questions.map((question) =>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
157
|
+
return questions.map((question) => {
|
|
158
|
+
const action = questionAction(question.resolutionOwner);
|
|
159
|
+
return {
|
|
160
|
+
value: question.id,
|
|
161
|
+
label: question.question,
|
|
162
|
+
description: `${question.status} · ${question.resolutionOwner} · ${question.gate} · ${action}`,
|
|
163
|
+
};
|
|
164
|
+
});
|
|
102
165
|
}
|
|
103
166
|
|
|
104
167
|
interface SelectDialogOptions {
|
|
105
168
|
title: string;
|
|
106
169
|
subtitle: string;
|
|
107
170
|
items: SelectItem[];
|
|
108
|
-
width:
|
|
171
|
+
width: SizeValue;
|
|
172
|
+
minWidth: number;
|
|
109
173
|
maxVisible: number;
|
|
110
|
-
|
|
174
|
+
selectedLabelMaxLines: number;
|
|
175
|
+
selectedDescriptionMaxLines: number;
|
|
111
176
|
}
|
|
112
177
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
178
|
+
function boundedWrappedLines(content: string, width: number, maxLines: number, ellipsis: string): string[] {
|
|
179
|
+
const contentWidth = Math.max(1, width);
|
|
180
|
+
const wrapped = wrapTextWithAnsi(content, contentWidth);
|
|
181
|
+
const visible = wrapped.slice(0, Math.max(1, maxLines));
|
|
182
|
+
if (wrapped.length > visible.length && visible.length > 0) {
|
|
183
|
+
const last = visible.length - 1;
|
|
184
|
+
visible[last] = truncateToWidth(visible[last] ?? "", Math.max(1, contentWidth - 1), "") + ellipsis;
|
|
185
|
+
}
|
|
186
|
+
return visible;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function renderModalFrame(container: Container, theme: Theme, width: number): string[] {
|
|
190
|
+
if (width < 3) return container.render(Math.max(1, width));
|
|
191
|
+
|
|
192
|
+
const innerWidth = width - 2;
|
|
193
|
+
const border = (text: string) => theme.fg("borderAccent", text);
|
|
194
|
+
const rows = container
|
|
195
|
+
.render(innerWidth)
|
|
196
|
+
.map((line) => `${border("│")}${truncateToWidth(line, innerWidth, "…", true)}${border("│")}`);
|
|
197
|
+
return [border(`╭${"─".repeat(innerWidth)}╮`), ...rows, border(`╰${"─".repeat(innerWidth)}╯`)];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
class WrappedSelectList {
|
|
201
|
+
private selectedIndex = 0;
|
|
202
|
+
private readonly items: SelectItem[];
|
|
203
|
+
private readonly keybindings: KeybindingsManager;
|
|
204
|
+
private readonly maxVisible: number;
|
|
205
|
+
private readonly renderHeight: number;
|
|
206
|
+
private readonly selectedDescriptionMaxLines: number;
|
|
207
|
+
private readonly selectedLabelMaxLines: number;
|
|
117
208
|
private readonly theme: Theme;
|
|
118
|
-
|
|
209
|
+
onSelect?: (item: SelectItem) => void;
|
|
210
|
+
onCancel?: () => void;
|
|
119
211
|
|
|
120
|
-
constructor(
|
|
212
|
+
constructor(
|
|
213
|
+
items: SelectItem[],
|
|
214
|
+
maxVisible: number,
|
|
215
|
+
theme: Theme,
|
|
216
|
+
keybindings: KeybindingsManager,
|
|
217
|
+
options: {
|
|
218
|
+
selectedLabelMaxLines: number;
|
|
219
|
+
selectedDescriptionMaxLines: number;
|
|
220
|
+
},
|
|
221
|
+
) {
|
|
222
|
+
this.items = items;
|
|
223
|
+
this.maxVisible = maxVisible;
|
|
121
224
|
this.theme = theme;
|
|
122
|
-
this.
|
|
225
|
+
this.keybindings = keybindings;
|
|
226
|
+
this.selectedLabelMaxLines = options.selectedLabelMaxLines;
|
|
227
|
+
this.selectedDescriptionMaxLines = options.selectedDescriptionMaxLines;
|
|
228
|
+
this.renderHeight = Math.max(
|
|
229
|
+
1,
|
|
230
|
+
maxVisible - 1 + options.selectedLabelMaxLines + options.selectedDescriptionMaxLines + 1,
|
|
231
|
+
);
|
|
123
232
|
}
|
|
124
233
|
|
|
125
|
-
|
|
126
|
-
this.
|
|
127
|
-
this.offset = 0;
|
|
128
|
-
}
|
|
234
|
+
render(width: number): string[] {
|
|
235
|
+
if (this.items.length === 0) return [this.theme.fg("warning", " No items")];
|
|
129
236
|
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
237
|
+
const lines: string[] = [];
|
|
238
|
+
const startIndex = Math.max(
|
|
239
|
+
0,
|
|
240
|
+
Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.items.length - this.maxVisible),
|
|
241
|
+
);
|
|
242
|
+
const endIndex = Math.min(startIndex + this.maxVisible, this.items.length);
|
|
243
|
+
|
|
244
|
+
for (let index = startIndex; index < endIndex; index += 1) {
|
|
245
|
+
const item = this.items[index];
|
|
246
|
+
if (!item) continue;
|
|
247
|
+
if (index !== this.selectedIndex) {
|
|
248
|
+
lines.push(` ${truncateToWidth(item.label, Math.max(1, width - 2), "…")}`);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
134
251
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
252
|
+
const labels = boundedWrappedLines(
|
|
253
|
+
this.theme.fg("accent", item.label),
|
|
254
|
+
width - 2,
|
|
255
|
+
this.selectedLabelMaxLines,
|
|
256
|
+
this.theme.fg("dim", "…"),
|
|
257
|
+
);
|
|
258
|
+
lines.push(`${this.theme.fg("accent", "→ ")}${labels[0] ?? ""}`);
|
|
259
|
+
for (const line of labels.slice(1)) lines.push(` ${line}`);
|
|
260
|
+
|
|
261
|
+
if (item.description) {
|
|
262
|
+
const descriptions = boundedWrappedLines(
|
|
263
|
+
this.theme.fg("muted", item.description),
|
|
264
|
+
width - 4,
|
|
265
|
+
this.selectedDescriptionMaxLines,
|
|
266
|
+
this.theme.fg("dim", "…"),
|
|
267
|
+
);
|
|
268
|
+
for (const line of descriptions) lines.push(` ${line}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (startIndex > 0 || endIndex < this.items.length) {
|
|
273
|
+
lines.push(
|
|
274
|
+
this.theme.fg(
|
|
275
|
+
"dim",
|
|
276
|
+
truncateToWidth(` (${this.selectedIndex + 1}/${this.items.length})`, Math.max(1, width - 2), ""),
|
|
277
|
+
),
|
|
278
|
+
);
|
|
142
279
|
}
|
|
280
|
+
const visible = lines.slice(0, this.renderHeight);
|
|
281
|
+
while (visible.length < this.renderHeight) visible.push("");
|
|
143
282
|
return visible;
|
|
144
283
|
}
|
|
145
284
|
|
|
146
|
-
|
|
285
|
+
handleInput(data: string): void {
|
|
286
|
+
const wheelDirection = mouseWheelDirection(data);
|
|
287
|
+
if (wheelDirection !== undefined) {
|
|
288
|
+
this.moveSelection(wheelDirection * 3);
|
|
289
|
+
} else if (this.keybindings.matches(data, "tui.select.up")) {
|
|
290
|
+
this.selectedIndex = this.selectedIndex === 0 ? this.items.length - 1 : this.selectedIndex - 1;
|
|
291
|
+
} else if (this.keybindings.matches(data, "tui.select.down")) {
|
|
292
|
+
this.selectedIndex = this.selectedIndex === this.items.length - 1 ? 0 : this.selectedIndex + 1;
|
|
293
|
+
} else if (this.keybindings.matches(data, "tui.select.pageUp")) {
|
|
294
|
+
this.moveSelection(-Math.max(1, this.maxVisible - 1));
|
|
295
|
+
} else if (this.keybindings.matches(data, "tui.select.pageDown")) {
|
|
296
|
+
this.moveSelection(Math.max(1, this.maxVisible - 1));
|
|
297
|
+
} else if (matchesKey(data, "home")) {
|
|
298
|
+
this.selectedIndex = 0;
|
|
299
|
+
} else if (matchesKey(data, "end")) {
|
|
300
|
+
this.selectedIndex = Math.max(0, this.items.length - 1);
|
|
301
|
+
} else if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
302
|
+
const selected = this.items[this.selectedIndex];
|
|
303
|
+
if (selected) this.onSelect?.(selected);
|
|
304
|
+
} else if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
305
|
+
this.onCancel?.();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
147
308
|
|
|
148
|
-
private
|
|
149
|
-
|
|
150
|
-
const content = [
|
|
151
|
-
this.theme.fg("accent", this.theme.bold(this.item.label)),
|
|
152
|
-
...(this.item.description ? [this.theme.fg("muted", this.item.description)] : []),
|
|
153
|
-
].join("\n");
|
|
154
|
-
return new Text(content, 1, 0).render(width);
|
|
309
|
+
private moveSelection(delta: number): void {
|
|
310
|
+
this.selectedIndex = Math.max(0, Math.min(this.items.length - 1, this.selectedIndex + delta));
|
|
155
311
|
}
|
|
312
|
+
|
|
313
|
+
invalidate(): void {}
|
|
156
314
|
}
|
|
157
315
|
|
|
158
316
|
async function showSelectDialog(
|
|
159
317
|
ctx: ExtensionCommandContext,
|
|
160
318
|
options: SelectDialogOptions,
|
|
161
319
|
): Promise<string | undefined> {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (matchesKey(data, "shift+up")) preview.scroll(-1);
|
|
219
|
-
else if (matchesKey(data, "shift+down")) preview.scroll(1);
|
|
220
|
-
else list.handleInput(data);
|
|
221
|
-
tui.requestRender();
|
|
320
|
+
let mouseTerminal: WritableTerminal | undefined;
|
|
321
|
+
try {
|
|
322
|
+
const result = await ctx.ui.custom<string | null>(
|
|
323
|
+
(tui, theme, keybindings, done) => {
|
|
324
|
+
mouseTerminal = enableMouseScroll(tui);
|
|
325
|
+
const container = new Container();
|
|
326
|
+
const title = new Text("", 1, 0);
|
|
327
|
+
const subtitle = new Text("", 1, 0);
|
|
328
|
+
const hint = new Text("", 1, 0);
|
|
329
|
+
const updateText = () => {
|
|
330
|
+
title.setText(theme.fg("accent", theme.bold(`◆ ${options.title}`)));
|
|
331
|
+
subtitle.setText(theme.fg("muted", options.subtitle));
|
|
332
|
+
hint.setText(theme.fg("dim", "wheel/↑↓ · PgUp/PgDn · Home/End · enter select · esc cancel"));
|
|
333
|
+
};
|
|
334
|
+
updateText();
|
|
335
|
+
|
|
336
|
+
const list = new WrappedSelectList(
|
|
337
|
+
options.items,
|
|
338
|
+
Math.min(options.items.length, options.maxVisible),
|
|
339
|
+
theme,
|
|
340
|
+
keybindings,
|
|
341
|
+
{
|
|
342
|
+
selectedLabelMaxLines: options.selectedLabelMaxLines,
|
|
343
|
+
selectedDescriptionMaxLines: options.selectedDescriptionMaxLines,
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
list.onSelect = (item) => done(item.value);
|
|
347
|
+
list.onCancel = () => done(null);
|
|
348
|
+
|
|
349
|
+
container.addChild(title);
|
|
350
|
+
container.addChild(subtitle);
|
|
351
|
+
container.addChild(list);
|
|
352
|
+
container.addChild(hint);
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
render(width: number) {
|
|
356
|
+
return renderModalFrame(container, theme, width);
|
|
357
|
+
},
|
|
358
|
+
invalidate() {
|
|
359
|
+
updateText();
|
|
360
|
+
container.invalidate();
|
|
361
|
+
},
|
|
362
|
+
handleInput(data: string) {
|
|
363
|
+
list.handleInput(data);
|
|
364
|
+
tui.requestRender();
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
overlay: true,
|
|
370
|
+
overlayOptions: {
|
|
371
|
+
anchor: "center",
|
|
372
|
+
width: options.width,
|
|
373
|
+
minWidth: options.minWidth,
|
|
374
|
+
maxHeight: "88%",
|
|
375
|
+
margin: 1,
|
|
222
376
|
},
|
|
223
|
-
};
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
overlay: true,
|
|
227
|
-
overlayOptions: {
|
|
228
|
-
anchor: "center",
|
|
229
|
-
width: options.width,
|
|
230
|
-
maxHeight: Math.min(options.maxVisible + 15, 26),
|
|
231
|
-
margin: 1,
|
|
232
377
|
},
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
378
|
+
);
|
|
379
|
+
return result ?? undefined;
|
|
380
|
+
} finally {
|
|
381
|
+
disableMouseScroll(mouseTerminal);
|
|
382
|
+
}
|
|
236
383
|
}
|
|
237
384
|
|
|
238
385
|
export async function showDeveloperActionSelector(
|
|
@@ -243,8 +390,11 @@ export async function showDeveloperActionSelector(
|
|
|
243
390
|
title: "Developer control",
|
|
244
391
|
subtitle: `Current · ${modeName(state.mode)} · ${protocolState(state)}`,
|
|
245
392
|
items: developerActionItems(state),
|
|
246
|
-
width:
|
|
393
|
+
width: "84%",
|
|
394
|
+
minWidth: 78,
|
|
247
395
|
maxVisible: 6,
|
|
396
|
+
selectedLabelMaxLines: 3,
|
|
397
|
+
selectedDescriptionMaxLines: 3,
|
|
248
398
|
});
|
|
249
399
|
if (result === "status" || result === "questions" || result === "on" || result === "strict" || result === "off") {
|
|
250
400
|
return result;
|
|
@@ -252,18 +402,20 @@ export async function showDeveloperActionSelector(
|
|
|
252
402
|
return undefined;
|
|
253
403
|
}
|
|
254
404
|
|
|
255
|
-
export
|
|
405
|
+
export function showPendingQuestionSelector(
|
|
256
406
|
ctx: ExtensionCommandContext,
|
|
257
407
|
questions: PendingQuestion[],
|
|
258
408
|
): Promise<string | undefined> {
|
|
259
|
-
if (questions.length === 0) return undefined;
|
|
409
|
+
if (questions.length === 0) return Promise.resolve(undefined);
|
|
260
410
|
return showSelectDialog(ctx, {
|
|
261
|
-
title: "
|
|
262
|
-
subtitle: "
|
|
411
|
+
title: "Resolve an open Developer question",
|
|
412
|
+
subtitle: "Enter opens an answer/evidence editor; the question closes after a resolved or not-applicable judgment",
|
|
263
413
|
items: pendingQuestionItems(questions),
|
|
264
|
-
width:
|
|
414
|
+
width: "92%",
|
|
415
|
+
minWidth: 88,
|
|
265
416
|
maxVisible: 10,
|
|
266
|
-
|
|
417
|
+
selectedLabelMaxLines: 5,
|
|
418
|
+
selectedDescriptionMaxLines: 3,
|
|
267
419
|
});
|
|
268
420
|
}
|
|
269
421
|
|
|
@@ -288,9 +440,10 @@ export class DeveloperWidget {
|
|
|
288
440
|
);
|
|
289
441
|
}
|
|
290
442
|
for (const question of this.state.pendingQuestions.slice(0, 3)) {
|
|
443
|
+
const label = question.resolutionOwner === "user" ? "? answer" : "? evidence";
|
|
291
444
|
lines.push(
|
|
292
445
|
truncateToWidth(
|
|
293
|
-
`${this.theme.fg(question.status === "blocked" ? "error" : "warning",
|
|
446
|
+
`${this.theme.fg(question.status === "blocked" ? "error" : "warning", label)} ${this.theme.fg("dim", `· ${question.gate} ·`)} ${this.theme.fg("muted", question.question)}`,
|
|
294
447
|
width,
|
|
295
448
|
"…",
|
|
296
449
|
),
|
|
@@ -302,6 +455,9 @@ export class DeveloperWidget {
|
|
|
302
455
|
if (this.state.implementationFramingRequired) {
|
|
303
456
|
lines.push(this.theme.fg("warning", "→ next · sketch feature shape or signal structural movement"));
|
|
304
457
|
}
|
|
458
|
+
if (this.state.rerouteRequired) {
|
|
459
|
+
lines.push(this.theme.fg("warning", "→ next · reroute from the latest direct landing"));
|
|
460
|
+
}
|
|
305
461
|
if (this.state.verificationRequired) {
|
|
306
462
|
lines.push(this.theme.fg("warning", "→ next · verify changed artifacts before completion"));
|
|
307
463
|
}
|
|
@@ -346,96 +502,120 @@ export class DeveloperStatusPanel {
|
|
|
346
502
|
this.onClose();
|
|
347
503
|
return;
|
|
348
504
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
505
|
+
const wheelDirection = mouseWheelDirection(data);
|
|
506
|
+
const pageSize = Math.max(1, this.viewportHeight - 6);
|
|
507
|
+
if (wheelDirection !== undefined) this.moveScroll(wheelDirection * 3);
|
|
508
|
+
else if (matchesKey(data, "up")) this.moveScroll(-1);
|
|
509
|
+
else if (matchesKey(data, "down")) this.moveScroll(1);
|
|
510
|
+
else if (matchesKey(data, "pageUp")) this.moveScroll(-pageSize);
|
|
511
|
+
else if (matchesKey(data, "pageDown")) this.moveScroll(pageSize);
|
|
512
|
+
else if (matchesKey(data, "home")) this.scrollOffset = 0;
|
|
513
|
+
else if (matchesKey(data, "end")) this.scrollOffset = this.maxScrollOffset;
|
|
514
|
+
else return;
|
|
353
515
|
this.invalidate();
|
|
354
516
|
this.requestRender();
|
|
355
517
|
}
|
|
356
518
|
|
|
519
|
+
private moveScroll(delta: number): void {
|
|
520
|
+
this.scrollOffset = Math.max(0, Math.min(this.maxScrollOffset, this.scrollOffset + delta));
|
|
521
|
+
}
|
|
522
|
+
|
|
357
523
|
render(width: number): string[] {
|
|
358
524
|
if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
|
|
359
525
|
|
|
360
|
-
const panelWidth = Math.max(
|
|
361
|
-
const innerWidth = Math.max(
|
|
526
|
+
const panelWidth = Math.max(1, width);
|
|
527
|
+
const innerWidth = Math.max(1, panelWidth - 2);
|
|
362
528
|
const rows: string[] = [];
|
|
363
|
-
const background = (text: string) => this.theme.bg("customMessageBg", text);
|
|
364
529
|
const border = (text: string) => this.theme.fg("borderAccent", text);
|
|
365
|
-
const row = (content = "") => {
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
|
|
530
|
+
const row = (content = "") => `${border("│")}${truncateToWidth(content, innerWidth, "…", true)}${border("│")}`;
|
|
531
|
+
const addWrapped = (label: string, value: string, color: ThemeColor = "muted", maxLines = 2) => {
|
|
532
|
+
const labelText = `${label} ·`;
|
|
533
|
+
const plainPrefix = ` ${labelText} `;
|
|
534
|
+
const styledPrefix = ` ${this.theme.fg("dim", labelText)} `;
|
|
535
|
+
const contentWidth = Math.max(1, innerWidth - visibleWidth(plainPrefix));
|
|
536
|
+
const wrapped = wrapTextWithAnsi(this.theme.fg(color, value.trim()), contentWidth);
|
|
537
|
+
const visible = wrapped.slice(0, Math.max(1, maxLines));
|
|
538
|
+
if (wrapped.length > visible.length && visible.length > 0) {
|
|
539
|
+
const last = visible.length - 1;
|
|
540
|
+
visible[last] =
|
|
541
|
+
truncateToWidth(visible[last] ?? "", Math.max(1, contentWidth - 1), "") + this.theme.fg("dim", "…");
|
|
542
|
+
}
|
|
543
|
+
rows.push(row(styledPrefix + (visible[0] ?? "")));
|
|
544
|
+
const hangingIndent = " ".repeat(visibleWidth(plainPrefix));
|
|
545
|
+
for (const line of visible.slice(1)) rows.push(row(hangingIndent + line));
|
|
374
546
|
};
|
|
375
547
|
const section = (title: string) => rows.push(row(` ${this.theme.fg("accent", this.theme.bold(title))}`));
|
|
376
548
|
|
|
377
|
-
rows.push(
|
|
549
|
+
rows.push(border(`╭${"─".repeat(innerWidth)}╮`));
|
|
378
550
|
rows.push(row(` ${this.theme.fg("accent", this.theme.bold("◆ Developer status"))}`));
|
|
379
551
|
rows.push(row());
|
|
380
552
|
|
|
381
553
|
const state = this.view.state;
|
|
382
554
|
const currentProtocol = protocolState(state);
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
),
|
|
391
|
-
);
|
|
555
|
+
const summary =
|
|
556
|
+
`mode ${this.theme.fg(modeColor(state.mode), modeName(state.mode))}` +
|
|
557
|
+
this.theme.fg("dim", " · ") +
|
|
558
|
+
`protocol ${this.theme.fg(protocolColor(currentProtocol), currentProtocol)}` +
|
|
559
|
+
this.theme.fg("dim", " · ") +
|
|
560
|
+
`target ${this.theme.fg("muted", state.activeRoute?.owner ?? "none")}`;
|
|
561
|
+
for (const line of wrapTextWithAnsi(summary, Math.max(1, innerWidth - 2))) rows.push(row(` ${line}`));
|
|
392
562
|
rows.push(row());
|
|
393
563
|
|
|
394
564
|
section("Active route");
|
|
395
565
|
if (state.activeRoute) {
|
|
396
566
|
addWrapped("id", state.activeRoute.routeId, "dim", 1);
|
|
397
|
-
addWrapped("question", state.activeRoute.question, "text");
|
|
398
|
-
addWrapped("reason", state.activeRoute.reason);
|
|
567
|
+
addWrapped("question", state.activeRoute.question, "text", 3);
|
|
568
|
+
addWrapped("reason", state.activeRoute.reason, "muted", 2);
|
|
399
569
|
addWrapped("skill", state.activeRoute.methodLocation ?? "direct action", "dim", 1);
|
|
400
570
|
addWrapped("known evidence", String(state.activeRoute.knownEvidence.length), "muted", 1);
|
|
401
571
|
} else {
|
|
402
|
-
addWrapped("state", "No route is currently waiting for judgment.", "dim",
|
|
572
|
+
addWrapped("state", "No route is currently waiting for judgment.", "dim", 2);
|
|
403
573
|
}
|
|
404
574
|
|
|
405
575
|
rows.push(row());
|
|
406
576
|
section(`Open questions · ${state.pendingQuestions.length}`);
|
|
407
577
|
if (state.pendingQuestions.length === 0) {
|
|
408
|
-
addWrapped("state", "No unresolved Developer questions.", "dim",
|
|
578
|
+
addWrapped("state", "No unresolved Developer questions.", "dim", 2);
|
|
409
579
|
} else {
|
|
410
|
-
for (const question of state.pendingQuestions) {
|
|
580
|
+
for (const question of state.pendingQuestions.slice(0, 4)) {
|
|
411
581
|
addWrapped(
|
|
412
|
-
question.status
|
|
582
|
+
`${question.status} · ${question.resolutionOwner} · ${question.gate}`,
|
|
413
583
|
question.question,
|
|
414
584
|
question.status === "blocked" ? "error" : "warning",
|
|
415
|
-
|
|
585
|
+
2,
|
|
416
586
|
);
|
|
587
|
+
addWrapped("resolves when", question.resolutionCriteria, "dim", 2);
|
|
588
|
+
}
|
|
589
|
+
if (state.pendingQuestions.length > 4) {
|
|
590
|
+
addWrapped("more", `${state.pendingQuestions.length - 4} additional open questions`, "dim", 1);
|
|
417
591
|
}
|
|
418
592
|
}
|
|
419
593
|
|
|
420
594
|
rows.push(row());
|
|
421
|
-
section(
|
|
422
|
-
if (state.
|
|
423
|
-
addWrapped("
|
|
424
|
-
state.lastJudgment.status === "blocked"
|
|
425
|
-
? "blocked"
|
|
426
|
-
: state.lastJudgment.status === "needs-evidence"
|
|
427
|
-
? "needs-evidence"
|
|
428
|
-
: "idle",
|
|
429
|
-
), 1);
|
|
430
|
-
addWrapped("result", state.lastJudgment.result, "muted");
|
|
431
|
-
addWrapped(
|
|
432
|
-
"evidence",
|
|
433
|
-
`${state.lastJudgment.basis.length} basis · ${state.lastJudgment.artifacts.length} artifacts`,
|
|
434
|
-
"dim",
|
|
435
|
-
1,
|
|
436
|
-
);
|
|
595
|
+
section(`Judgment history · ${state.judgmentHistory.length}`);
|
|
596
|
+
if (state.judgmentHistory.length === 0) {
|
|
597
|
+
addWrapped("state", "No judgment has been recorded on this branch.", "dim", 2);
|
|
437
598
|
} else {
|
|
438
|
-
|
|
599
|
+
const recentJudgments = state.judgmentHistory.slice(-10).toReversed();
|
|
600
|
+
for (const judgment of recentJudgments) {
|
|
601
|
+
const route = state.routeHistory.find((candidate) => candidate.routeId === judgment.routeId);
|
|
602
|
+
addWrapped(
|
|
603
|
+
`${judgment.owner} ${judgment.status}`,
|
|
604
|
+
`${judgment.question} → ${judgmentSummary(judgment.result)}`,
|
|
605
|
+
judgmentColor(judgment.status),
|
|
606
|
+
3,
|
|
607
|
+
);
|
|
608
|
+
if (route) addWrapped("route reason", route.reason, "dim", 2);
|
|
609
|
+
for (const alternative of route?.consideredAlternatives ?? []) {
|
|
610
|
+
addWrapped(`considered ${alternative.owner}`, alternative.reason, "dim", 2);
|
|
611
|
+
}
|
|
612
|
+
for (const update of judgment.questionUpdates ?? []) {
|
|
613
|
+
addWrapped(`question ${update.status}`, `${update.questionId} → ${update.result}`, "accent", 2);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (state.judgmentHistory.length > recentJudgments.length) {
|
|
617
|
+
addWrapped("earlier", `${state.judgmentHistory.length - recentJudgments.length} earlier judgments`, "dim", 1);
|
|
618
|
+
}
|
|
439
619
|
}
|
|
440
620
|
|
|
441
621
|
rows.push(row());
|
|
@@ -446,23 +626,23 @@ export class DeveloperStatusPanel {
|
|
|
446
626
|
1,
|
|
447
627
|
);
|
|
448
628
|
rows.push(row());
|
|
449
|
-
rows.push(row(` ${this.theme.fg("dim", "enter/esc close · /develop questions revisits open work")}`));
|
|
450
|
-
rows.push(background(border(`╰${"─".repeat(innerWidth)}╯`)));
|
|
451
629
|
|
|
452
630
|
const header = rows.slice(0, 2);
|
|
453
|
-
const body = rows.slice(2
|
|
631
|
+
const body = rows.slice(2);
|
|
454
632
|
const bodyCapacity = Math.max(1, this.viewportHeight - 4);
|
|
455
633
|
this.maxScrollOffset = Math.max(0, body.length - bodyCapacity);
|
|
456
634
|
this.scrollOffset = Math.min(this.scrollOffset, this.maxScrollOffset);
|
|
457
635
|
const visibleBody = body.slice(this.scrollOffset, this.scrollOffset + bodyCapacity);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
636
|
+
while (visibleBody.length < bodyCapacity) visibleBody.push(row());
|
|
637
|
+
const position =
|
|
638
|
+
body.length > bodyCapacity
|
|
639
|
+
? `wheel/↑↓ · PgUp/PgDn · Home/End · ${this.scrollOffset + 1}–${Math.min(body.length, this.scrollOffset + bodyCapacity)}/${body.length} · enter/esc close`
|
|
640
|
+
: "enter/esc close · /develop questions revisits open work";
|
|
461
641
|
const visibleRows = [
|
|
462
642
|
...header,
|
|
463
643
|
...visibleBody,
|
|
464
644
|
row(` ${this.theme.fg("dim", position)}`),
|
|
465
|
-
|
|
645
|
+
border(`╰${"─".repeat(innerWidth)}╯`),
|
|
466
646
|
];
|
|
467
647
|
|
|
468
648
|
this.cachedWidth = width;
|
|
@@ -476,31 +656,56 @@ export class DeveloperStatusPanel {
|
|
|
476
656
|
}
|
|
477
657
|
}
|
|
478
658
|
|
|
479
|
-
export async function showDeveloperStatus(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
{
|
|
490
|
-
overlay: true,
|
|
491
|
-
overlayOptions: {
|
|
492
|
-
anchor: "center",
|
|
493
|
-
width: "82%",
|
|
494
|
-
minWidth: 56,
|
|
495
|
-
maxHeight: "85%",
|
|
496
|
-
margin: 1,
|
|
659
|
+
export async function showDeveloperStatus(ctx: ExtensionCommandContext, view: DeveloperStatusView): Promise<void> {
|
|
660
|
+
let mouseTerminal: WritableTerminal | undefined;
|
|
661
|
+
try {
|
|
662
|
+
await ctx.ui.custom<void>(
|
|
663
|
+
(tui, theme, _keybindings, done) => {
|
|
664
|
+
mouseTerminal = enableMouseScroll(tui);
|
|
665
|
+
return new DeveloperStatusPanel(view, theme, () => done(), {
|
|
666
|
+
viewportHeight: Math.max(10, Math.min(36, Math.floor(tui.terminal.rows * 0.88))),
|
|
667
|
+
requestRender: () => tui.requestRender(),
|
|
668
|
+
});
|
|
497
669
|
},
|
|
498
|
-
|
|
499
|
-
|
|
670
|
+
{
|
|
671
|
+
overlay: true,
|
|
672
|
+
overlayOptions: {
|
|
673
|
+
anchor: "center",
|
|
674
|
+
width: "90%",
|
|
675
|
+
minWidth: 72,
|
|
676
|
+
maxHeight: "88%",
|
|
677
|
+
margin: 1,
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
);
|
|
681
|
+
} finally {
|
|
682
|
+
disableMouseScroll(mouseTerminal);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export function questionResolutionPrompt(question: PendingQuestion): string {
|
|
687
|
+
const requestLabel = resolutionRequestLabel(question.resolutionOwner);
|
|
688
|
+
return [
|
|
689
|
+
"Resolve this open Developer question.",
|
|
690
|
+
"",
|
|
691
|
+
`Question: ${question.question}`,
|
|
692
|
+
`Resolution owner: ${question.resolutionOwner}`,
|
|
693
|
+
`Gate: ${question.gate}`,
|
|
694
|
+
`Resolution criteria: ${question.resolutionCriteria}`,
|
|
695
|
+
"",
|
|
696
|
+
requestLabel,
|
|
697
|
+
"",
|
|
698
|
+
"Use the supplied answer as new evidence, or investigate with available tools when the owner is agent.",
|
|
699
|
+
"Route the focused question, then record resolved/not-applicable with question_updates when it is settled; otherwise retain it with the specific remaining evidence gap.",
|
|
700
|
+
].join("\n");
|
|
500
701
|
}
|
|
501
702
|
|
|
502
|
-
export function
|
|
503
|
-
|
|
703
|
+
export function editQuestionResolutionRequest(
|
|
704
|
+
ctx: ExtensionCommandContext,
|
|
705
|
+
question: PendingQuestion,
|
|
706
|
+
): Promise<string | undefined> {
|
|
504
707
|
const current = ctx.ui.getEditorText();
|
|
505
|
-
|
|
708
|
+
const request = questionResolutionPrompt(question);
|
|
709
|
+
const initial = current.trim() ? `${current.trimEnd()}\n\n${request}` : request;
|
|
710
|
+
return ctx.ui.editor("Answer or investigate the selected Developer question", initial);
|
|
506
711
|
}
|