@hobin/developer 0.1.6 → 0.1.8
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 +127 -95
- package/SOURCES.md +8 -8
- package/extensions/developer.ts +1752 -1441
- package/extensions/machine.ts +418 -286
- package/extensions/references/behavior-preserving-structural-change.md +14 -13
- package/extensions/state.ts +463 -419
- package/extensions/tool-policy.ts +158 -65
- package/extensions/tui.ts +1666 -910
- package/package.json +1 -1
- package/skills/sketch/references/responsibility-and-variation.md +1 -1
package/extensions/tui.ts
CHANGED
|
@@ -1,1015 +1,1771 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
ExtensionCommandContext,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
KeybindingsManager,
|
|
5
|
+
Theme,
|
|
6
|
+
ThemeColor,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
Container,
|
|
10
|
+
Markdown,
|
|
11
|
+
type MarkdownTheme,
|
|
12
|
+
matchesKey,
|
|
13
|
+
type SelectItem,
|
|
14
|
+
type SettingItem,
|
|
15
|
+
SettingsList,
|
|
16
|
+
type SettingsListTheme,
|
|
17
|
+
type SizeValue,
|
|
18
|
+
Text,
|
|
19
|
+
truncateToWidth,
|
|
20
|
+
visibleWidth,
|
|
21
|
+
wrapTextWithAnsi,
|
|
17
22
|
} from "@earendil-works/pi-tui";
|
|
18
23
|
|
|
19
24
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
protocolState,
|
|
26
|
+
type ChoiceResponseField,
|
|
27
|
+
type ChoiceResponseOption,
|
|
28
|
+
type DeveloperState,
|
|
29
|
+
type JudgmentEvent,
|
|
30
|
+
type JudgmentStatus,
|
|
31
|
+
type PendingQuestion,
|
|
32
|
+
type ProtocolState,
|
|
33
|
+
type RouteEvent,
|
|
28
34
|
} from "./state.ts";
|
|
29
35
|
|
|
30
|
-
export type
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
export type DeveloperSettingsNavigation =
|
|
37
|
+
| { kind: "status" }
|
|
38
|
+
| { kind: "questions" }
|
|
39
|
+
| { kind: "history" };
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
export interface DeveloperSettingsBinding {
|
|
42
|
+
read(): DeveloperState;
|
|
43
|
+
commitActivation(enabled: boolean): DeveloperState;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
function developerMarkdownTheme(theme: Theme): MarkdownTheme {
|
|
47
|
+
return {
|
|
48
|
+
heading: (text) => theme.fg("mdHeading", text),
|
|
49
|
+
link: (text) => theme.fg("mdLink", text),
|
|
50
|
+
linkUrl: (text) => theme.fg("mdLinkUrl", text),
|
|
51
|
+
code: (text) => theme.fg("mdCode", text),
|
|
52
|
+
codeBlock: (text) => theme.fg("mdCodeBlock", text),
|
|
53
|
+
codeBlockBorder: (text) => theme.fg("mdCodeBlockBorder", text),
|
|
54
|
+
quote: (text) => theme.fg("mdQuote", text),
|
|
55
|
+
quoteBorder: (text) => theme.fg("mdQuoteBorder", text),
|
|
56
|
+
hr: (text) => theme.fg("mdHr", text),
|
|
57
|
+
listBullet: (text) => theme.fg("mdListBullet", text),
|
|
58
|
+
bold: (text) => theme.bold(text),
|
|
59
|
+
italic: (text) => theme.italic(text),
|
|
60
|
+
strikethrough: (text) => theme.strikethrough(text),
|
|
61
|
+
underline: (text) => theme.underline(text),
|
|
62
|
+
};
|
|
50
63
|
}
|
|
51
64
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
function protocolColor(value: ProtocolState): ThemeColor {
|
|
66
|
+
if (value === "blocked") return "error";
|
|
67
|
+
if (
|
|
68
|
+
value === "needs-evidence" ||
|
|
69
|
+
value === "needs-answer" ||
|
|
70
|
+
value === "needs-routing" ||
|
|
71
|
+
value === "needs-verification"
|
|
72
|
+
)
|
|
73
|
+
return "warning";
|
|
74
|
+
if (value === "needs-judgment") return "accent";
|
|
75
|
+
return "dim";
|
|
56
76
|
}
|
|
57
77
|
|
|
58
78
|
function judgmentColor(status: JudgmentStatus): ThemeColor {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
if (status === "blocked") return "error";
|
|
80
|
+
if (status === "needs-evidence") return "warning";
|
|
81
|
+
if (status === "resolved") return "success";
|
|
82
|
+
return "muted";
|
|
63
83
|
}
|
|
64
84
|
|
|
65
85
|
function judgmentSummary(result: string): string {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function renderDeveloperFooter(state: DeveloperState, theme: Theme): string {
|
|
75
|
-
const currentProtocol = protocolState(state);
|
|
76
|
-
const target = state.activeRoute?.owner ?? "none";
|
|
77
|
-
return (
|
|
78
|
-
theme.fg("accent", "developer") +
|
|
79
|
-
theme.fg("dim", " · ") +
|
|
80
|
-
theme.fg(modeColor(state.mode), modeName(state.mode)) +
|
|
81
|
-
theme.fg("dim", " · ") +
|
|
82
|
-
theme.fg(protocolColor(currentProtocol), currentProtocol) +
|
|
83
|
-
theme.fg("dim", ` · ${target}`)
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function developerActionItems(state: DeveloperState): SelectItem[] {
|
|
88
|
-
const currentProtocol = protocolState(state);
|
|
89
|
-
const items: SelectItem[] = [
|
|
90
|
-
{
|
|
91
|
-
value: "status",
|
|
92
|
-
label: "Inspect status",
|
|
93
|
-
description: `${modeName(state.mode)} · ${currentProtocol} · ${state.pendingQuestions.length} open`,
|
|
94
|
-
},
|
|
95
|
-
];
|
|
96
|
-
items.push(...pendingQuestionItems(state.pendingQuestions));
|
|
97
|
-
items.push(
|
|
98
|
-
{
|
|
99
|
-
value: "on",
|
|
100
|
-
label: state.mode === "on" ? "Adaptive mode (active)" : "Adaptive mode",
|
|
101
|
-
description: "Route judgments adaptively while preserving the current active tool set",
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
value: "strict",
|
|
105
|
-
label: state.mode === "strict" ? "Strict mode (active)" : "Strict mode",
|
|
106
|
-
description: "Allow bash on judgment routes; require direct for Pi built-in edit/write",
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
value: "off",
|
|
110
|
-
label: state.mode === "off" ? "Off (active)" : "Turn off",
|
|
111
|
-
description: "Clear Developer protocol state and remove its persistent UI",
|
|
112
|
-
},
|
|
113
|
-
);
|
|
114
|
-
return items;
|
|
86
|
+
return (
|
|
87
|
+
result
|
|
88
|
+
.split(/\r?\n/)
|
|
89
|
+
.map((line) => line.trim())
|
|
90
|
+
.find((line) => line && !line.startsWith("```")) ?? result
|
|
91
|
+
);
|
|
115
92
|
}
|
|
116
93
|
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
94
|
+
export function renderDeveloperFooter(
|
|
95
|
+
state: DeveloperState,
|
|
96
|
+
theme: Theme,
|
|
97
|
+
): string {
|
|
98
|
+
const currentProtocol = protocolState(state);
|
|
99
|
+
const target = state.activeRoute?.target ?? "none";
|
|
100
|
+
return (
|
|
101
|
+
theme.fg("accent", "developer") +
|
|
102
|
+
theme.fg("dim", " · ") +
|
|
103
|
+
theme.fg(state.enabled ? "success" : "dim", state.enabled ? "on" : "off") +
|
|
104
|
+
theme.fg("dim", " · ") +
|
|
105
|
+
theme.fg(protocolColor(currentProtocol), currentProtocol) +
|
|
106
|
+
theme.fg("dim", ` · ${target}`)
|
|
107
|
+
);
|
|
122
108
|
}
|
|
123
109
|
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
if (owner === "environment") return "External access, observation, or environment evidence:";
|
|
127
|
-
return "Evidence or investigation request for Pi:";
|
|
110
|
+
export function hasDiscardableDeveloperWork(state: DeveloperState): boolean {
|
|
111
|
+
return Boolean(state.activeRoute) || state.pendingQuestions.length > 0;
|
|
128
112
|
}
|
|
129
113
|
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
114
|
+
export function developerSettingItems(state: DeveloperState): SettingItem[] {
|
|
115
|
+
const currentProtocol = protocolState(state);
|
|
116
|
+
const items: SettingItem[] = [
|
|
117
|
+
{
|
|
118
|
+
id: "activation",
|
|
119
|
+
label: "Developer",
|
|
120
|
+
currentValue: state.enabled ? "On" : "Off",
|
|
121
|
+
values: ["On", "Off"],
|
|
122
|
+
description:
|
|
123
|
+
"Route judgments and control Pi built-in bash, edit, and write",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "status",
|
|
127
|
+
label: "Status",
|
|
128
|
+
currentValue: currentProtocol,
|
|
129
|
+
values: [currentProtocol],
|
|
130
|
+
description:
|
|
131
|
+
"Inspect the current route, evidence, debt, and active tools",
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
if (state.judgmentHistory.length > 0) {
|
|
135
|
+
const count = String(state.judgmentHistory.length);
|
|
136
|
+
items.push({
|
|
137
|
+
id: "history",
|
|
138
|
+
label: "History",
|
|
139
|
+
currentValue: count,
|
|
140
|
+
values: [count],
|
|
141
|
+
description: "Inspect complete route and judgment records",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (state.pendingQuestions.length > 0) {
|
|
145
|
+
const count = String(state.pendingQuestions.length);
|
|
146
|
+
items.push({
|
|
147
|
+
id: "questions",
|
|
148
|
+
label: "Open questions",
|
|
149
|
+
currentValue: count,
|
|
150
|
+
values: [count],
|
|
151
|
+
description: "Review and answer unresolved Developer questions",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return items;
|
|
155
|
+
}
|
|
134
156
|
|
|
135
|
-
|
|
157
|
+
function settingsListTheme(theme: Theme): SettingsListTheme {
|
|
158
|
+
return {
|
|
159
|
+
label: (text, selected) =>
|
|
160
|
+
selected ? theme.fg("accent", theme.bold(text)) : theme.fg("text", text),
|
|
161
|
+
value: (text, selected) => theme.fg(selected ? "accent" : "muted", text),
|
|
162
|
+
description: (text) => theme.fg("muted", text),
|
|
163
|
+
cursor: theme.fg("accent", "→ "),
|
|
164
|
+
hint: (text) => theme.fg("dim", text),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
136
167
|
|
|
137
|
-
|
|
138
|
-
|
|
168
|
+
function questionAction(owner: PendingQuestion["resolutionOwner"]): string {
|
|
169
|
+
if (owner === "user") return "enter to provide the required answer";
|
|
170
|
+
if (owner === "environment")
|
|
171
|
+
return "enter to provide access or external evidence";
|
|
172
|
+
if (owner === "agent") return "enter to ask Pi to investigate";
|
|
173
|
+
return "enter to classify and resolve this legacy question";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolutionRequestLabel(
|
|
177
|
+
owner: PendingQuestion["resolutionOwner"],
|
|
178
|
+
): string {
|
|
179
|
+
if (owner === "user") return "Required answer or product decision:";
|
|
180
|
+
if (owner === "environment")
|
|
181
|
+
return "External access, observation, or environment evidence:";
|
|
182
|
+
return "Evidence or investigation request for Pi:";
|
|
139
183
|
}
|
|
140
184
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
185
|
+
const MAX_IMMEDIATE_REQUEST_BYTES = 16_000;
|
|
186
|
+
const MAX_IMMEDIATE_REQUEST_LINES = 1_000;
|
|
187
|
+
|
|
188
|
+
export type ImmediateQuestionDisposition =
|
|
189
|
+
| { kind: "answer"; request: string }
|
|
190
|
+
| { kind: "defer" };
|
|
191
|
+
|
|
192
|
+
export function pendingQuestionItems(
|
|
193
|
+
questions: PendingQuestion[],
|
|
194
|
+
): SelectItem[] {
|
|
195
|
+
return questions.map((question) => {
|
|
196
|
+
const action = questionAction(question.resolutionOwner);
|
|
197
|
+
return {
|
|
198
|
+
value: question.id,
|
|
199
|
+
label: question.question,
|
|
200
|
+
description: `${question.status} · ${question.resolutionOwner} · ${question.gate} · ${action}`,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
146
203
|
}
|
|
147
204
|
|
|
148
|
-
|
|
149
|
-
|
|
205
|
+
export interface DeveloperHistoryEntry {
|
|
206
|
+
id: string;
|
|
207
|
+
judgment: JudgmentEvent;
|
|
208
|
+
route?: RouteEvent;
|
|
150
209
|
}
|
|
151
210
|
|
|
152
|
-
function
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
211
|
+
export function developerHistoryEntries(
|
|
212
|
+
state: DeveloperState,
|
|
213
|
+
): DeveloperHistoryEntry[] {
|
|
214
|
+
return state.judgmentHistory.toReversed().map((judgment) => ({
|
|
215
|
+
id: judgment.routeId,
|
|
216
|
+
judgment,
|
|
217
|
+
route: state.routeHistory.find(
|
|
218
|
+
(candidate) => candidate.routeId === judgment.routeId,
|
|
219
|
+
),
|
|
220
|
+
}));
|
|
158
221
|
}
|
|
159
222
|
|
|
160
|
-
export function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
});
|
|
223
|
+
export function historySelectItems(
|
|
224
|
+
entries: DeveloperHistoryEntry[],
|
|
225
|
+
): SelectItem[] {
|
|
226
|
+
return entries.map(({ id, judgment }) => ({
|
|
227
|
+
value: id,
|
|
228
|
+
label: `${judgment.target} · ${judgment.status} · ${judgment.question}`,
|
|
229
|
+
description: judgmentSummary(judgment.result),
|
|
230
|
+
}));
|
|
169
231
|
}
|
|
170
232
|
|
|
171
233
|
interface SelectDialogOptions {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
234
|
+
title: string;
|
|
235
|
+
subtitle: string;
|
|
236
|
+
items: SelectItem[];
|
|
237
|
+
width: SizeValue;
|
|
238
|
+
minWidth: number;
|
|
239
|
+
maxVisible: number;
|
|
240
|
+
selectedLabelMaxLines: number;
|
|
241
|
+
selectedDescriptionMaxLines: number;
|
|
242
|
+
initialValue?: string;
|
|
180
243
|
}
|
|
181
244
|
|
|
182
245
|
function boundedWrappedLines(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
246
|
+
content: string,
|
|
247
|
+
width: number,
|
|
248
|
+
maxLines: number,
|
|
249
|
+
ellipsis: string,
|
|
187
250
|
): string[] {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
251
|
+
const contentWidth = Math.max(1, width);
|
|
252
|
+
const wrapped = wrapTextWithAnsi(content, contentWidth);
|
|
253
|
+
const visible = wrapped.slice(0, Math.max(1, maxLines));
|
|
254
|
+
if (wrapped.length > visible.length && visible.length > 0) {
|
|
255
|
+
const last = visible.length - 1;
|
|
256
|
+
visible[last] =
|
|
257
|
+
truncateToWidth(visible[last] ?? "", Math.max(1, contentWidth - 1), "") +
|
|
258
|
+
ellipsis;
|
|
259
|
+
}
|
|
260
|
+
return visible;
|
|
197
261
|
}
|
|
198
262
|
|
|
199
|
-
function renderModalFrame(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
263
|
+
function renderModalFrame(
|
|
264
|
+
container: Container,
|
|
265
|
+
theme: Theme,
|
|
266
|
+
width: number,
|
|
267
|
+
): string[] {
|
|
268
|
+
if (width < 3) return container.render(Math.max(1, width));
|
|
269
|
+
|
|
270
|
+
const innerWidth = width - 2;
|
|
271
|
+
const border = (text: string) => theme.fg("borderAccent", text);
|
|
272
|
+
const rows = container
|
|
273
|
+
.render(innerWidth)
|
|
274
|
+
.map(
|
|
275
|
+
(line) =>
|
|
276
|
+
`${border("│")}${truncateToWidth(line, innerWidth, "…", true)}${border("│")}`,
|
|
277
|
+
);
|
|
278
|
+
return [
|
|
279
|
+
border(`╭${"─".repeat(innerWidth)}╮`),
|
|
280
|
+
...rows,
|
|
281
|
+
border(`╰${"─".repeat(innerWidth)}╯`),
|
|
282
|
+
];
|
|
208
283
|
}
|
|
209
284
|
|
|
210
285
|
class WrappedSelectList {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
286
|
+
private selectedIndex: number;
|
|
287
|
+
private readonly items: SelectItem[];
|
|
288
|
+
private readonly keybindings: KeybindingsManager;
|
|
289
|
+
private readonly maxVisible: number;
|
|
290
|
+
private readonly renderHeight: number;
|
|
291
|
+
private readonly selectedDescriptionMaxLines: number;
|
|
292
|
+
private readonly selectedLabelMaxLines: number;
|
|
293
|
+
private readonly theme: Theme;
|
|
294
|
+
onSelect?: (item: SelectItem) => void;
|
|
295
|
+
onCancel?: () => void;
|
|
296
|
+
|
|
297
|
+
constructor(
|
|
298
|
+
items: SelectItem[],
|
|
299
|
+
maxVisible: number,
|
|
300
|
+
theme: Theme,
|
|
301
|
+
keybindings: KeybindingsManager,
|
|
302
|
+
options: {
|
|
303
|
+
selectedLabelMaxLines: number;
|
|
304
|
+
selectedDescriptionMaxLines: number;
|
|
305
|
+
initialValue?: string;
|
|
306
|
+
},
|
|
307
|
+
) {
|
|
308
|
+
this.items = items;
|
|
309
|
+
this.maxVisible = maxVisible;
|
|
310
|
+
this.theme = theme;
|
|
311
|
+
this.keybindings = keybindings;
|
|
312
|
+
this.selectedLabelMaxLines = options.selectedLabelMaxLines;
|
|
313
|
+
this.selectedDescriptionMaxLines = options.selectedDescriptionMaxLines;
|
|
314
|
+
const initialIndex = options.initialValue
|
|
315
|
+
? items.findIndex((item) => item.value === options.initialValue)
|
|
316
|
+
: -1;
|
|
317
|
+
this.selectedIndex = initialIndex >= 0 ? initialIndex : 0;
|
|
318
|
+
this.renderHeight = Math.max(
|
|
319
|
+
1,
|
|
320
|
+
maxVisible -
|
|
321
|
+
1 +
|
|
322
|
+
options.selectedLabelMaxLines +
|
|
323
|
+
options.selectedDescriptionMaxLines +
|
|
324
|
+
1,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
render(width: number): string[] {
|
|
329
|
+
if (this.items.length === 0)
|
|
330
|
+
return [this.theme.fg("warning", " No items")];
|
|
331
|
+
|
|
332
|
+
const lines: string[] = [];
|
|
333
|
+
const startIndex = Math.max(
|
|
334
|
+
0,
|
|
335
|
+
Math.min(
|
|
336
|
+
this.selectedIndex - Math.floor(this.maxVisible / 2),
|
|
337
|
+
this.items.length - this.maxVisible,
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
const endIndex = Math.min(startIndex + this.maxVisible, this.items.length);
|
|
341
|
+
|
|
342
|
+
for (let index = startIndex; index < endIndex; index += 1) {
|
|
343
|
+
const item = this.items[index];
|
|
344
|
+
if (!item) continue;
|
|
345
|
+
if (index !== this.selectedIndex) {
|
|
346
|
+
lines.push(
|
|
347
|
+
` ${truncateToWidth(item.label, Math.max(1, width - 2), "…")}`,
|
|
348
|
+
);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const labels = boundedWrappedLines(
|
|
353
|
+
this.theme.fg("accent", item.label),
|
|
354
|
+
width - 2,
|
|
355
|
+
this.selectedLabelMaxLines,
|
|
356
|
+
this.theme.fg("dim", "…"),
|
|
357
|
+
);
|
|
358
|
+
lines.push(`${this.theme.fg("accent", "→ ")}${labels[0] ?? ""}`);
|
|
359
|
+
for (const line of labels.slice(1)) lines.push(` ${line}`);
|
|
360
|
+
|
|
361
|
+
if (item.description) {
|
|
362
|
+
const descriptions = boundedWrappedLines(
|
|
363
|
+
this.theme.fg("muted", item.description),
|
|
364
|
+
width - 4,
|
|
365
|
+
this.selectedDescriptionMaxLines,
|
|
366
|
+
this.theme.fg("dim", "…"),
|
|
367
|
+
);
|
|
368
|
+
for (const line of descriptions) lines.push(` ${line}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (startIndex > 0 || endIndex < this.items.length) {
|
|
373
|
+
lines.push(
|
|
374
|
+
this.theme.fg(
|
|
375
|
+
"dim",
|
|
376
|
+
truncateToWidth(
|
|
377
|
+
` (${this.selectedIndex + 1}/${this.items.length})`,
|
|
378
|
+
Math.max(1, width - 2),
|
|
379
|
+
"",
|
|
380
|
+
),
|
|
381
|
+
),
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const visible = lines.slice(0, this.renderHeight);
|
|
385
|
+
while (visible.length < this.renderHeight) visible.push("");
|
|
386
|
+
return visible;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
handleInput(data: string): void {
|
|
390
|
+
if (this.keybindings.matches(data, "tui.select.up")) {
|
|
391
|
+
this.selectedIndex =
|
|
392
|
+
this.selectedIndex === 0
|
|
393
|
+
? this.items.length - 1
|
|
394
|
+
: this.selectedIndex - 1;
|
|
395
|
+
} else if (this.keybindings.matches(data, "tui.select.down")) {
|
|
396
|
+
this.selectedIndex =
|
|
397
|
+
this.selectedIndex === this.items.length - 1
|
|
398
|
+
? 0
|
|
399
|
+
: this.selectedIndex + 1;
|
|
400
|
+
} else if (this.keybindings.matches(data, "tui.select.pageUp")) {
|
|
401
|
+
this.moveSelection(-Math.max(1, this.maxVisible - 1));
|
|
402
|
+
} else if (this.keybindings.matches(data, "tui.select.pageDown")) {
|
|
403
|
+
this.moveSelection(Math.max(1, this.maxVisible - 1));
|
|
404
|
+
} else if (matchesKey(data, "home")) {
|
|
405
|
+
this.selectedIndex = 0;
|
|
406
|
+
} else if (matchesKey(data, "end")) {
|
|
407
|
+
this.selectedIndex = Math.max(0, this.items.length - 1);
|
|
408
|
+
} else if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
409
|
+
const selected = this.items[this.selectedIndex];
|
|
410
|
+
if (selected) this.onSelect?.(selected);
|
|
411
|
+
} else if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
412
|
+
this.onCancel?.();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private moveSelection(delta: number): void {
|
|
417
|
+
this.selectedIndex = Math.max(
|
|
418
|
+
0,
|
|
419
|
+
Math.min(this.items.length - 1, this.selectedIndex + delta),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
invalidate(): void {}
|
|
333
424
|
}
|
|
334
425
|
|
|
335
426
|
async function showSelectDialog(
|
|
336
|
-
|
|
337
|
-
|
|
427
|
+
ctx: ExtensionContext,
|
|
428
|
+
options: SelectDialogOptions,
|
|
429
|
+
presentation: "overlay" | "surface" = "overlay",
|
|
338
430
|
): Promise<string | undefined> {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
431
|
+
const result = await ctx.ui.custom<string | null>(
|
|
432
|
+
(tui, theme, keybindings, done) => {
|
|
433
|
+
const container = new Container();
|
|
434
|
+
const title = new Text("", 1, 0);
|
|
435
|
+
const subtitle = new Text("", 1, 0);
|
|
436
|
+
const hint = new Text("", 1, 0);
|
|
437
|
+
const updateText = () => {
|
|
438
|
+
title.setText(theme.fg("accent", theme.bold(`◆ ${options.title}`)));
|
|
439
|
+
subtitle.setText(theme.fg("muted", options.subtitle));
|
|
440
|
+
hint.setText(
|
|
441
|
+
theme.fg(
|
|
442
|
+
"dim",
|
|
443
|
+
"mouse wheel scroll · drag select · Cmd+C copy · ↑↓ select · enter open · esc back",
|
|
444
|
+
),
|
|
445
|
+
);
|
|
446
|
+
};
|
|
447
|
+
updateText();
|
|
448
|
+
|
|
449
|
+
const list = new WrappedSelectList(
|
|
450
|
+
options.items,
|
|
451
|
+
Math.min(options.items.length, options.maxVisible),
|
|
452
|
+
theme,
|
|
453
|
+
keybindings,
|
|
454
|
+
{
|
|
455
|
+
selectedLabelMaxLines: options.selectedLabelMaxLines,
|
|
456
|
+
selectedDescriptionMaxLines: options.selectedDescriptionMaxLines,
|
|
457
|
+
initialValue: options.initialValue,
|
|
458
|
+
},
|
|
459
|
+
);
|
|
460
|
+
list.onSelect = (item) => done(item.value);
|
|
461
|
+
list.onCancel = () => done(null);
|
|
462
|
+
|
|
463
|
+
container.addChild(title);
|
|
464
|
+
container.addChild(subtitle);
|
|
465
|
+
container.addChild(list);
|
|
466
|
+
container.addChild(hint);
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
render(width: number) {
|
|
470
|
+
return presentation === "overlay"
|
|
471
|
+
? renderModalFrame(container, theme, width)
|
|
472
|
+
: container.render(width);
|
|
473
|
+
},
|
|
474
|
+
invalidate() {
|
|
475
|
+
updateText();
|
|
476
|
+
container.invalidate();
|
|
477
|
+
},
|
|
478
|
+
handleInput(data: string) {
|
|
479
|
+
list.handleInput(data);
|
|
480
|
+
tui.requestRender();
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
},
|
|
484
|
+
presentation === "overlay"
|
|
485
|
+
? {
|
|
486
|
+
overlay: true,
|
|
487
|
+
overlayOptions: {
|
|
488
|
+
anchor: "center",
|
|
489
|
+
width: options.width,
|
|
490
|
+
minWidth: options.minWidth,
|
|
491
|
+
maxHeight: "88%",
|
|
492
|
+
margin: 1,
|
|
493
|
+
},
|
|
494
|
+
}
|
|
495
|
+
: undefined,
|
|
496
|
+
);
|
|
497
|
+
return result ?? undefined;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function discardableDeveloperWorkLabel(state: DeveloperState): string {
|
|
501
|
+
return [
|
|
502
|
+
...(state.activeRoute ? ["the active route"] : []),
|
|
503
|
+
...(state.pendingQuestions.length > 0
|
|
504
|
+
? [`${state.pendingQuestions.length} open question(s)`]
|
|
505
|
+
: []),
|
|
506
|
+
].join(" and ");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export async function confirmDisableDeveloper(
|
|
510
|
+
ctx: ExtensionContext,
|
|
511
|
+
state: DeveloperState,
|
|
512
|
+
): Promise<boolean> {
|
|
513
|
+
const work = discardableDeveloperWorkLabel(state);
|
|
514
|
+
const selected = await showSelectDialog(ctx, {
|
|
515
|
+
title: "Turn off Developer?",
|
|
516
|
+
subtitle: `Turning off clears ${work} from current protocol state. Session history remains.`,
|
|
517
|
+
items: [
|
|
518
|
+
{
|
|
519
|
+
value: "keep-on",
|
|
520
|
+
label: "Keep Developer On",
|
|
521
|
+
description: "Return without changing protocol state",
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
value: "turn-off",
|
|
525
|
+
label: "Turn Off and Clear",
|
|
526
|
+
description: `Clear ${work}`,
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
width: 64,
|
|
530
|
+
minWidth: 42,
|
|
531
|
+
maxVisible: 2,
|
|
532
|
+
selectedLabelMaxLines: 2,
|
|
533
|
+
selectedDescriptionMaxLines: 2,
|
|
534
|
+
});
|
|
535
|
+
return selected === "turn-off";
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export class DeveloperSettingsSurface extends Container {
|
|
539
|
+
private activationPending = false;
|
|
540
|
+
private readonly binding: DeveloperSettingsBinding;
|
|
541
|
+
private readonly ctx: ExtensionCommandContext;
|
|
542
|
+
private readonly done: (result: DeveloperSettingsNavigation | null) => void;
|
|
543
|
+
private readonly requestRender: () => void;
|
|
544
|
+
private settings!: SettingsList;
|
|
545
|
+
private state: DeveloperState;
|
|
546
|
+
private readonly theme: Theme;
|
|
547
|
+
|
|
548
|
+
constructor(
|
|
549
|
+
ctx: ExtensionCommandContext,
|
|
550
|
+
binding: DeveloperSettingsBinding,
|
|
551
|
+
theme: Theme,
|
|
552
|
+
done: (result: DeveloperSettingsNavigation | null) => void,
|
|
553
|
+
requestRender: () => void,
|
|
554
|
+
) {
|
|
555
|
+
super();
|
|
556
|
+
this.ctx = ctx;
|
|
557
|
+
this.binding = binding;
|
|
558
|
+
this.theme = theme;
|
|
559
|
+
this.done = done;
|
|
560
|
+
this.requestRender = requestRender;
|
|
561
|
+
this.state = binding.read();
|
|
562
|
+
this.rebuild();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private rebuild(): void {
|
|
566
|
+
this.clear();
|
|
567
|
+
this.addChild(
|
|
568
|
+
new Text(this.theme.fg("accent", this.theme.bold("◆ Developer")), 0, 0),
|
|
569
|
+
);
|
|
570
|
+
this.addChild(
|
|
571
|
+
new Text(
|
|
572
|
+
this.theme.fg("muted", "Current branch settings and protocol details"),
|
|
573
|
+
0,
|
|
574
|
+
0,
|
|
575
|
+
),
|
|
576
|
+
);
|
|
577
|
+
this.addChild(new Text("", 0, 0));
|
|
578
|
+
|
|
579
|
+
const items = developerSettingItems(this.state);
|
|
580
|
+
this.settings = new SettingsList(
|
|
581
|
+
items,
|
|
582
|
+
items.length,
|
|
583
|
+
settingsListTheme(this.theme),
|
|
584
|
+
(id, value) => {
|
|
585
|
+
if (id === "activation") {
|
|
586
|
+
this.sync(this.binding.read());
|
|
587
|
+
this.requestRender();
|
|
588
|
+
void this.requestActivation(value === "On");
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (id === "status") {
|
|
592
|
+
this.done({ kind: "status" });
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (id === "history") {
|
|
596
|
+
this.done({ kind: "history" });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (id === "questions") this.done({ kind: "questions" });
|
|
600
|
+
},
|
|
601
|
+
() => this.done(null),
|
|
602
|
+
);
|
|
603
|
+
this.addChild(this.settings);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private sync(next: DeveloperState): void {
|
|
607
|
+
const questionsShapeChanged =
|
|
608
|
+
this.state.pendingQuestions.length > 0 !==
|
|
609
|
+
next.pendingQuestions.length > 0;
|
|
610
|
+
this.state = next;
|
|
611
|
+
if (questionsShapeChanged) {
|
|
612
|
+
this.rebuild();
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
this.settings.updateValue("activation", next.enabled ? "On" : "Off");
|
|
616
|
+
this.settings.updateValue("status", protocolState(next));
|
|
617
|
+
if (next.pendingQuestions.length > 0) {
|
|
618
|
+
this.settings.updateValue(
|
|
619
|
+
"questions",
|
|
620
|
+
String(next.pendingQuestions.length),
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
this.invalidate();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private async requestActivation(enabled: boolean): Promise<void> {
|
|
627
|
+
if (this.activationPending) return;
|
|
628
|
+
this.activationPending = true;
|
|
629
|
+
try {
|
|
630
|
+
const current = this.binding.read();
|
|
631
|
+
const accepted =
|
|
632
|
+
enabled || !hasDiscardableDeveloperWork(current)
|
|
633
|
+
? true
|
|
634
|
+
: await confirmDisableDeveloper(this.ctx, current);
|
|
635
|
+
this.sync(
|
|
636
|
+
accepted ? this.binding.commitActivation(enabled) : this.binding.read(),
|
|
637
|
+
);
|
|
638
|
+
} catch (error) {
|
|
639
|
+
this.sync(this.binding.read());
|
|
640
|
+
this.ctx.ui.notify(
|
|
641
|
+
`Developer activation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
642
|
+
"error",
|
|
643
|
+
);
|
|
644
|
+
} finally {
|
|
645
|
+
this.activationPending = false;
|
|
646
|
+
this.requestRender();
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
handleInput(data: string): void {
|
|
651
|
+
if (this.activationPending) return;
|
|
652
|
+
this.settings.handleInput(data);
|
|
653
|
+
this.requestRender();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export async function showDeveloperSettings(
|
|
658
|
+
ctx: ExtensionCommandContext,
|
|
659
|
+
binding: DeveloperSettingsBinding,
|
|
660
|
+
): Promise<DeveloperSettingsNavigation | undefined> {
|
|
661
|
+
const result = await ctx.ui.custom<DeveloperSettingsNavigation | null>(
|
|
662
|
+
(tui, theme, _keybindings, done) =>
|
|
663
|
+
new DeveloperSettingsSurface(ctx, binding, theme, done, () =>
|
|
664
|
+
tui.requestRender(),
|
|
665
|
+
),
|
|
666
|
+
);
|
|
667
|
+
return result ?? undefined;
|
|
434
668
|
}
|
|
435
669
|
|
|
436
670
|
export function showPendingQuestionSelector(
|
|
437
|
-
|
|
438
|
-
|
|
671
|
+
ctx: ExtensionCommandContext,
|
|
672
|
+
questions: PendingQuestion[],
|
|
439
673
|
): Promise<string | undefined> {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
674
|
+
if (questions.length === 0) return Promise.resolve(undefined);
|
|
675
|
+
return showSelectDialog(
|
|
676
|
+
ctx,
|
|
677
|
+
{
|
|
678
|
+
title: "Resolve an open Developer question",
|
|
679
|
+
subtitle:
|
|
680
|
+
"Enter opens an answer/evidence editor; the question closes after a resolved or not-applicable judgment",
|
|
681
|
+
items: pendingQuestionItems(questions),
|
|
682
|
+
width: "92%",
|
|
683
|
+
minWidth: 88,
|
|
684
|
+
maxVisible: questions.length,
|
|
685
|
+
selectedLabelMaxLines: 5,
|
|
686
|
+
selectedDescriptionMaxLines: 3,
|
|
687
|
+
},
|
|
688
|
+
"surface",
|
|
689
|
+
);
|
|
452
690
|
}
|
|
453
691
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
692
|
+
export function showDeveloperHistorySelector(
|
|
693
|
+
ctx: ExtensionCommandContext,
|
|
694
|
+
state: DeveloperState,
|
|
695
|
+
initialRouteId?: string,
|
|
696
|
+
): Promise<string | undefined> {
|
|
697
|
+
const entries = developerHistoryEntries(state);
|
|
698
|
+
if (entries.length === 0) return Promise.resolve(undefined);
|
|
699
|
+
return showSelectDialog(
|
|
700
|
+
ctx,
|
|
701
|
+
{
|
|
702
|
+
title: `Developer history · ${entries.length}`,
|
|
703
|
+
subtitle:
|
|
704
|
+
"Enter opens complete route and judgment evidence; drag selects terminal text",
|
|
705
|
+
items: historySelectItems(entries),
|
|
706
|
+
width: "92%",
|
|
707
|
+
minWidth: 72,
|
|
708
|
+
maxVisible: entries.length,
|
|
709
|
+
selectedLabelMaxLines: 4,
|
|
710
|
+
selectedDescriptionMaxLines: 3,
|
|
711
|
+
initialValue: initialRouteId,
|
|
712
|
+
},
|
|
713
|
+
"surface",
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export type QuestionBriefAction = "continue" | "defer";
|
|
718
|
+
|
|
719
|
+
function questionGateExplanation(question: PendingQuestion): string {
|
|
720
|
+
if (question.gate === "before-implementation")
|
|
721
|
+
return "Implementation remains blocked until this question is resolved.";
|
|
722
|
+
if (question.gate === "before-completion")
|
|
723
|
+
return "Completion remains blocked until this question is resolved.";
|
|
724
|
+
return "This question does not currently block implementation or completion.";
|
|
457
725
|
}
|
|
458
726
|
|
|
727
|
+
export class DeveloperQuestionBriefPanel {
|
|
728
|
+
private cachedLines?: string[];
|
|
729
|
+
private cachedWidth?: number;
|
|
730
|
+
private readonly done: (action: QuestionBriefAction) => void;
|
|
731
|
+
private readonly keybindings: KeybindingsManager;
|
|
732
|
+
private readonly question: PendingQuestion;
|
|
733
|
+
private readonly requestRender: () => void;
|
|
734
|
+
private selected: QuestionBriefAction = "continue";
|
|
735
|
+
private readonly theme: Theme;
|
|
736
|
+
|
|
737
|
+
constructor(
|
|
738
|
+
question: PendingQuestion,
|
|
739
|
+
theme: Theme,
|
|
740
|
+
keybindings: KeybindingsManager,
|
|
741
|
+
done: (action: QuestionBriefAction) => void,
|
|
742
|
+
requestRender: () => void = () => {},
|
|
743
|
+
) {
|
|
744
|
+
this.question = question;
|
|
745
|
+
this.theme = theme;
|
|
746
|
+
this.keybindings = keybindings;
|
|
747
|
+
this.done = done;
|
|
748
|
+
this.requestRender = requestRender;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
handleInput(data: string): void {
|
|
752
|
+
if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
753
|
+
this.done("defer");
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (
|
|
757
|
+
this.keybindings.matches(data, "tui.select.up") ||
|
|
758
|
+
this.keybindings.matches(data, "tui.select.down")
|
|
759
|
+
) {
|
|
760
|
+
this.selected = this.selected === "continue" ? "defer" : "continue";
|
|
761
|
+
this.invalidate();
|
|
762
|
+
this.requestRender();
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (this.keybindings.matches(data, "tui.select.confirm"))
|
|
766
|
+
this.done(this.selected);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
render(width: number): string[] {
|
|
770
|
+
if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
|
|
771
|
+
|
|
772
|
+
const panelWidth = Math.max(1, width);
|
|
773
|
+
const innerWidth = Math.max(1, panelWidth - 2);
|
|
774
|
+
const rows: string[] = [];
|
|
775
|
+
const border = (text: string) => this.theme.fg("borderAccent", text);
|
|
776
|
+
const row = (content = "") =>
|
|
777
|
+
`${border("│")}${truncateToWidth(content, innerWidth, "", true)}${border("│")}`;
|
|
778
|
+
const section = (title: string) =>
|
|
779
|
+
rows.push(row(` ${this.theme.fg("accent", this.theme.bold(title))}`));
|
|
780
|
+
const addField = (
|
|
781
|
+
label: string,
|
|
782
|
+
value: string,
|
|
783
|
+
color: ThemeColor = "text",
|
|
784
|
+
) => {
|
|
785
|
+
const labelText = `${label} ·`;
|
|
786
|
+
const plainPrefix = ` ${labelText} `;
|
|
787
|
+
const styledPrefix = ` ${this.theme.fg("dim", labelText)} `;
|
|
788
|
+
const contentWidth = Math.max(1, innerWidth - visibleWidth(plainPrefix));
|
|
789
|
+
const wrapped = wrapTextWithAnsi(
|
|
790
|
+
this.theme.fg(color, value.length > 0 ? value : "(none)"),
|
|
791
|
+
contentWidth,
|
|
792
|
+
);
|
|
793
|
+
rows.push(row(styledPrefix + (wrapped[0] ?? "")));
|
|
794
|
+
const indent = " ".repeat(visibleWidth(plainPrefix));
|
|
795
|
+
for (const line of wrapped.slice(1)) rows.push(row(indent + line));
|
|
796
|
+
};
|
|
797
|
+
const addMarkdownField = (
|
|
798
|
+
label: string,
|
|
799
|
+
value: string,
|
|
800
|
+
color: ThemeColor = "text",
|
|
801
|
+
) => {
|
|
802
|
+
rows.push(row(` ${this.theme.fg("dim", `${label} ·`)}`));
|
|
803
|
+
const contentWidth = Math.max(1, innerWidth - 4);
|
|
804
|
+
const markdown = new Markdown(
|
|
805
|
+
value,
|
|
806
|
+
0,
|
|
807
|
+
0,
|
|
808
|
+
developerMarkdownTheme(this.theme),
|
|
809
|
+
{ color: (text) => this.theme.fg(color, text) },
|
|
810
|
+
);
|
|
811
|
+
for (const line of markdown.render(contentWidth))
|
|
812
|
+
rows.push(row(` ${line}`));
|
|
813
|
+
};
|
|
814
|
+
const action = (
|
|
815
|
+
value: QuestionBriefAction,
|
|
816
|
+
label: string,
|
|
817
|
+
description: string,
|
|
818
|
+
) => {
|
|
819
|
+
const selected = this.selected === value;
|
|
820
|
+
const cursor = selected ? this.theme.fg("accent", "→ ") : " ";
|
|
821
|
+
const labelColor: ThemeColor = selected ? "accent" : "text";
|
|
822
|
+
for (const [index, line] of wrapTextWithAnsi(
|
|
823
|
+
this.theme.fg(labelColor, selected ? this.theme.bold(label) : label),
|
|
824
|
+
Math.max(1, innerWidth - 2),
|
|
825
|
+
).entries())
|
|
826
|
+
rows.push(row(`${index === 0 ? cursor : " "}${line}`));
|
|
827
|
+
for (const line of wrapTextWithAnsi(
|
|
828
|
+
this.theme.fg("muted", description),
|
|
829
|
+
Math.max(1, innerWidth - 4),
|
|
830
|
+
))
|
|
831
|
+
rows.push(row(` ${line}`));
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
rows.push(border(`╭${"─".repeat(innerWidth)}╮`));
|
|
835
|
+
rows.push(
|
|
836
|
+
row(
|
|
837
|
+
` ${this.theme.fg("accent", this.theme.bold("◆ Developer decision brief"))}`,
|
|
838
|
+
),
|
|
839
|
+
);
|
|
840
|
+
rows.push(row());
|
|
841
|
+
|
|
842
|
+
section("Why this decision is required");
|
|
843
|
+
if (this.question.context)
|
|
844
|
+
addMarkdownField("context", this.question.context);
|
|
845
|
+
else
|
|
846
|
+
addField(
|
|
847
|
+
"context",
|
|
848
|
+
"No additional context was recorded for this legacy question.",
|
|
849
|
+
"warning",
|
|
850
|
+
);
|
|
851
|
+
rows.push(row());
|
|
852
|
+
|
|
853
|
+
section("Decision contract");
|
|
854
|
+
addField("question", this.question.question);
|
|
855
|
+
addField(
|
|
856
|
+
"owner / gate",
|
|
857
|
+
`${this.question.resolutionOwner} / ${this.question.gate}`,
|
|
858
|
+
"muted",
|
|
859
|
+
);
|
|
860
|
+
addField("blocked work", questionGateExplanation(this.question), "warning");
|
|
861
|
+
addField("resolves when", this.question.resolutionCriteria, "muted");
|
|
862
|
+
|
|
863
|
+
const fields = this.question.responseSpec?.fields ?? [];
|
|
864
|
+
if (fields.length > 0) {
|
|
865
|
+
rows.push(row());
|
|
866
|
+
section(`Choice preview · ${fields.length}`);
|
|
867
|
+
for (const [fieldIndex, field] of fields.entries()) {
|
|
868
|
+
addField(
|
|
869
|
+
`field ${fieldIndex + 1}/${fields.length} · ${field.id}`,
|
|
870
|
+
field.prompt,
|
|
871
|
+
"accent",
|
|
872
|
+
);
|
|
873
|
+
if (field.description)
|
|
874
|
+
addField("description", field.description, "muted");
|
|
875
|
+
for (const [optionIndex, option] of field.options.entries()) {
|
|
876
|
+
addField(
|
|
877
|
+
`option ${optionIndex + 1}`,
|
|
878
|
+
`${option.value} · ${option.label}`,
|
|
879
|
+
);
|
|
880
|
+
if (option.description)
|
|
881
|
+
addField("meaning", option.description, "muted");
|
|
882
|
+
if (option.detailPrompt)
|
|
883
|
+
addField("required detail", option.detailPrompt, "warning");
|
|
884
|
+
}
|
|
885
|
+
addField(
|
|
886
|
+
"custom answer",
|
|
887
|
+
"Available after continuing if none of the listed options fit.",
|
|
888
|
+
"dim",
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
rows.push(row());
|
|
894
|
+
section("Choose after reviewing the explanation above");
|
|
895
|
+
action(
|
|
896
|
+
"continue",
|
|
897
|
+
"Continue to answer",
|
|
898
|
+
"Open the response fields or editor now.",
|
|
899
|
+
);
|
|
900
|
+
action(
|
|
901
|
+
"defer",
|
|
902
|
+
"Leave open",
|
|
903
|
+
"Keep the question and its gate in Developer for later.",
|
|
904
|
+
);
|
|
905
|
+
rows.push(row());
|
|
906
|
+
for (const line of wrapTextWithAnsi(
|
|
907
|
+
this.theme.fg(
|
|
908
|
+
"dim",
|
|
909
|
+
"mouse wheel scroll · drag select · Cmd+C copy · ↑↓ choose · enter continue · esc leave open",
|
|
910
|
+
),
|
|
911
|
+
Math.max(1, innerWidth - 2),
|
|
912
|
+
))
|
|
913
|
+
rows.push(row(` ${line}`));
|
|
914
|
+
rows.push(border(`╰${"─".repeat(innerWidth)}╯`));
|
|
915
|
+
|
|
916
|
+
this.cachedWidth = width;
|
|
917
|
+
this.cachedLines = rows;
|
|
918
|
+
return rows;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
invalidate(): void {
|
|
922
|
+
this.cachedLines = undefined;
|
|
923
|
+
this.cachedWidth = undefined;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export async function showDeveloperQuestionBrief(
|
|
928
|
+
ctx: ExtensionContext,
|
|
929
|
+
question: PendingQuestion,
|
|
930
|
+
): Promise<QuestionBriefAction> {
|
|
931
|
+
const action = await ctx.ui.custom<QuestionBriefAction | null>(
|
|
932
|
+
(tui, theme, keybindings, done) =>
|
|
933
|
+
new DeveloperQuestionBriefPanel(
|
|
934
|
+
question,
|
|
935
|
+
theme,
|
|
936
|
+
keybindings,
|
|
937
|
+
(value) => done(value),
|
|
938
|
+
() => tui.requestRender(),
|
|
939
|
+
),
|
|
940
|
+
);
|
|
941
|
+
return action ?? "defer";
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const CUSTOM_CHOICE_VALUE = "__custom__";
|
|
945
|
+
|
|
946
|
+
type ChoiceResponseAnswer =
|
|
947
|
+
| { kind: "preset"; option: ChoiceResponseOption; detail?: string }
|
|
948
|
+
| { kind: "custom"; text: string };
|
|
949
|
+
|
|
459
950
|
function responseWithinLimits(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
951
|
+
ctx: ExtensionContext,
|
|
952
|
+
request: string,
|
|
953
|
+
label = "Question response",
|
|
463
954
|
): boolean {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
955
|
+
const requestBytes = new TextEncoder().encode(request).byteLength;
|
|
956
|
+
const requestLines = request.split(/\r?\n/).length;
|
|
957
|
+
if (
|
|
958
|
+
requestBytes <= MAX_IMMEDIATE_REQUEST_BYTES &&
|
|
959
|
+
requestLines <= MAX_IMMEDIATE_REQUEST_LINES
|
|
960
|
+
) {
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
ctx.ui.notify(
|
|
964
|
+
`${label} is too large (${requestBytes} bytes, ${requestLines} lines); shorten it before submitting.`,
|
|
965
|
+
"warning",
|
|
966
|
+
);
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function choiceDetailInitial(
|
|
971
|
+
field: ChoiceResponseField,
|
|
972
|
+
option: ChoiceResponseOption,
|
|
973
|
+
): string {
|
|
974
|
+
return [
|
|
975
|
+
`Decision: ${field.prompt}`,
|
|
976
|
+
`Selected: ${option.value} — ${option.label}`,
|
|
977
|
+
"",
|
|
978
|
+
option.detailPrompt,
|
|
979
|
+
"",
|
|
980
|
+
"Required detail:",
|
|
981
|
+
].join("\n");
|
|
485
982
|
}
|
|
486
983
|
|
|
487
984
|
async function editChoiceDetail(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
985
|
+
ctx: ExtensionContext,
|
|
986
|
+
field: ChoiceResponseField,
|
|
987
|
+
option: ChoiceResponseOption,
|
|
491
988
|
): Promise<string | undefined> {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
989
|
+
const initial = choiceDetailInitial(field, option);
|
|
990
|
+
while (true) {
|
|
991
|
+
const response = await ctx.ui.editor(
|
|
992
|
+
`Add detail for ${field.id} · ${option.value}`,
|
|
993
|
+
initial,
|
|
994
|
+
);
|
|
995
|
+
if (response === undefined) return undefined;
|
|
996
|
+
const detail = (
|
|
997
|
+
response.startsWith(initial) ? response.slice(initial.length) : response
|
|
998
|
+
).trim();
|
|
999
|
+
if (!detail) {
|
|
1000
|
+
ctx.ui.notify("This choice requires a non-empty detail.", "warning");
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
if (!responseWithinLimits(ctx, detail, "Choice detail")) continue;
|
|
1004
|
+
return detail;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async function editCustomChoiceAnswer(
|
|
1009
|
+
ctx: ExtensionContext,
|
|
1010
|
+
field: ChoiceResponseField,
|
|
1011
|
+
): Promise<string | undefined> {
|
|
1012
|
+
const context = field.description
|
|
1013
|
+
? `${field.prompt}\n\n${field.description}`
|
|
1014
|
+
: field.prompt;
|
|
1015
|
+
const prefix = `${context}\n\nYour answer:\n`;
|
|
1016
|
+
let initial = prefix;
|
|
1017
|
+
while (true) {
|
|
1018
|
+
const response = await ctx.ui.editor(
|
|
1019
|
+
`Write another answer · ${field.id}`,
|
|
1020
|
+
initial,
|
|
1021
|
+
);
|
|
1022
|
+
if (response === undefined) return undefined;
|
|
1023
|
+
const text = (
|
|
1024
|
+
response.startsWith(prefix) ? response.slice(prefix.length) : response
|
|
1025
|
+
).trim();
|
|
1026
|
+
if (!text) {
|
|
1027
|
+
ctx.ui.notify(
|
|
1028
|
+
"Write a non-empty answer or press Esc to go back.",
|
|
1029
|
+
"warning",
|
|
1030
|
+
);
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (!responseWithinLimits(ctx, text, "Custom answer")) {
|
|
1034
|
+
initial = `${prefix}${text}`;
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
return text;
|
|
1038
|
+
}
|
|
506
1039
|
}
|
|
507
1040
|
|
|
508
1041
|
function structuredResolutionRequest(
|
|
509
|
-
|
|
510
|
-
|
|
1042
|
+
question: PendingQuestion,
|
|
1043
|
+
answers: ReadonlyArray<ChoiceResponseAnswer | undefined>,
|
|
511
1044
|
): string {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
1045
|
+
const fields = question.responseSpec?.fields ?? [];
|
|
1046
|
+
const lines = [questionResolutionPrompt(question), "", "Structured answer:"];
|
|
1047
|
+
for (const [index, field] of fields.entries()) {
|
|
1048
|
+
const answer = answers[index];
|
|
1049
|
+
if (!answer) continue;
|
|
1050
|
+
if (answer.kind === "custom") {
|
|
1051
|
+
const [firstLine = "", ...remainingLines] = answer.text.split(/\r?\n/);
|
|
1052
|
+
lines.push(`- ${field.id}: custom — user wrote: ${firstLine}`);
|
|
1053
|
+
lines.push(...remainingLines.map((line) => ` ${line}`));
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
lines.push(
|
|
1057
|
+
`- ${field.id}: ${answer.option.value} — ${answer.option.label}`,
|
|
1058
|
+
);
|
|
1059
|
+
if (answer.detail) lines.push(` Detail: ${answer.detail}`);
|
|
1060
|
+
}
|
|
1061
|
+
return lines.join("\n");
|
|
521
1062
|
}
|
|
522
1063
|
|
|
523
1064
|
type ChoiceFieldAction =
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1065
|
+
| { kind: "answer"; answer: ChoiceResponseAnswer }
|
|
1066
|
+
| { kind: "cancel" }
|
|
1067
|
+
| { kind: "retry" };
|
|
527
1068
|
|
|
528
1069
|
type ChoiceReviewAction =
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
1070
|
+
| { kind: "submit" }
|
|
1071
|
+
| { kind: "edit"; fieldIndex: number }
|
|
1072
|
+
| { kind: "back" };
|
|
532
1073
|
|
|
533
1074
|
async function selectChoiceAnswer(
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
1075
|
+
ctx: ExtensionContext,
|
|
1076
|
+
field: ChoiceResponseField,
|
|
1077
|
+
fieldIndex: number,
|
|
1078
|
+
fieldCount: number,
|
|
538
1079
|
): Promise<ChoiceFieldAction> {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
1080
|
+
const selectedValue = await showSelectDialog(
|
|
1081
|
+
ctx,
|
|
1082
|
+
{
|
|
1083
|
+
title: `Decision ${fieldIndex + 1}/${fieldCount} · ${field.id}`,
|
|
1084
|
+
subtitle: field.description ?? field.prompt,
|
|
1085
|
+
items: [
|
|
1086
|
+
...field.options.map((option) => ({
|
|
1087
|
+
value: option.value,
|
|
1088
|
+
label: `${option.value} · ${option.label}`,
|
|
1089
|
+
description: option.description ?? field.prompt,
|
|
1090
|
+
})),
|
|
1091
|
+
{
|
|
1092
|
+
value: CUSTOM_CHOICE_VALUE,
|
|
1093
|
+
label: "Write another answer…",
|
|
1094
|
+
description: "Enter an answer that is not listed above",
|
|
1095
|
+
},
|
|
1096
|
+
],
|
|
1097
|
+
width: "92%",
|
|
1098
|
+
minWidth: 88,
|
|
1099
|
+
maxVisible: field.options.length + 1,
|
|
1100
|
+
selectedLabelMaxLines: 3,
|
|
1101
|
+
selectedDescriptionMaxLines: 4,
|
|
1102
|
+
},
|
|
1103
|
+
"surface",
|
|
1104
|
+
);
|
|
1105
|
+
if (!selectedValue) return { kind: "cancel" };
|
|
1106
|
+
if (selectedValue === CUSTOM_CHOICE_VALUE) {
|
|
1107
|
+
const text = await editCustomChoiceAnswer(ctx, field);
|
|
1108
|
+
return text === undefined
|
|
1109
|
+
? { kind: "retry" }
|
|
1110
|
+
: { kind: "answer", answer: { kind: "custom", text } };
|
|
1111
|
+
}
|
|
1112
|
+
const option = field.options.find(
|
|
1113
|
+
(candidate) => candidate.value === selectedValue,
|
|
1114
|
+
);
|
|
1115
|
+
if (!option) return { kind: "retry" };
|
|
1116
|
+
const detail = option.detailPrompt
|
|
1117
|
+
? await editChoiceDetail(ctx, field, option)
|
|
1118
|
+
: undefined;
|
|
1119
|
+
if (option.detailPrompt && detail === undefined) return { kind: "retry" };
|
|
1120
|
+
return { kind: "answer", answer: { kind: "preset", option, detail } };
|
|
559
1121
|
}
|
|
560
1122
|
|
|
561
1123
|
async function reviewChoiceAnswers(
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
1124
|
+
ctx: ExtensionContext,
|
|
1125
|
+
question: PendingQuestion,
|
|
1126
|
+
fields: ChoiceResponseField[],
|
|
1127
|
+
answers: ReadonlyArray<ChoiceResponseAnswer | undefined>,
|
|
566
1128
|
): Promise<ChoiceReviewAction> {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
1129
|
+
const action = await showSelectDialog(
|
|
1130
|
+
ctx,
|
|
1131
|
+
{
|
|
1132
|
+
title: "Review structured answer",
|
|
1133
|
+
subtitle: question.question,
|
|
1134
|
+
items: [
|
|
1135
|
+
{
|
|
1136
|
+
value: "submit",
|
|
1137
|
+
label: "Submit answers",
|
|
1138
|
+
description:
|
|
1139
|
+
"Send these decisions to Pi; the question remains open until judgment",
|
|
1140
|
+
},
|
|
1141
|
+
...fields.map((field, index) => {
|
|
1142
|
+
const answer = answers[index];
|
|
1143
|
+
if (!answer) {
|
|
1144
|
+
return {
|
|
1145
|
+
value: `edit:${index}`,
|
|
1146
|
+
label: `${field.id} · missing — Missing answer`,
|
|
1147
|
+
description: "Enter to answer this decision",
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
if (answer.kind === "custom") {
|
|
1151
|
+
return {
|
|
1152
|
+
value: `edit:${index}`,
|
|
1153
|
+
label: `${field.id} · custom — ${answer.text.replace(/\s+/g, " ").trim()}`,
|
|
1154
|
+
description: "User-entered answer · Enter to change this answer",
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
value: `edit:${index}`,
|
|
1159
|
+
label: `${field.id} · ${answer.option.value} — ${answer.option.label}`,
|
|
1160
|
+
description: answer.detail
|
|
1161
|
+
? `Detail: ${answer.detail}`
|
|
1162
|
+
: "Enter to change this answer",
|
|
1163
|
+
};
|
|
1164
|
+
}),
|
|
1165
|
+
],
|
|
1166
|
+
width: "92%",
|
|
1167
|
+
minWidth: 88,
|
|
1168
|
+
maxVisible: fields.length + 1,
|
|
1169
|
+
selectedLabelMaxLines: 3,
|
|
1170
|
+
selectedDescriptionMaxLines: 4,
|
|
1171
|
+
},
|
|
1172
|
+
"surface",
|
|
1173
|
+
);
|
|
1174
|
+
if (action === "submit") return { kind: "submit" };
|
|
1175
|
+
if (!action?.startsWith("edit:")) return { kind: "back" };
|
|
1176
|
+
const fieldIndex = Number(action.slice("edit:".length));
|
|
1177
|
+
if (
|
|
1178
|
+
!Number.isInteger(fieldIndex) ||
|
|
1179
|
+
fieldIndex < 0 ||
|
|
1180
|
+
fieldIndex >= fields.length
|
|
1181
|
+
) {
|
|
1182
|
+
return { kind: "back" };
|
|
1183
|
+
}
|
|
1184
|
+
return { kind: "edit", fieldIndex };
|
|
598
1185
|
}
|
|
599
1186
|
|
|
600
1187
|
function previousChoiceField(
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
1188
|
+
fieldIndex: number,
|
|
1189
|
+
fieldCount: number,
|
|
1190
|
+
returnToReview: boolean,
|
|
604
1191
|
): number | undefined {
|
|
605
|
-
|
|
606
|
-
|
|
1192
|
+
if (returnToReview) return fieldCount;
|
|
1193
|
+
return fieldIndex === 0 ? undefined : fieldIndex - 1;
|
|
607
1194
|
}
|
|
608
1195
|
|
|
609
1196
|
async function collectChoiceResponse(
|
|
610
|
-
|
|
611
|
-
|
|
1197
|
+
ctx: ExtensionContext,
|
|
1198
|
+
question: PendingQuestion,
|
|
612
1199
|
): Promise<string | undefined> {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1200
|
+
const fields = question.responseSpec?.fields;
|
|
1201
|
+
if (!fields || fields.length === 0) return undefined;
|
|
1202
|
+
const answers: Array<ChoiceResponseAnswer | undefined> = [];
|
|
1203
|
+
let fieldIndex = 0;
|
|
1204
|
+
let returnToReview = false;
|
|
1205
|
+
|
|
1206
|
+
while (true) {
|
|
1207
|
+
if (fieldIndex < fields.length) {
|
|
1208
|
+
const field = fields[fieldIndex];
|
|
1209
|
+
if (!field) return undefined;
|
|
1210
|
+
const action = await selectChoiceAnswer(
|
|
1211
|
+
ctx,
|
|
1212
|
+
field,
|
|
1213
|
+
fieldIndex,
|
|
1214
|
+
fields.length,
|
|
1215
|
+
);
|
|
1216
|
+
if (action.kind === "retry") continue;
|
|
1217
|
+
if (action.kind === "cancel") {
|
|
1218
|
+
fieldIndex =
|
|
1219
|
+
previousChoiceField(fieldIndex, fields.length, returnToReview) ?? -1;
|
|
1220
|
+
}
|
|
1221
|
+
if (fieldIndex < 0) return undefined;
|
|
1222
|
+
if (action.kind === "cancel") continue;
|
|
1223
|
+
answers[fieldIndex] = action.answer;
|
|
1224
|
+
fieldIndex = returnToReview ? fields.length : fieldIndex + 1;
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const action = await reviewChoiceAnswers(ctx, question, fields, answers);
|
|
1229
|
+
if (action.kind === "submit")
|
|
1230
|
+
return structuredResolutionRequest(question, answers);
|
|
1231
|
+
if (action.kind === "edit") {
|
|
1232
|
+
fieldIndex = action.fieldIndex;
|
|
1233
|
+
returnToReview = true;
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
returnToReview = false;
|
|
1237
|
+
fieldIndex = fields.length - 1;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
interface QuestionEditorSpec {
|
|
1242
|
+
title: string;
|
|
1243
|
+
initial: string;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
async function prepareQuestionResolution(
|
|
1247
|
+
ctx: ExtensionContext,
|
|
1248
|
+
question: PendingQuestion,
|
|
1249
|
+
editor: QuestionEditorSpec,
|
|
650
1250
|
): Promise<ImmediateQuestionDisposition> {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
selectedDescriptionMaxLines: 3,
|
|
673
|
-
});
|
|
674
|
-
if (action !== "answer") return { kind: "defer" };
|
|
675
|
-
|
|
676
|
-
const request = question.responseSpec
|
|
677
|
-
? await collectChoiceResponse(ctx, question)
|
|
678
|
-
: await ctx.ui.editor(
|
|
679
|
-
"Answer the new Developer question",
|
|
680
|
-
questionResolutionPrompt(question),
|
|
681
|
-
);
|
|
682
|
-
if (request === undefined) continue;
|
|
683
|
-
if (!responseWithinLimits(ctx, request)) continue;
|
|
684
|
-
return { kind: "answer", request };
|
|
685
|
-
}
|
|
1251
|
+
while (true) {
|
|
1252
|
+
const action = await showDeveloperQuestionBrief(ctx, question);
|
|
1253
|
+
if (action === "defer") return { kind: "defer" };
|
|
1254
|
+
|
|
1255
|
+
const request = question.responseSpec
|
|
1256
|
+
? await collectChoiceResponse(ctx, question)
|
|
1257
|
+
: await ctx.ui.editor(editor.title, editor.initial);
|
|
1258
|
+
if (request === undefined) continue;
|
|
1259
|
+
if (!responseWithinLimits(ctx, request)) continue;
|
|
1260
|
+
return { kind: "answer", request };
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
export function promptImmediateUserQuestion(
|
|
1265
|
+
ctx: ExtensionContext,
|
|
1266
|
+
question: PendingQuestion,
|
|
1267
|
+
): Promise<ImmediateQuestionDisposition> {
|
|
1268
|
+
return prepareQuestionResolution(ctx, question, {
|
|
1269
|
+
title: "Answer the new Developer question",
|
|
1270
|
+
initial: questionResolutionPrompt(question),
|
|
1271
|
+
});
|
|
686
1272
|
}
|
|
687
1273
|
|
|
688
1274
|
export class DeveloperWidget {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1275
|
+
private readonly state: DeveloperState;
|
|
1276
|
+
private readonly theme: Theme;
|
|
1277
|
+
|
|
1278
|
+
constructor(state: DeveloperState, theme: Theme) {
|
|
1279
|
+
this.state = state;
|
|
1280
|
+
this.theme = theme;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
render(width: number): string[] {
|
|
1284
|
+
const lines: string[] = [];
|
|
1285
|
+
if (this.state.activeRoute) {
|
|
1286
|
+
lines.push(
|
|
1287
|
+
truncateToWidth(
|
|
1288
|
+
`${this.theme.fg("accent", "◆ route")} ${this.theme.fg("muted", "·")} ${this.theme.fg("accent", this.state.activeRoute.target)} ${this.theme.fg("muted", this.state.activeRoute.question)}`,
|
|
1289
|
+
width,
|
|
1290
|
+
"…",
|
|
1291
|
+
),
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
for (const question of this.state.pendingQuestions.slice(0, 3)) {
|
|
1295
|
+
const label =
|
|
1296
|
+
question.resolutionOwner === "user" ? "? answer" : "? evidence";
|
|
1297
|
+
lines.push(
|
|
1298
|
+
truncateToWidth(
|
|
1299
|
+
`${this.theme.fg(question.status === "blocked" ? "error" : "warning", label)} ${this.theme.fg("dim", `· ${question.gate} ·`)} ${this.theme.fg("muted", question.question)}`,
|
|
1300
|
+
width,
|
|
1301
|
+
"…",
|
|
1302
|
+
),
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
if (this.state.pendingQuestions.length > 3) {
|
|
1306
|
+
lines.push(
|
|
1307
|
+
this.theme.fg(
|
|
1308
|
+
"dim",
|
|
1309
|
+
` +${this.state.pendingQuestions.length - 3} more open questions`,
|
|
1310
|
+
),
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
if (this.state.implementationFramingRequired) {
|
|
1314
|
+
lines.push(
|
|
1315
|
+
this.theme.fg(
|
|
1316
|
+
"warning",
|
|
1317
|
+
"◇ gate · frame implementation before mutation (sketch or signal)",
|
|
1318
|
+
),
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
if (this.state.rerouteRequired) {
|
|
1322
|
+
lines.push(
|
|
1323
|
+
this.theme.fg(
|
|
1324
|
+
"warning",
|
|
1325
|
+
"→ next · reroute from the latest implementation landing",
|
|
1326
|
+
),
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
if (this.state.verificationRequired) {
|
|
1330
|
+
lines.push(
|
|
1331
|
+
this.theme.fg(
|
|
1332
|
+
"warning",
|
|
1333
|
+
"→ next · verify changed artifacts before completion",
|
|
1334
|
+
),
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
return lines;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
invalidate(): void {}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
export class DeveloperHistoryDetailPanel {
|
|
1344
|
+
private cachedLines?: string[];
|
|
1345
|
+
private cachedWidth?: number;
|
|
1346
|
+
private readonly entry: DeveloperHistoryEntry;
|
|
1347
|
+
private readonly onClose: () => void;
|
|
1348
|
+
private readonly theme: Theme;
|
|
1349
|
+
|
|
1350
|
+
constructor(entry: DeveloperHistoryEntry, theme: Theme, onClose: () => void) {
|
|
1351
|
+
this.entry = entry;
|
|
1352
|
+
this.theme = theme;
|
|
1353
|
+
this.onClose = onClose;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
handleInput(data: string): void {
|
|
1357
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c"))
|
|
1358
|
+
this.onClose();
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
render(width: number): string[] {
|
|
1362
|
+
if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
|
|
1363
|
+
|
|
1364
|
+
const panelWidth = Math.max(1, width);
|
|
1365
|
+
const innerWidth = Math.max(1, panelWidth - 2);
|
|
1366
|
+
const body: string[] = [];
|
|
1367
|
+
const border = (text: string) => this.theme.fg("borderAccent", text);
|
|
1368
|
+
const row = (content = "") =>
|
|
1369
|
+
`${border("│")}${truncateToWidth(content, innerWidth, "", true)}${border("│")}`;
|
|
1370
|
+
const section = (title: string) =>
|
|
1371
|
+
body.push(row(` ${this.theme.fg("accent", this.theme.bold(title))}`));
|
|
1372
|
+
const addField = (
|
|
1373
|
+
label: string,
|
|
1374
|
+
value: string,
|
|
1375
|
+
color: ThemeColor = "text",
|
|
1376
|
+
) => {
|
|
1377
|
+
const labelText = `${label} ·`;
|
|
1378
|
+
const plainPrefix = ` ${labelText} `;
|
|
1379
|
+
const styledPrefix = ` ${this.theme.fg("dim", labelText)} `;
|
|
1380
|
+
const contentWidth = Math.max(1, innerWidth - visibleWidth(plainPrefix));
|
|
1381
|
+
const wrapped = wrapTextWithAnsi(
|
|
1382
|
+
this.theme.fg(color, value.length > 0 ? value : "(none)"),
|
|
1383
|
+
contentWidth,
|
|
1384
|
+
);
|
|
1385
|
+
body.push(row(styledPrefix + (wrapped[0] ?? "")));
|
|
1386
|
+
const indent = " ".repeat(visibleWidth(plainPrefix));
|
|
1387
|
+
for (const line of wrapped.slice(1)) body.push(row(indent + line));
|
|
1388
|
+
};
|
|
1389
|
+
const addValues = (label: string, values: string[]) => {
|
|
1390
|
+
if (values.length === 0) {
|
|
1391
|
+
addField(label, "(none)", "dim");
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
values.forEach((value, index) => {
|
|
1395
|
+
addField(`${label} ${index + 1}`, value, "muted");
|
|
1396
|
+
});
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
const { judgment, route } = this.entry;
|
|
1400
|
+
section("Route");
|
|
1401
|
+
addField("id", judgment.routeId, "dim");
|
|
1402
|
+
if (route) {
|
|
1403
|
+
addField("target", route.target, "accent");
|
|
1404
|
+
addField("question", route.question);
|
|
1405
|
+
addField("reason", route.reason, "muted");
|
|
1406
|
+
addField(
|
|
1407
|
+
"method",
|
|
1408
|
+
route.methodLocation ?? "implementation action",
|
|
1409
|
+
"dim",
|
|
1410
|
+
);
|
|
1411
|
+
if (route.executionProfile)
|
|
1412
|
+
addField("execution profile", route.executionProfile, "dim");
|
|
1413
|
+
if (route.targetQuestionId)
|
|
1414
|
+
addField("target question", route.targetQuestionId, "dim");
|
|
1415
|
+
if (route.implementationStep) {
|
|
1416
|
+
addField("movement", route.implementationStep.movement);
|
|
1417
|
+
addField("stop condition", route.implementationStep.stopCondition);
|
|
1418
|
+
addField("verification", route.implementationStep.verification);
|
|
1419
|
+
}
|
|
1420
|
+
addValues("known evidence", route.knownEvidence);
|
|
1421
|
+
addValues(
|
|
1422
|
+
"considered alternative",
|
|
1423
|
+
route.consideredAlternatives.map(
|
|
1424
|
+
(alternative) => `${alternative.target} · ${alternative.reason}`,
|
|
1425
|
+
),
|
|
1426
|
+
);
|
|
1427
|
+
} else {
|
|
1428
|
+
addField(
|
|
1429
|
+
"details",
|
|
1430
|
+
"Route details unavailable for this recorded judgment.",
|
|
1431
|
+
"warning",
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
body.push(row());
|
|
1436
|
+
section("Judgment");
|
|
1437
|
+
addField("target", judgment.target, "accent");
|
|
1438
|
+
addField("status", judgment.status, judgmentColor(judgment.status));
|
|
1439
|
+
addField("question", judgment.question);
|
|
1440
|
+
addField("result", judgment.result);
|
|
1441
|
+
addField(
|
|
1442
|
+
"changed artifacts",
|
|
1443
|
+
judgment.changedArtifacts ? "yes" : "no",
|
|
1444
|
+
judgment.changedArtifacts ? "warning" : "dim",
|
|
1445
|
+
);
|
|
1446
|
+
addValues("basis", judgment.basis);
|
|
1447
|
+
for (const [index, question] of judgment.openedQuestions.entries()) {
|
|
1448
|
+
addField(
|
|
1449
|
+
`opened question ${index + 1}`,
|
|
1450
|
+
`${question.id} · ${question.status} · ${question.resolutionOwner} · ${question.gate} · ${question.question}`,
|
|
1451
|
+
"warning",
|
|
1452
|
+
);
|
|
1453
|
+
addField("source route", question.sourceRouteId, "dim");
|
|
1454
|
+
if (question.context) addField("context", question.context, "muted");
|
|
1455
|
+
addField("resolves when", question.resolutionCriteria, "muted");
|
|
1456
|
+
if (question.responseSpec)
|
|
1457
|
+
addField("response spec", JSON.stringify(question.responseSpec), "dim");
|
|
1458
|
+
}
|
|
1459
|
+
for (const [index, update] of judgment.questionUpdates.entries()) {
|
|
1460
|
+
addField(
|
|
1461
|
+
`question update ${index + 1}`,
|
|
1462
|
+
`${update.questionId} · ${update.status} · ${update.result}`,
|
|
1463
|
+
"accent",
|
|
1464
|
+
);
|
|
1465
|
+
addValues("update basis", update.basis);
|
|
1466
|
+
}
|
|
1467
|
+
addValues("artifact", judgment.artifacts);
|
|
1468
|
+
|
|
1469
|
+
const header = [
|
|
1470
|
+
border(`╭${"─".repeat(innerWidth)}╮`),
|
|
1471
|
+
row(
|
|
1472
|
+
` ${this.theme.fg("accent", this.theme.bold("◆ Developer history detail"))}`,
|
|
1473
|
+
),
|
|
1474
|
+
];
|
|
1475
|
+
const hint = wrapTextWithAnsi(
|
|
1476
|
+
this.theme.fg(
|
|
1477
|
+
"dim",
|
|
1478
|
+
"mouse wheel scroll · drag select · Cmd+C copy · esc back",
|
|
1479
|
+
),
|
|
1480
|
+
Math.max(1, innerWidth - 2),
|
|
1481
|
+
).map((line) => row(` ${line}`));
|
|
1482
|
+
const lines = [
|
|
1483
|
+
...header,
|
|
1484
|
+
...body,
|
|
1485
|
+
...hint,
|
|
1486
|
+
border(`╰${"─".repeat(innerWidth)}╯`),
|
|
1487
|
+
];
|
|
1488
|
+
this.cachedWidth = width;
|
|
1489
|
+
this.cachedLines = lines;
|
|
1490
|
+
return lines;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
invalidate(): void {
|
|
1494
|
+
this.cachedLines = undefined;
|
|
1495
|
+
this.cachedWidth = undefined;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
export async function showDeveloperHistoryDetail(
|
|
1500
|
+
ctx: ExtensionCommandContext,
|
|
1501
|
+
entry: DeveloperHistoryEntry,
|
|
1502
|
+
): Promise<void> {
|
|
1503
|
+
await ctx.ui.custom<void>(
|
|
1504
|
+
(_tui, theme, _keybindings, done) =>
|
|
1505
|
+
new DeveloperHistoryDetailPanel(entry, theme, () => done()),
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
interface DeveloperStatusView {
|
|
1510
|
+
state: DeveloperState;
|
|
1511
|
+
activeTools: string[];
|
|
1512
|
+
availableSkills: string[];
|
|
744
1513
|
}
|
|
745
1514
|
|
|
746
1515
|
export class DeveloperStatusPanel {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1516
|
+
private cachedWidth?: number;
|
|
1517
|
+
private cachedLines?: string[];
|
|
1518
|
+
private readonly view: DeveloperStatusView;
|
|
1519
|
+
private readonly theme: Theme;
|
|
1520
|
+
private readonly onClose: () => void;
|
|
1521
|
+
|
|
1522
|
+
constructor(view: DeveloperStatusView, theme: Theme, onClose: () => void) {
|
|
1523
|
+
this.view = view;
|
|
1524
|
+
this.theme = theme;
|
|
1525
|
+
this.onClose = onClose;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
handleInput(data: string): void {
|
|
1529
|
+
if (
|
|
1530
|
+
matchesKey(data, "escape") ||
|
|
1531
|
+
matchesKey(data, "enter") ||
|
|
1532
|
+
matchesKey(data, "ctrl+c")
|
|
1533
|
+
)
|
|
1534
|
+
this.onClose();
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
render(width: number): string[] {
|
|
1538
|
+
if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
|
|
1539
|
+
|
|
1540
|
+
const panelWidth = Math.max(1, width);
|
|
1541
|
+
const innerWidth = Math.max(1, panelWidth - 2);
|
|
1542
|
+
const rows: string[] = [];
|
|
1543
|
+
const border = (text: string) => this.theme.fg("borderAccent", text);
|
|
1544
|
+
const row = (content = "") =>
|
|
1545
|
+
`${border("│")}${truncateToWidth(content, innerWidth, "…", true)}${border("│")}`;
|
|
1546
|
+
const addWrapped = (
|
|
1547
|
+
label: string,
|
|
1548
|
+
value: string,
|
|
1549
|
+
color: ThemeColor = "muted",
|
|
1550
|
+
maxLines = 2,
|
|
1551
|
+
) => {
|
|
1552
|
+
const labelText = `${label} ·`;
|
|
1553
|
+
const plainPrefix = ` ${labelText} `;
|
|
1554
|
+
const styledPrefix = ` ${this.theme.fg("dim", labelText)} `;
|
|
1555
|
+
const contentWidth = Math.max(1, innerWidth - visibleWidth(plainPrefix));
|
|
1556
|
+
const wrapped = wrapTextWithAnsi(
|
|
1557
|
+
this.theme.fg(color, value.trim()),
|
|
1558
|
+
contentWidth,
|
|
1559
|
+
);
|
|
1560
|
+
const visible = wrapped.slice(0, Math.max(1, maxLines));
|
|
1561
|
+
if (wrapped.length > visible.length && visible.length > 0) {
|
|
1562
|
+
const last = visible.length - 1;
|
|
1563
|
+
visible[last] =
|
|
1564
|
+
truncateToWidth(
|
|
1565
|
+
visible[last] ?? "",
|
|
1566
|
+
Math.max(1, contentWidth - 1),
|
|
1567
|
+
"",
|
|
1568
|
+
) + this.theme.fg("dim", "…");
|
|
1569
|
+
}
|
|
1570
|
+
rows.push(row(styledPrefix + (visible[0] ?? "")));
|
|
1571
|
+
const hangingIndent = " ".repeat(visibleWidth(plainPrefix));
|
|
1572
|
+
for (const line of visible.slice(1)) rows.push(row(hangingIndent + line));
|
|
1573
|
+
};
|
|
1574
|
+
const section = (title: string) =>
|
|
1575
|
+
rows.push(row(` ${this.theme.fg("accent", this.theme.bold(title))}`));
|
|
1576
|
+
|
|
1577
|
+
rows.push(border(`╭${"─".repeat(innerWidth)}╮`));
|
|
1578
|
+
rows.push(
|
|
1579
|
+
row(
|
|
1580
|
+
` ${this.theme.fg("accent", this.theme.bold("◆ Developer status"))}`,
|
|
1581
|
+
),
|
|
1582
|
+
);
|
|
1583
|
+
rows.push(row());
|
|
1584
|
+
|
|
1585
|
+
const state = this.view.state;
|
|
1586
|
+
const currentProtocol = protocolState(state);
|
|
1587
|
+
const summary =
|
|
1588
|
+
`Developer ${this.theme.fg(state.enabled ? "success" : "dim", state.enabled ? "on" : "off")}` +
|
|
1589
|
+
this.theme.fg("dim", " · ") +
|
|
1590
|
+
`protocol ${this.theme.fg(protocolColor(currentProtocol), currentProtocol)}` +
|
|
1591
|
+
this.theme.fg("dim", " · ") +
|
|
1592
|
+
`target ${this.theme.fg("muted", state.activeRoute?.target ?? "none")}`;
|
|
1593
|
+
for (const line of wrapTextWithAnsi(summary, Math.max(1, innerWidth - 2)))
|
|
1594
|
+
rows.push(row(` ${line}`));
|
|
1595
|
+
rows.push(row());
|
|
1596
|
+
|
|
1597
|
+
section("Active route");
|
|
1598
|
+
if (state.activeRoute) {
|
|
1599
|
+
addWrapped("id", state.activeRoute.routeId, "dim", 1);
|
|
1600
|
+
addWrapped("question", state.activeRoute.question, "text", 3);
|
|
1601
|
+
addWrapped("reason", state.activeRoute.reason, "muted", 2);
|
|
1602
|
+
addWrapped(
|
|
1603
|
+
"skill",
|
|
1604
|
+
state.activeRoute.methodLocation ?? "implementation action",
|
|
1605
|
+
"dim",
|
|
1606
|
+
1,
|
|
1607
|
+
);
|
|
1608
|
+
addWrapped(
|
|
1609
|
+
"known evidence",
|
|
1610
|
+
String(state.activeRoute.knownEvidence.length),
|
|
1611
|
+
"muted",
|
|
1612
|
+
1,
|
|
1613
|
+
);
|
|
1614
|
+
} else {
|
|
1615
|
+
addWrapped(
|
|
1616
|
+
"state",
|
|
1617
|
+
"No route is currently waiting for judgment.",
|
|
1618
|
+
"dim",
|
|
1619
|
+
2,
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
rows.push(row());
|
|
1624
|
+
section(`Open questions · ${state.pendingQuestions.length}`);
|
|
1625
|
+
if (state.pendingQuestions.length === 0) {
|
|
1626
|
+
addWrapped("state", "No unresolved Developer questions.", "dim", 2);
|
|
1627
|
+
} else {
|
|
1628
|
+
for (const question of state.pendingQuestions.slice(0, 4)) {
|
|
1629
|
+
addWrapped(
|
|
1630
|
+
`${question.status} · ${question.resolutionOwner} · ${question.gate}`,
|
|
1631
|
+
question.question,
|
|
1632
|
+
question.status === "blocked" ? "error" : "warning",
|
|
1633
|
+
2,
|
|
1634
|
+
);
|
|
1635
|
+
addWrapped("resolves when", question.resolutionCriteria, "dim", 2);
|
|
1636
|
+
}
|
|
1637
|
+
if (state.pendingQuestions.length > 4) {
|
|
1638
|
+
addWrapped(
|
|
1639
|
+
"more",
|
|
1640
|
+
`${state.pendingQuestions.length - 4} additional open questions`,
|
|
1641
|
+
"dim",
|
|
1642
|
+
1,
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
rows.push(row());
|
|
1648
|
+
section(`Judgment history · ${state.judgmentHistory.length}`);
|
|
1649
|
+
if (state.judgmentHistory.length === 0) {
|
|
1650
|
+
addWrapped(
|
|
1651
|
+
"state",
|
|
1652
|
+
"No judgment has been recorded on this branch.",
|
|
1653
|
+
"dim",
|
|
1654
|
+
2,
|
|
1655
|
+
);
|
|
1656
|
+
} else {
|
|
1657
|
+
const recentJudgments = state.judgmentHistory.slice(-10).toReversed();
|
|
1658
|
+
for (const judgment of recentJudgments) {
|
|
1659
|
+
const route = state.routeHistory.find(
|
|
1660
|
+
(candidate) => candidate.routeId === judgment.routeId,
|
|
1661
|
+
);
|
|
1662
|
+
addWrapped(
|
|
1663
|
+
`${judgment.target} ${judgment.status}`,
|
|
1664
|
+
`${judgment.question} → ${judgmentSummary(judgment.result)}`,
|
|
1665
|
+
judgmentColor(judgment.status),
|
|
1666
|
+
3,
|
|
1667
|
+
);
|
|
1668
|
+
if (route) addWrapped("route reason", route.reason, "dim", 2);
|
|
1669
|
+
for (const alternative of route?.consideredAlternatives ?? []) {
|
|
1670
|
+
addWrapped(
|
|
1671
|
+
`considered ${alternative.target}`,
|
|
1672
|
+
alternative.reason,
|
|
1673
|
+
"dim",
|
|
1674
|
+
2,
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
for (const update of judgment.questionUpdates ?? []) {
|
|
1678
|
+
addWrapped(
|
|
1679
|
+
`question ${update.status}`,
|
|
1680
|
+
`${update.questionId} → ${update.result}`,
|
|
1681
|
+
"accent",
|
|
1682
|
+
2,
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
if (state.judgmentHistory.length > recentJudgments.length) {
|
|
1687
|
+
addWrapped(
|
|
1688
|
+
"earlier",
|
|
1689
|
+
`${state.judgmentHistory.length - recentJudgments.length} earlier judgments`,
|
|
1690
|
+
"dim",
|
|
1691
|
+
1,
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
rows.push(row());
|
|
1697
|
+
addWrapped(
|
|
1698
|
+
"resources",
|
|
1699
|
+
`${this.view.availableSkills.length} skills · ${this.view.activeTools.length} active tools`,
|
|
1700
|
+
"dim",
|
|
1701
|
+
1,
|
|
1702
|
+
);
|
|
1703
|
+
rows.push(row());
|
|
1704
|
+
|
|
1705
|
+
for (const line of wrapTextWithAnsi(
|
|
1706
|
+
this.theme.fg(
|
|
1707
|
+
"dim",
|
|
1708
|
+
"mouse wheel scroll · drag select · Cmd+C copy · enter/esc close",
|
|
1709
|
+
),
|
|
1710
|
+
Math.max(1, innerWidth - 2),
|
|
1711
|
+
))
|
|
1712
|
+
rows.push(row(` ${line}`));
|
|
1713
|
+
rows.push(border(`╰${"─".repeat(innerWidth)}╯`));
|
|
1714
|
+
|
|
1715
|
+
this.cachedWidth = width;
|
|
1716
|
+
this.cachedLines = rows;
|
|
1717
|
+
return rows;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
invalidate(): void {
|
|
1721
|
+
this.cachedWidth = undefined;
|
|
1722
|
+
this.cachedLines = undefined;
|
|
1723
|
+
}
|
|
953
1724
|
}
|
|
954
1725
|
|
|
955
1726
|
export async function showDeveloperStatus(
|
|
956
|
-
|
|
957
|
-
|
|
1727
|
+
ctx: ExtensionCommandContext,
|
|
1728
|
+
view: DeveloperStatusView,
|
|
958
1729
|
): Promise<void> {
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
mouseTerminal = enableMouseScroll(tui);
|
|
964
|
-
return new DeveloperStatusPanel(view, theme, () => done(), {
|
|
965
|
-
viewportHeight: Math.max(10, Math.min(36, Math.floor(tui.terminal.rows * 0.88))),
|
|
966
|
-
requestRender: () => tui.requestRender(),
|
|
967
|
-
});
|
|
968
|
-
},
|
|
969
|
-
{
|
|
970
|
-
overlay: true,
|
|
971
|
-
overlayOptions: {
|
|
972
|
-
anchor: "center",
|
|
973
|
-
width: "90%",
|
|
974
|
-
minWidth: 72,
|
|
975
|
-
maxHeight: "88%",
|
|
976
|
-
margin: 1,
|
|
977
|
-
},
|
|
978
|
-
},
|
|
979
|
-
);
|
|
980
|
-
} finally {
|
|
981
|
-
disableMouseScroll(mouseTerminal);
|
|
982
|
-
}
|
|
1730
|
+
await ctx.ui.custom<void>(
|
|
1731
|
+
(_tui, theme, _keybindings, done) =>
|
|
1732
|
+
new DeveloperStatusPanel(view, theme, () => done()),
|
|
1733
|
+
);
|
|
983
1734
|
}
|
|
984
1735
|
|
|
985
1736
|
export function questionResolutionPrompt(question: PendingQuestion): string {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
export function editQuestionResolutionRequest(
|
|
1007
|
-
|
|
1008
|
-
|
|
1737
|
+
const requestLabel = resolutionRequestLabel(question.resolutionOwner);
|
|
1738
|
+
const context = question.context
|
|
1739
|
+
? ["", "Decision or evidence context:", question.context, ""]
|
|
1740
|
+
: [];
|
|
1741
|
+
return [
|
|
1742
|
+
"Resolve this open Developer question.",
|
|
1743
|
+
"",
|
|
1744
|
+
`Question: ${question.question}`,
|
|
1745
|
+
...context,
|
|
1746
|
+
`Resolution owner: ${question.resolutionOwner}`,
|
|
1747
|
+
`Gate: ${question.gate}`,
|
|
1748
|
+
`Resolution criteria: ${question.resolutionCriteria}`,
|
|
1749
|
+
"",
|
|
1750
|
+
requestLabel,
|
|
1751
|
+
"",
|
|
1752
|
+
"Use the supplied answer as new evidence, or investigate with available tools when the owner is agent.",
|
|
1753
|
+
"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.",
|
|
1754
|
+
].join("\n");
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
export async function editQuestionResolutionRequest(
|
|
1758
|
+
ctx: ExtensionCommandContext,
|
|
1759
|
+
question: PendingQuestion,
|
|
1009
1760
|
): Promise<string | undefined> {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1761
|
+
const request = questionResolutionPrompt(question);
|
|
1762
|
+
const current = question.responseSpec ? "" : ctx.ui.getEditorText();
|
|
1763
|
+
const initial = current.trim()
|
|
1764
|
+
? `${current.trimEnd()}\n\n${request}`
|
|
1765
|
+
: request;
|
|
1766
|
+
const disposition = await prepareQuestionResolution(ctx, question, {
|
|
1767
|
+
title: "Answer or investigate the selected Developer question",
|
|
1768
|
+
initial,
|
|
1769
|
+
});
|
|
1770
|
+
return disposition.kind === "answer" ? disposition.request : undefined;
|
|
1015
1771
|
}
|