@hobin/developer 0.1.3 → 0.1.4
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 +11 -0
- package/extensions/tui.ts +183 -117
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -267,6 +267,17 @@ Load the workspace package into Pi without installing it:
|
|
|
267
267
|
pi -e ./packages/developer
|
|
268
268
|
```
|
|
269
269
|
|
|
270
|
+
Launch the isolated modal fixture in a new Ghostty window for visual QA:
|
|
271
|
+
|
|
272
|
+
```sh
|
|
273
|
+
./packages/developer/scripts/ghostty-tui-qa.sh
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Choose **Inspect status** or **Revisit an open question**, then resize the window
|
|
277
|
+
while the modal is open. The fixture disables discovered extensions, skills,
|
|
278
|
+
tools, sessions, and network startup so it cannot mutate Developer state or call
|
|
279
|
+
a model.
|
|
280
|
+
|
|
270
281
|
`check` validates package structure and deterministic behavior. `eval` launches
|
|
271
282
|
the real Pi RPC surface without a model and covers package resources, commands,
|
|
272
283
|
mode state, and strict tool gating. Maintainers can use `eval:json` and the live
|
package/extensions/tui.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
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
|
-
SelectList,
|
|
13
11
|
Text,
|
|
14
12
|
truncateToWidth,
|
|
15
13
|
visibleWidth,
|
|
@@ -97,7 +95,7 @@ export function pendingQuestionItems(questions: PendingQuestion[]): SelectItem[]
|
|
|
97
95
|
return questions.map((question) => ({
|
|
98
96
|
value: question.id,
|
|
99
97
|
label: question.question,
|
|
100
|
-
description:
|
|
98
|
+
description: question.status,
|
|
101
99
|
}));
|
|
102
100
|
}
|
|
103
101
|
|
|
@@ -107,52 +105,124 @@ interface SelectDialogOptions {
|
|
|
107
105
|
items: SelectItem[];
|
|
108
106
|
width: number;
|
|
109
107
|
maxVisible: number;
|
|
110
|
-
|
|
108
|
+
selectedLabelMaxLines: number;
|
|
109
|
+
selectedDescriptionMaxLines: number;
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
function boundedWrappedLines(content: string, width: number, maxLines: number, ellipsis: string): string[] {
|
|
113
|
+
const contentWidth = Math.max(1, width);
|
|
114
|
+
const wrapped = wrapTextWithAnsi(content, contentWidth);
|
|
115
|
+
const visible = wrapped.slice(0, Math.max(1, maxLines));
|
|
116
|
+
if (wrapped.length > visible.length && visible.length > 0) {
|
|
117
|
+
const last = visible.length - 1;
|
|
118
|
+
visible[last] = truncateToWidth(visible[last] ?? "", Math.max(1, contentWidth - 1), "") + ellipsis;
|
|
119
|
+
}
|
|
120
|
+
return visible;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderModalFrame(container: Container, theme: Theme, width: number): string[] {
|
|
124
|
+
if (width < 3) return container.render(Math.max(1, width));
|
|
125
|
+
|
|
126
|
+
const innerWidth = width - 2;
|
|
127
|
+
const border = (text: string) => theme.fg("borderAccent", text);
|
|
128
|
+
const rows = container
|
|
129
|
+
.render(innerWidth)
|
|
130
|
+
.map((line) => `${border("│")}${truncateToWidth(line, innerWidth, "…", true)}${border("│")}`);
|
|
131
|
+
return [border(`╭${"─".repeat(innerWidth)}╮`), ...rows, border(`╰${"─".repeat(innerWidth)}╯`)];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
class WrappedSelectList {
|
|
135
|
+
private selectedIndex = 0;
|
|
136
|
+
private readonly items: SelectItem[];
|
|
137
|
+
private readonly keybindings: KeybindingsManager;
|
|
138
|
+
private readonly maxVisible: number;
|
|
139
|
+
private readonly selectedDescriptionMaxLines: number;
|
|
140
|
+
private readonly selectedLabelMaxLines: number;
|
|
117
141
|
private readonly theme: Theme;
|
|
118
|
-
|
|
142
|
+
onSelect?: (item: SelectItem) => void;
|
|
143
|
+
onCancel?: () => void;
|
|
119
144
|
|
|
120
|
-
constructor(
|
|
145
|
+
constructor(
|
|
146
|
+
items: SelectItem[],
|
|
147
|
+
maxVisible: number,
|
|
148
|
+
theme: Theme,
|
|
149
|
+
keybindings: KeybindingsManager,
|
|
150
|
+
options: {
|
|
151
|
+
selectedLabelMaxLines: number;
|
|
152
|
+
selectedDescriptionMaxLines: number;
|
|
153
|
+
},
|
|
154
|
+
) {
|
|
155
|
+
this.items = items;
|
|
156
|
+
this.maxVisible = maxVisible;
|
|
121
157
|
this.theme = theme;
|
|
122
|
-
this.
|
|
158
|
+
this.keybindings = keybindings;
|
|
159
|
+
this.selectedLabelMaxLines = options.selectedLabelMaxLines;
|
|
160
|
+
this.selectedDescriptionMaxLines = options.selectedDescriptionMaxLines;
|
|
123
161
|
}
|
|
124
162
|
|
|
125
|
-
|
|
126
|
-
this.
|
|
127
|
-
|
|
128
|
-
|
|
163
|
+
render(width: number): string[] {
|
|
164
|
+
if (this.items.length === 0) return [this.theme.fg("warning", " No items")];
|
|
165
|
+
|
|
166
|
+
const lines: string[] = [];
|
|
167
|
+
const startIndex = Math.max(
|
|
168
|
+
0,
|
|
169
|
+
Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.items.length - this.maxVisible),
|
|
170
|
+
);
|
|
171
|
+
const endIndex = Math.min(startIndex + this.maxVisible, this.items.length);
|
|
172
|
+
|
|
173
|
+
for (let index = startIndex; index < endIndex; index += 1) {
|
|
174
|
+
const item = this.items[index];
|
|
175
|
+
if (!item) continue;
|
|
176
|
+
if (index !== this.selectedIndex) {
|
|
177
|
+
lines.push(` ${truncateToWidth(item.label, Math.max(1, width - 2), "…")}`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const labels = boundedWrappedLines(
|
|
182
|
+
this.theme.fg("accent", item.label),
|
|
183
|
+
width - 2,
|
|
184
|
+
this.selectedLabelMaxLines,
|
|
185
|
+
this.theme.fg("dim", "…"),
|
|
186
|
+
);
|
|
187
|
+
lines.push(`${this.theme.fg("accent", "→ ")}${labels[0] ?? ""}`);
|
|
188
|
+
for (const line of labels.slice(1)) lines.push(` ${line}`);
|
|
189
|
+
|
|
190
|
+
if (item.description) {
|
|
191
|
+
const descriptions = boundedWrappedLines(
|
|
192
|
+
this.theme.fg("muted", item.description),
|
|
193
|
+
width - 4,
|
|
194
|
+
this.selectedDescriptionMaxLines,
|
|
195
|
+
this.theme.fg("dim", "…"),
|
|
196
|
+
);
|
|
197
|
+
for (const line of descriptions) lines.push(` ${line}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
129
200
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
201
|
+
if (startIndex > 0 || endIndex < this.items.length) {
|
|
202
|
+
lines.push(
|
|
203
|
+
this.theme.fg(
|
|
204
|
+
"dim",
|
|
205
|
+
truncateToWidth(` (${this.selectedIndex + 1}/${this.items.length})`, Math.max(1, width - 2), ""),
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return lines;
|
|
133
210
|
}
|
|
134
211
|
|
|
135
|
-
|
|
136
|
-
this.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
212
|
+
handleInput(data: string): void {
|
|
213
|
+
if (this.keybindings.matches(data, "tui.select.up")) {
|
|
214
|
+
this.selectedIndex = this.selectedIndex === 0 ? this.items.length - 1 : this.selectedIndex - 1;
|
|
215
|
+
} else if (this.keybindings.matches(data, "tui.select.down")) {
|
|
216
|
+
this.selectedIndex = this.selectedIndex === this.items.length - 1 ? 0 : this.selectedIndex + 1;
|
|
217
|
+
} else if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
218
|
+
const selected = this.items[this.selectedIndex];
|
|
219
|
+
if (selected) this.onSelect?.(selected);
|
|
220
|
+
} else if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
221
|
+
this.onCancel?.();
|
|
142
222
|
}
|
|
143
|
-
return visible;
|
|
144
223
|
}
|
|
145
224
|
|
|
146
225
|
invalidate(): void {}
|
|
147
|
-
|
|
148
|
-
private contentLines(width: number): string[] {
|
|
149
|
-
if (!this.item) return [];
|
|
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);
|
|
155
|
-
}
|
|
156
226
|
}
|
|
157
227
|
|
|
158
228
|
async function showSelectDialog(
|
|
@@ -160,64 +230,46 @@ async function showSelectDialog(
|
|
|
160
230
|
options: SelectDialogOptions,
|
|
161
231
|
): Promise<string | undefined> {
|
|
162
232
|
const result = await ctx.ui.custom<string | null>(
|
|
163
|
-
(tui, theme,
|
|
233
|
+
(tui, theme, keybindings, done) => {
|
|
164
234
|
const container = new Container();
|
|
165
|
-
const surface = new Box(1, 0, (text) => theme.bg("customMessageBg", text));
|
|
166
|
-
surface.addChild(container);
|
|
167
235
|
const title = new Text("", 1, 0);
|
|
168
236
|
const subtitle = new Text("", 1, 0);
|
|
169
237
|
const hint = new Text("", 1, 0);
|
|
170
|
-
const previewLabel = new Text(theme.fg("dim", "Selected detail"), 1, 0);
|
|
171
|
-
const preview = new SelectionPreview(theme);
|
|
172
238
|
const updateText = () => {
|
|
173
239
|
title.setText(theme.fg("accent", theme.bold(`◆ ${options.title}`)));
|
|
174
240
|
subtitle.setText(theme.fg("muted", options.subtitle));
|
|
175
|
-
hint.setText(theme.fg("dim", "↑↓ navigate ·
|
|
241
|
+
hint.setText(theme.fg("dim", "↑↓ navigate · enter select · esc cancel"));
|
|
176
242
|
};
|
|
177
243
|
updateText();
|
|
178
244
|
|
|
179
|
-
const list = new
|
|
245
|
+
const list = new WrappedSelectList(
|
|
180
246
|
options.items,
|
|
181
247
|
Math.min(options.items.length, options.maxVisible),
|
|
248
|
+
theme,
|
|
249
|
+
keybindings,
|
|
182
250
|
{
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
description: (text) => theme.fg("muted", text),
|
|
186
|
-
scrollInfo: (text) => theme.fg("dim", text),
|
|
187
|
-
noMatch: (text) => theme.fg("warning", text),
|
|
251
|
+
selectedLabelMaxLines: options.selectedLabelMaxLines,
|
|
252
|
+
selectedDescriptionMaxLines: options.selectedDescriptionMaxLines,
|
|
188
253
|
},
|
|
189
|
-
options.maxPrimaryColumnWidth
|
|
190
|
-
? { minPrimaryColumnWidth: 24, maxPrimaryColumnWidth: options.maxPrimaryColumnWidth }
|
|
191
|
-
: undefined,
|
|
192
254
|
);
|
|
193
255
|
list.onSelect = (item) => done(item.value);
|
|
194
256
|
list.onCancel = () => done(null);
|
|
195
|
-
list.onSelectionChange = (item) => preview.setItem(item);
|
|
196
|
-
const initialItem = list.getSelectedItem();
|
|
197
|
-
if (initialItem) preview.setItem(initialItem);
|
|
198
257
|
|
|
199
|
-
container.addChild(new DynamicBorder((text) => theme.fg("borderAccent", text)));
|
|
200
258
|
container.addChild(title);
|
|
201
259
|
container.addChild(subtitle);
|
|
202
260
|
container.addChild(list);
|
|
203
|
-
container.addChild(new DynamicBorder((text) => theme.fg("borderMuted", text)));
|
|
204
|
-
container.addChild(previewLabel);
|
|
205
|
-
container.addChild(preview);
|
|
206
261
|
container.addChild(hint);
|
|
207
|
-
container.addChild(new DynamicBorder((text) => theme.fg("borderAccent", text)));
|
|
208
262
|
|
|
209
263
|
return {
|
|
210
264
|
render(width: number) {
|
|
211
|
-
return
|
|
265
|
+
return renderModalFrame(container, theme, width);
|
|
212
266
|
},
|
|
213
267
|
invalidate() {
|
|
214
268
|
updateText();
|
|
215
|
-
|
|
269
|
+
container.invalidate();
|
|
216
270
|
},
|
|
217
271
|
handleInput(data: string) {
|
|
218
|
-
|
|
219
|
-
else if (matchesKey(data, "shift+down")) preview.scroll(1);
|
|
220
|
-
else list.handleInput(data);
|
|
272
|
+
list.handleInput(data);
|
|
221
273
|
tui.requestRender();
|
|
222
274
|
},
|
|
223
275
|
};
|
|
@@ -227,7 +279,10 @@ async function showSelectDialog(
|
|
|
227
279
|
overlayOptions: {
|
|
228
280
|
anchor: "center",
|
|
229
281
|
width: options.width,
|
|
230
|
-
maxHeight: Math.min(
|
|
282
|
+
maxHeight: Math.min(
|
|
283
|
+
options.maxVisible + options.selectedLabelMaxLines + options.selectedDescriptionMaxLines + 7,
|
|
284
|
+
24,
|
|
285
|
+
),
|
|
231
286
|
margin: 1,
|
|
232
287
|
},
|
|
233
288
|
},
|
|
@@ -245,6 +300,8 @@ export async function showDeveloperActionSelector(
|
|
|
245
300
|
items: developerActionItems(state),
|
|
246
301
|
width: 78,
|
|
247
302
|
maxVisible: 6,
|
|
303
|
+
selectedLabelMaxLines: 2,
|
|
304
|
+
selectedDescriptionMaxLines: 2,
|
|
248
305
|
});
|
|
249
306
|
if (result === "status" || result === "questions" || result === "on" || result === "strict" || result === "off") {
|
|
250
307
|
return result;
|
|
@@ -263,7 +320,8 @@ export async function showPendingQuestionSelector(
|
|
|
263
320
|
items: pendingQuestionItems(questions),
|
|
264
321
|
width: 96,
|
|
265
322
|
maxVisible: 10,
|
|
266
|
-
|
|
323
|
+
selectedLabelMaxLines: 5,
|
|
324
|
+
selectedDescriptionMaxLines: 1,
|
|
267
325
|
});
|
|
268
326
|
}
|
|
269
327
|
|
|
@@ -357,77 +415,89 @@ export class DeveloperStatusPanel {
|
|
|
357
415
|
render(width: number): string[] {
|
|
358
416
|
if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
|
|
359
417
|
|
|
360
|
-
const panelWidth = Math.max(
|
|
361
|
-
const innerWidth = Math.max(
|
|
418
|
+
const panelWidth = Math.max(1, width);
|
|
419
|
+
const innerWidth = Math.max(1, panelWidth - 2);
|
|
362
420
|
const rows: string[] = [];
|
|
363
|
-
const background = (text: string) => this.theme.bg("customMessageBg", text);
|
|
364
421
|
const border = (text: string) => this.theme.fg("borderAccent", text);
|
|
365
|
-
const row = (content = "") => {
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
|
|
422
|
+
const row = (content = "") => `${border("│")}${truncateToWidth(content, innerWidth, "…", true)}${border("│")}`;
|
|
423
|
+
const addWrapped = (label: string, value: string, color: ThemeColor = "muted", maxLines = 2) => {
|
|
424
|
+
const labelText = `${label} ·`;
|
|
425
|
+
const plainPrefix = ` ${labelText} `;
|
|
426
|
+
const styledPrefix = ` ${this.theme.fg("dim", labelText)} `;
|
|
427
|
+
const contentWidth = Math.max(1, innerWidth - visibleWidth(plainPrefix));
|
|
428
|
+
const wrapped = wrapTextWithAnsi(this.theme.fg(color, value.trim()), contentWidth);
|
|
429
|
+
const visible = wrapped.slice(0, Math.max(1, maxLines));
|
|
430
|
+
if (wrapped.length > visible.length && visible.length > 0) {
|
|
431
|
+
const last = visible.length - 1;
|
|
432
|
+
visible[last] =
|
|
433
|
+
truncateToWidth(visible[last] ?? "", Math.max(1, contentWidth - 1), "") + this.theme.fg("dim", "…");
|
|
434
|
+
}
|
|
435
|
+
rows.push(row(styledPrefix + (visible[0] ?? "")));
|
|
436
|
+
const hangingIndent = " ".repeat(visibleWidth(plainPrefix));
|
|
437
|
+
for (const line of visible.slice(1)) rows.push(row(hangingIndent + line));
|
|
374
438
|
};
|
|
375
439
|
const section = (title: string) => rows.push(row(` ${this.theme.fg("accent", this.theme.bold(title))}`));
|
|
376
440
|
|
|
377
|
-
rows.push(
|
|
441
|
+
rows.push(border(`╭${"─".repeat(innerWidth)}╮`));
|
|
378
442
|
rows.push(row(` ${this.theme.fg("accent", this.theme.bold("◆ Developer status"))}`));
|
|
379
443
|
rows.push(row());
|
|
380
444
|
|
|
381
445
|
const state = this.view.state;
|
|
382
446
|
const currentProtocol = protocolState(state);
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
),
|
|
391
|
-
);
|
|
447
|
+
const summary =
|
|
448
|
+
`mode ${this.theme.fg(modeColor(state.mode), modeName(state.mode))}` +
|
|
449
|
+
this.theme.fg("dim", " · ") +
|
|
450
|
+
`protocol ${this.theme.fg(protocolColor(currentProtocol), currentProtocol)}` +
|
|
451
|
+
this.theme.fg("dim", " · ") +
|
|
452
|
+
`target ${this.theme.fg("muted", state.activeRoute?.owner ?? "none")}`;
|
|
453
|
+
for (const line of wrapTextWithAnsi(summary, Math.max(1, innerWidth - 2))) rows.push(row(` ${line}`));
|
|
392
454
|
rows.push(row());
|
|
393
455
|
|
|
394
456
|
section("Active route");
|
|
395
457
|
if (state.activeRoute) {
|
|
396
458
|
addWrapped("id", state.activeRoute.routeId, "dim", 1);
|
|
397
|
-
addWrapped("question", state.activeRoute.question, "text");
|
|
398
|
-
addWrapped("reason", state.activeRoute.reason);
|
|
459
|
+
addWrapped("question", state.activeRoute.question, "text", 3);
|
|
460
|
+
addWrapped("reason", state.activeRoute.reason, "muted", 2);
|
|
399
461
|
addWrapped("skill", state.activeRoute.methodLocation ?? "direct action", "dim", 1);
|
|
400
462
|
addWrapped("known evidence", String(state.activeRoute.knownEvidence.length), "muted", 1);
|
|
401
463
|
} else {
|
|
402
|
-
addWrapped("state", "No route is currently waiting for judgment.", "dim",
|
|
464
|
+
addWrapped("state", "No route is currently waiting for judgment.", "dim", 2);
|
|
403
465
|
}
|
|
404
466
|
|
|
405
467
|
rows.push(row());
|
|
406
468
|
section(`Open questions · ${state.pendingQuestions.length}`);
|
|
407
469
|
if (state.pendingQuestions.length === 0) {
|
|
408
|
-
addWrapped("state", "No unresolved Developer questions.", "dim",
|
|
470
|
+
addWrapped("state", "No unresolved Developer questions.", "dim", 2);
|
|
409
471
|
} else {
|
|
410
|
-
for (const question of state.pendingQuestions) {
|
|
472
|
+
for (const question of state.pendingQuestions.slice(0, 4)) {
|
|
411
473
|
addWrapped(
|
|
412
474
|
question.status === "blocked" ? "blocked" : "needs evidence",
|
|
413
475
|
question.question,
|
|
414
476
|
question.status === "blocked" ? "error" : "warning",
|
|
415
|
-
|
|
477
|
+
2,
|
|
416
478
|
);
|
|
417
479
|
}
|
|
480
|
+
if (state.pendingQuestions.length > 4) {
|
|
481
|
+
addWrapped("more", `${state.pendingQuestions.length - 4} additional open questions`, "dim", 1);
|
|
482
|
+
}
|
|
418
483
|
}
|
|
419
484
|
|
|
420
485
|
rows.push(row());
|
|
421
486
|
section("Last judgment");
|
|
422
487
|
if (state.lastJudgment) {
|
|
423
|
-
addWrapped(
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
488
|
+
addWrapped(
|
|
489
|
+
"status",
|
|
490
|
+
state.lastJudgment.status,
|
|
491
|
+
protocolColor(
|
|
492
|
+
state.lastJudgment.status === "blocked"
|
|
493
|
+
? "blocked"
|
|
494
|
+
: state.lastJudgment.status === "needs-evidence"
|
|
495
|
+
? "needs-evidence"
|
|
496
|
+
: "idle",
|
|
497
|
+
),
|
|
498
|
+
1,
|
|
499
|
+
);
|
|
500
|
+
addWrapped("result", state.lastJudgment.result, "muted", 3);
|
|
431
501
|
addWrapped(
|
|
432
502
|
"evidence",
|
|
433
503
|
`${state.lastJudgment.basis.length} basis · ${state.lastJudgment.artifacts.length} artifacts`,
|
|
@@ -435,7 +505,7 @@ export class DeveloperStatusPanel {
|
|
|
435
505
|
1,
|
|
436
506
|
);
|
|
437
507
|
} else {
|
|
438
|
-
addWrapped("state", "No judgment has been recorded on this branch.", "dim",
|
|
508
|
+
addWrapped("state", "No judgment has been recorded on this branch.", "dim", 2);
|
|
439
509
|
}
|
|
440
510
|
|
|
441
511
|
rows.push(row());
|
|
@@ -446,23 +516,22 @@ export class DeveloperStatusPanel {
|
|
|
446
516
|
1,
|
|
447
517
|
);
|
|
448
518
|
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
519
|
|
|
452
520
|
const header = rows.slice(0, 2);
|
|
453
|
-
const body = rows.slice(2
|
|
521
|
+
const body = rows.slice(2);
|
|
454
522
|
const bodyCapacity = Math.max(1, this.viewportHeight - 4);
|
|
455
523
|
this.maxScrollOffset = Math.max(0, body.length - bodyCapacity);
|
|
456
524
|
this.scrollOffset = Math.min(this.scrollOffset, this.maxScrollOffset);
|
|
457
525
|
const visibleBody = body.slice(this.scrollOffset, this.scrollOffset + bodyCapacity);
|
|
458
|
-
const position =
|
|
459
|
-
|
|
460
|
-
|
|
526
|
+
const position =
|
|
527
|
+
body.length > bodyCapacity
|
|
528
|
+
? `↑↓ scroll · ${this.scrollOffset + 1}–${Math.min(body.length, this.scrollOffset + bodyCapacity)}/${body.length} · enter/esc close`
|
|
529
|
+
: "enter/esc close · /develop questions revisits open work";
|
|
461
530
|
const visibleRows = [
|
|
462
531
|
...header,
|
|
463
532
|
...visibleBody,
|
|
464
533
|
row(` ${this.theme.fg("dim", position)}`),
|
|
465
|
-
|
|
534
|
+
border(`╰${"─".repeat(innerWidth)}╯`),
|
|
466
535
|
];
|
|
467
536
|
|
|
468
537
|
this.cachedWidth = width;
|
|
@@ -476,23 +545,20 @@ export class DeveloperStatusPanel {
|
|
|
476
545
|
}
|
|
477
546
|
}
|
|
478
547
|
|
|
479
|
-
export async function showDeveloperStatus(
|
|
480
|
-
ctx: ExtensionCommandContext,
|
|
481
|
-
view: DeveloperStatusView,
|
|
482
|
-
): Promise<void> {
|
|
548
|
+
export async function showDeveloperStatus(ctx: ExtensionCommandContext, view: DeveloperStatusView): Promise<void> {
|
|
483
549
|
await ctx.ui.custom<void>(
|
|
484
550
|
(tui, theme, _keybindings, done) =>
|
|
485
551
|
new DeveloperStatusPanel(view, theme, () => done(), {
|
|
486
|
-
viewportHeight: Math.max(
|
|
552
|
+
viewportHeight: Math.max(6, Math.min(28, tui.terminal.rows - 2)),
|
|
487
553
|
requestRender: () => tui.requestRender(),
|
|
488
554
|
}),
|
|
489
555
|
{
|
|
490
556
|
overlay: true,
|
|
491
557
|
overlayOptions: {
|
|
492
558
|
anchor: "center",
|
|
493
|
-
width:
|
|
559
|
+
width: 92,
|
|
494
560
|
minWidth: 56,
|
|
495
|
-
maxHeight:
|
|
561
|
+
maxHeight: 28,
|
|
496
562
|
margin: 1,
|
|
497
563
|
},
|
|
498
564
|
},
|