@hobin/developer 0.1.5 → 0.1.6
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 +35 -11
- package/extensions/developer.ts +381 -92
- package/extensions/state.ts +120 -8
- package/extensions/tui.ts +341 -37
- package/package.json +1 -1
package/extensions/tui.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionCommandContext,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
KeybindingsManager,
|
|
5
|
+
Theme,
|
|
6
|
+
ThemeColor,
|
|
6
7
|
} from "@earendil-works/pi-coding-agent";
|
|
7
8
|
import {
|
|
8
9
|
Container,
|
|
@@ -17,6 +18,8 @@ import {
|
|
|
17
18
|
|
|
18
19
|
import {
|
|
19
20
|
protocolState,
|
|
21
|
+
type ChoiceResponseField,
|
|
22
|
+
type ChoiceResponseOption,
|
|
20
23
|
type DeveloperMode,
|
|
21
24
|
type DeveloperState,
|
|
22
25
|
type JudgmentStatus,
|
|
@@ -24,7 +27,9 @@ import {
|
|
|
24
27
|
type ProtocolState,
|
|
25
28
|
} from "./state.ts";
|
|
26
29
|
|
|
27
|
-
export type DeveloperAction =
|
|
30
|
+
export type DeveloperAction =
|
|
31
|
+
| { kind: "command"; value: "status" | "questions" | DeveloperMode }
|
|
32
|
+
| { kind: "question"; questionId: string };
|
|
28
33
|
|
|
29
34
|
function modeName(mode: DeveloperMode): string {
|
|
30
35
|
if (mode === "on") return "adaptive";
|
|
@@ -38,7 +43,8 @@ function protocolColor(value: ProtocolState): ThemeColor {
|
|
|
38
43
|
value === "needs-answer" ||
|
|
39
44
|
value === "needs-routing" ||
|
|
40
45
|
value === "needs-verification"
|
|
41
|
-
)
|
|
46
|
+
)
|
|
47
|
+
return "warning";
|
|
42
48
|
if (value === "needs-judgment") return "accent";
|
|
43
49
|
return "dim";
|
|
44
50
|
}
|
|
@@ -87,13 +93,7 @@ export function developerActionItems(state: DeveloperState): SelectItem[] {
|
|
|
87
93
|
description: `${modeName(state.mode)} · ${currentProtocol} · ${state.pendingQuestions.length} open`,
|
|
88
94
|
},
|
|
89
95
|
];
|
|
90
|
-
|
|
91
|
-
items.push({
|
|
92
|
-
value: "questions",
|
|
93
|
-
label: "Revisit an open question",
|
|
94
|
-
description: `Choose from ${state.pendingQuestions.length} unresolved question(s) and prepare the editor`,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
96
|
+
items.push(...pendingQuestionItems(state.pendingQuestions));
|
|
97
97
|
items.push(
|
|
98
98
|
{
|
|
99
99
|
value: "on",
|
|
@@ -127,9 +127,13 @@ function resolutionRequestLabel(owner: PendingQuestion["resolutionOwner"]): stri
|
|
|
127
127
|
return "Evidence or investigation request for Pi:";
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
const MAX_IMMEDIATE_REQUEST_BYTES = 16_000;
|
|
131
|
+
const MAX_IMMEDIATE_REQUEST_LINES = 1_000;
|
|
130
132
|
const ENABLE_MOUSE_SCROLL = "\u001b[?1000h\u001b[?1006h";
|
|
131
133
|
const DISABLE_MOUSE_SCROLL = "\u001b[?1006l\u001b[?1000l";
|
|
132
134
|
|
|
135
|
+
export type ImmediateQuestionDisposition = { kind: "answer"; request: string } | { kind: "defer" };
|
|
136
|
+
|
|
133
137
|
interface WritableTerminal {
|
|
134
138
|
write(data: string): void;
|
|
135
139
|
}
|
|
@@ -175,13 +179,19 @@ interface SelectDialogOptions {
|
|
|
175
179
|
selectedDescriptionMaxLines: number;
|
|
176
180
|
}
|
|
177
181
|
|
|
178
|
-
function boundedWrappedLines(
|
|
182
|
+
function boundedWrappedLines(
|
|
183
|
+
content: string,
|
|
184
|
+
width: number,
|
|
185
|
+
maxLines: number,
|
|
186
|
+
ellipsis: string,
|
|
187
|
+
): string[] {
|
|
179
188
|
const contentWidth = Math.max(1, width);
|
|
180
189
|
const wrapped = wrapTextWithAnsi(content, contentWidth);
|
|
181
190
|
const visible = wrapped.slice(0, Math.max(1, maxLines));
|
|
182
191
|
if (wrapped.length > visible.length && visible.length > 0) {
|
|
183
192
|
const last = visible.length - 1;
|
|
184
|
-
visible[last] =
|
|
193
|
+
visible[last] =
|
|
194
|
+
truncateToWidth(visible[last] ?? "", Math.max(1, contentWidth - 1), "") + ellipsis;
|
|
185
195
|
}
|
|
186
196
|
return visible;
|
|
187
197
|
}
|
|
@@ -237,7 +247,10 @@ class WrappedSelectList {
|
|
|
237
247
|
const lines: string[] = [];
|
|
238
248
|
const startIndex = Math.max(
|
|
239
249
|
0,
|
|
240
|
-
Math.min(
|
|
250
|
+
Math.min(
|
|
251
|
+
this.selectedIndex - Math.floor(this.maxVisible / 2),
|
|
252
|
+
this.items.length - this.maxVisible,
|
|
253
|
+
),
|
|
241
254
|
);
|
|
242
255
|
const endIndex = Math.min(startIndex + this.maxVisible, this.items.length);
|
|
243
256
|
|
|
@@ -273,7 +286,11 @@ class WrappedSelectList {
|
|
|
273
286
|
lines.push(
|
|
274
287
|
this.theme.fg(
|
|
275
288
|
"dim",
|
|
276
|
-
truncateToWidth(
|
|
289
|
+
truncateToWidth(
|
|
290
|
+
` (${this.selectedIndex + 1}/${this.items.length})`,
|
|
291
|
+
Math.max(1, width - 2),
|
|
292
|
+
"",
|
|
293
|
+
),
|
|
277
294
|
),
|
|
278
295
|
);
|
|
279
296
|
}
|
|
@@ -287,9 +304,11 @@ class WrappedSelectList {
|
|
|
287
304
|
if (wheelDirection !== undefined) {
|
|
288
305
|
this.moveSelection(wheelDirection * 3);
|
|
289
306
|
} else if (this.keybindings.matches(data, "tui.select.up")) {
|
|
290
|
-
this.selectedIndex =
|
|
307
|
+
this.selectedIndex =
|
|
308
|
+
this.selectedIndex === 0 ? this.items.length - 1 : this.selectedIndex - 1;
|
|
291
309
|
} else if (this.keybindings.matches(data, "tui.select.down")) {
|
|
292
|
-
this.selectedIndex =
|
|
310
|
+
this.selectedIndex =
|
|
311
|
+
this.selectedIndex === this.items.length - 1 ? 0 : this.selectedIndex + 1;
|
|
293
312
|
} else if (this.keybindings.matches(data, "tui.select.pageUp")) {
|
|
294
313
|
this.moveSelection(-Math.max(1, this.maxVisible - 1));
|
|
295
314
|
} else if (this.keybindings.matches(data, "tui.select.pageDown")) {
|
|
@@ -314,7 +333,7 @@ class WrappedSelectList {
|
|
|
314
333
|
}
|
|
315
334
|
|
|
316
335
|
async function showSelectDialog(
|
|
317
|
-
ctx:
|
|
336
|
+
ctx: ExtensionContext,
|
|
318
337
|
options: SelectDialogOptions,
|
|
319
338
|
): Promise<string | undefined> {
|
|
320
339
|
let mouseTerminal: WritableTerminal | undefined;
|
|
@@ -329,7 +348,9 @@ async function showSelectDialog(
|
|
|
329
348
|
const updateText = () => {
|
|
330
349
|
title.setText(theme.fg("accent", theme.bold(`◆ ${options.title}`)));
|
|
331
350
|
subtitle.setText(theme.fg("muted", options.subtitle));
|
|
332
|
-
hint.setText(
|
|
351
|
+
hint.setText(
|
|
352
|
+
theme.fg("dim", "wheel/↑↓ · PgUp/PgDn · Home/End · enter select · esc cancel"),
|
|
353
|
+
);
|
|
333
354
|
};
|
|
334
355
|
updateText();
|
|
335
356
|
|
|
@@ -396,8 +417,18 @@ export async function showDeveloperActionSelector(
|
|
|
396
417
|
selectedLabelMaxLines: 3,
|
|
397
418
|
selectedDescriptionMaxLines: 3,
|
|
398
419
|
});
|
|
399
|
-
if (result
|
|
400
|
-
|
|
420
|
+
if (!result) return undefined;
|
|
421
|
+
if (
|
|
422
|
+
result === "status" ||
|
|
423
|
+
result === "questions" ||
|
|
424
|
+
result === "on" ||
|
|
425
|
+
result === "strict" ||
|
|
426
|
+
result === "off"
|
|
427
|
+
) {
|
|
428
|
+
return { kind: "command", value: result };
|
|
429
|
+
}
|
|
430
|
+
if (state.pendingQuestions.some((question) => question.id === result)) {
|
|
431
|
+
return { kind: "question", questionId: result };
|
|
401
432
|
}
|
|
402
433
|
return undefined;
|
|
403
434
|
}
|
|
@@ -409,7 +440,8 @@ export function showPendingQuestionSelector(
|
|
|
409
440
|
if (questions.length === 0) return Promise.resolve(undefined);
|
|
410
441
|
return showSelectDialog(ctx, {
|
|
411
442
|
title: "Resolve an open Developer question",
|
|
412
|
-
subtitle:
|
|
443
|
+
subtitle:
|
|
444
|
+
"Enter opens an answer/evidence editor; the question closes after a resolved or not-applicable judgment",
|
|
413
445
|
items: pendingQuestionItems(questions),
|
|
414
446
|
width: "92%",
|
|
415
447
|
minWidth: 88,
|
|
@@ -419,6 +451,240 @@ export function showPendingQuestionSelector(
|
|
|
419
451
|
});
|
|
420
452
|
}
|
|
421
453
|
|
|
454
|
+
interface ChoiceResponseAnswer {
|
|
455
|
+
option: ChoiceResponseOption;
|
|
456
|
+
detail?: string;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function responseWithinLimits(
|
|
460
|
+
ctx: ExtensionContext,
|
|
461
|
+
request: string,
|
|
462
|
+
label = "Question response",
|
|
463
|
+
): boolean {
|
|
464
|
+
const requestBytes = new TextEncoder().encode(request).byteLength;
|
|
465
|
+
const requestLines = request.split(/\r?\n/).length;
|
|
466
|
+
if (requestBytes <= MAX_IMMEDIATE_REQUEST_BYTES && requestLines <= MAX_IMMEDIATE_REQUEST_LINES) {
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
ctx.ui.notify(
|
|
470
|
+
`${label} is too large (${requestBytes} bytes, ${requestLines} lines); shorten it before submitting.`,
|
|
471
|
+
"warning",
|
|
472
|
+
);
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function choiceDetailInitial(field: ChoiceResponseField, option: ChoiceResponseOption): string {
|
|
477
|
+
return [
|
|
478
|
+
`Decision: ${field.prompt}`,
|
|
479
|
+
`Selected: ${option.value} — ${option.label}`,
|
|
480
|
+
"",
|
|
481
|
+
option.detailPrompt,
|
|
482
|
+
"",
|
|
483
|
+
"Required detail:",
|
|
484
|
+
].join("\n");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function editChoiceDetail(
|
|
488
|
+
ctx: ExtensionContext,
|
|
489
|
+
field: ChoiceResponseField,
|
|
490
|
+
option: ChoiceResponseOption,
|
|
491
|
+
): Promise<string | undefined> {
|
|
492
|
+
const initial = choiceDetailInitial(field, option);
|
|
493
|
+
while (true) {
|
|
494
|
+
const response = await ctx.ui.editor(`Add detail for ${field.id} · ${option.value}`, initial);
|
|
495
|
+
if (response === undefined) return undefined;
|
|
496
|
+
const detail = (
|
|
497
|
+
response.startsWith(initial) ? response.slice(initial.length) : response
|
|
498
|
+
).trim();
|
|
499
|
+
if (!detail) {
|
|
500
|
+
ctx.ui.notify("This choice requires a non-empty detail.", "warning");
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (!responseWithinLimits(ctx, detail, "Choice detail")) continue;
|
|
504
|
+
return detail;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function structuredResolutionRequest(
|
|
509
|
+
question: PendingQuestion,
|
|
510
|
+
answers: ReadonlyArray<ChoiceResponseAnswer | undefined>,
|
|
511
|
+
): string {
|
|
512
|
+
const fields = question.responseSpec?.fields ?? [];
|
|
513
|
+
const lines = [questionResolutionPrompt(question), "", "Structured answer:"];
|
|
514
|
+
for (const [index, field] of fields.entries()) {
|
|
515
|
+
const answer = answers[index];
|
|
516
|
+
if (!answer) continue;
|
|
517
|
+
lines.push(`- ${field.id}: ${answer.option.value} — ${answer.option.label}`);
|
|
518
|
+
if (answer.detail) lines.push(` Detail: ${answer.detail}`);
|
|
519
|
+
}
|
|
520
|
+
return lines.join("\n");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
type ChoiceFieldAction =
|
|
524
|
+
| { kind: "answer"; answer: ChoiceResponseAnswer }
|
|
525
|
+
| { kind: "cancel" }
|
|
526
|
+
| { kind: "retry" };
|
|
527
|
+
|
|
528
|
+
type ChoiceReviewAction =
|
|
529
|
+
| { kind: "submit" }
|
|
530
|
+
| { kind: "edit"; fieldIndex: number }
|
|
531
|
+
| { kind: "back" };
|
|
532
|
+
|
|
533
|
+
async function selectChoiceAnswer(
|
|
534
|
+
ctx: ExtensionContext,
|
|
535
|
+
field: ChoiceResponseField,
|
|
536
|
+
fieldIndex: number,
|
|
537
|
+
fieldCount: number,
|
|
538
|
+
): Promise<ChoiceFieldAction> {
|
|
539
|
+
const selectedValue = await showSelectDialog(ctx, {
|
|
540
|
+
title: `Decision ${fieldIndex + 1}/${fieldCount} · ${field.id}`,
|
|
541
|
+
subtitle: field.description ?? field.prompt,
|
|
542
|
+
items: field.options.map((option) => ({
|
|
543
|
+
value: option.value,
|
|
544
|
+
label: `${option.value} · ${option.label}`,
|
|
545
|
+
description: option.description ?? field.prompt,
|
|
546
|
+
})),
|
|
547
|
+
width: "92%",
|
|
548
|
+
minWidth: 88,
|
|
549
|
+
maxVisible: Math.min(field.options.length, 12),
|
|
550
|
+
selectedLabelMaxLines: 3,
|
|
551
|
+
selectedDescriptionMaxLines: 4,
|
|
552
|
+
});
|
|
553
|
+
if (!selectedValue) return { kind: "cancel" };
|
|
554
|
+
const option = field.options.find((candidate) => candidate.value === selectedValue);
|
|
555
|
+
if (!option) return { kind: "retry" };
|
|
556
|
+
const detail = option.detailPrompt ? await editChoiceDetail(ctx, field, option) : undefined;
|
|
557
|
+
if (option.detailPrompt && detail === undefined) return { kind: "retry" };
|
|
558
|
+
return { kind: "answer", answer: { option, detail } };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function reviewChoiceAnswers(
|
|
562
|
+
ctx: ExtensionContext,
|
|
563
|
+
question: PendingQuestion,
|
|
564
|
+
fields: ChoiceResponseField[],
|
|
565
|
+
answers: ReadonlyArray<ChoiceResponseAnswer | undefined>,
|
|
566
|
+
): Promise<ChoiceReviewAction> {
|
|
567
|
+
const action = await showSelectDialog(ctx, {
|
|
568
|
+
title: "Review structured answer",
|
|
569
|
+
subtitle: question.question,
|
|
570
|
+
items: [
|
|
571
|
+
{
|
|
572
|
+
value: "submit",
|
|
573
|
+
label: "Submit answers",
|
|
574
|
+
description: "Send these decisions to Pi; the question remains open until judgment",
|
|
575
|
+
},
|
|
576
|
+
...fields.map((field, index) => {
|
|
577
|
+
const answer = answers[index];
|
|
578
|
+
return {
|
|
579
|
+
value: `edit:${index}`,
|
|
580
|
+
label: `${field.id} · ${answer?.option.value ?? "missing"} — ${answer?.option.label ?? "Missing answer"}`,
|
|
581
|
+
description: answer?.detail ? `Detail: ${answer.detail}` : "Enter to change this answer",
|
|
582
|
+
};
|
|
583
|
+
}),
|
|
584
|
+
],
|
|
585
|
+
width: "92%",
|
|
586
|
+
minWidth: 88,
|
|
587
|
+
maxVisible: Math.min(fields.length + 1, 12),
|
|
588
|
+
selectedLabelMaxLines: 3,
|
|
589
|
+
selectedDescriptionMaxLines: 4,
|
|
590
|
+
});
|
|
591
|
+
if (action === "submit") return { kind: "submit" };
|
|
592
|
+
if (!action?.startsWith("edit:")) return { kind: "back" };
|
|
593
|
+
const fieldIndex = Number(action.slice("edit:".length));
|
|
594
|
+
if (!Number.isInteger(fieldIndex) || fieldIndex < 0 || fieldIndex >= fields.length) {
|
|
595
|
+
return { kind: "back" };
|
|
596
|
+
}
|
|
597
|
+
return { kind: "edit", fieldIndex };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function previousChoiceField(
|
|
601
|
+
fieldIndex: number,
|
|
602
|
+
fieldCount: number,
|
|
603
|
+
returnToReview: boolean,
|
|
604
|
+
): number | undefined {
|
|
605
|
+
if (returnToReview) return fieldCount;
|
|
606
|
+
return fieldIndex === 0 ? undefined : fieldIndex - 1;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function collectChoiceResponse(
|
|
610
|
+
ctx: ExtensionContext,
|
|
611
|
+
question: PendingQuestion,
|
|
612
|
+
): Promise<string | undefined> {
|
|
613
|
+
const fields = question.responseSpec?.fields;
|
|
614
|
+
if (!fields || fields.length === 0) return undefined;
|
|
615
|
+
const answers: Array<ChoiceResponseAnswer | undefined> = [];
|
|
616
|
+
let fieldIndex = 0;
|
|
617
|
+
let returnToReview = false;
|
|
618
|
+
|
|
619
|
+
while (true) {
|
|
620
|
+
if (fieldIndex < fields.length) {
|
|
621
|
+
const field = fields[fieldIndex];
|
|
622
|
+
if (!field) return undefined;
|
|
623
|
+
const action = await selectChoiceAnswer(ctx, field, fieldIndex, fields.length);
|
|
624
|
+
if (action.kind === "retry") continue;
|
|
625
|
+
if (action.kind === "cancel") {
|
|
626
|
+
fieldIndex = previousChoiceField(fieldIndex, fields.length, returnToReview) ?? -1;
|
|
627
|
+
}
|
|
628
|
+
if (fieldIndex < 0) return undefined;
|
|
629
|
+
if (action.kind === "cancel") continue;
|
|
630
|
+
answers[fieldIndex] = action.answer;
|
|
631
|
+
fieldIndex = returnToReview ? fields.length : fieldIndex + 1;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const action = await reviewChoiceAnswers(ctx, question, fields, answers);
|
|
636
|
+
if (action.kind === "submit") return structuredResolutionRequest(question, answers);
|
|
637
|
+
if (action.kind === "edit") {
|
|
638
|
+
fieldIndex = action.fieldIndex;
|
|
639
|
+
returnToReview = true;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
returnToReview = false;
|
|
643
|
+
fieldIndex = fields.length - 1;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export async function promptImmediateUserQuestion(
|
|
648
|
+
ctx: ExtensionContext,
|
|
649
|
+
question: PendingQuestion,
|
|
650
|
+
): Promise<ImmediateQuestionDisposition> {
|
|
651
|
+
while (true) {
|
|
652
|
+
const action = await showSelectDialog(ctx, {
|
|
653
|
+
title: "Developer needs a decision",
|
|
654
|
+
subtitle: question.question,
|
|
655
|
+
items: [
|
|
656
|
+
{
|
|
657
|
+
value: "answer",
|
|
658
|
+
label: "Answer now",
|
|
659
|
+
description:
|
|
660
|
+
"Review the full context and provide the decision required before direct work",
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
value: "defer",
|
|
664
|
+
label: "Leave open",
|
|
665
|
+
description: "Keep this question in Developer and answer it later",
|
|
666
|
+
},
|
|
667
|
+
],
|
|
668
|
+
width: "92%",
|
|
669
|
+
minWidth: 88,
|
|
670
|
+
maxVisible: 2,
|
|
671
|
+
selectedLabelMaxLines: 3,
|
|
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
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
422
688
|
export class DeveloperWidget {
|
|
423
689
|
private readonly state: DeveloperState;
|
|
424
690
|
private readonly theme: Theme;
|
|
@@ -450,10 +716,14 @@ export class DeveloperWidget {
|
|
|
450
716
|
);
|
|
451
717
|
}
|
|
452
718
|
if (this.state.pendingQuestions.length > 3) {
|
|
453
|
-
lines.push(
|
|
719
|
+
lines.push(
|
|
720
|
+
this.theme.fg("dim", ` +${this.state.pendingQuestions.length - 3} more open questions`),
|
|
721
|
+
);
|
|
454
722
|
}
|
|
455
723
|
if (this.state.implementationFramingRequired) {
|
|
456
|
-
lines.push(
|
|
724
|
+
lines.push(
|
|
725
|
+
this.theme.fg("warning", "◇ gate · frame implementation before direct (sketch or signal)"),
|
|
726
|
+
);
|
|
457
727
|
}
|
|
458
728
|
if (this.state.rerouteRequired) {
|
|
459
729
|
lines.push(this.theme.fg("warning", "→ next · reroute from the latest direct landing"));
|
|
@@ -527,8 +797,14 @@ export class DeveloperStatusPanel {
|
|
|
527
797
|
const innerWidth = Math.max(1, panelWidth - 2);
|
|
528
798
|
const rows: string[] = [];
|
|
529
799
|
const border = (text: string) => this.theme.fg("borderAccent", text);
|
|
530
|
-
const row = (content = "") =>
|
|
531
|
-
|
|
800
|
+
const row = (content = "") =>
|
|
801
|
+
`${border("│")}${truncateToWidth(content, innerWidth, "…", true)}${border("│")}`;
|
|
802
|
+
const addWrapped = (
|
|
803
|
+
label: string,
|
|
804
|
+
value: string,
|
|
805
|
+
color: ThemeColor = "muted",
|
|
806
|
+
maxLines = 2,
|
|
807
|
+
) => {
|
|
532
808
|
const labelText = `${label} ·`;
|
|
533
809
|
const plainPrefix = ` ${labelText} `;
|
|
534
810
|
const styledPrefix = ` ${this.theme.fg("dim", labelText)} `;
|
|
@@ -538,13 +814,15 @@ export class DeveloperStatusPanel {
|
|
|
538
814
|
if (wrapped.length > visible.length && visible.length > 0) {
|
|
539
815
|
const last = visible.length - 1;
|
|
540
816
|
visible[last] =
|
|
541
|
-
truncateToWidth(visible[last] ?? "", Math.max(1, contentWidth - 1), "") +
|
|
817
|
+
truncateToWidth(visible[last] ?? "", Math.max(1, contentWidth - 1), "") +
|
|
818
|
+
this.theme.fg("dim", "…");
|
|
542
819
|
}
|
|
543
820
|
rows.push(row(styledPrefix + (visible[0] ?? "")));
|
|
544
821
|
const hangingIndent = " ".repeat(visibleWidth(plainPrefix));
|
|
545
822
|
for (const line of visible.slice(1)) rows.push(row(hangingIndent + line));
|
|
546
823
|
};
|
|
547
|
-
const section = (title: string) =>
|
|
824
|
+
const section = (title: string) =>
|
|
825
|
+
rows.push(row(` ${this.theme.fg("accent", this.theme.bold(title))}`));
|
|
548
826
|
|
|
549
827
|
rows.push(border(`╭${"─".repeat(innerWidth)}╮`));
|
|
550
828
|
rows.push(row(` ${this.theme.fg("accent", this.theme.bold("◆ Developer status"))}`));
|
|
@@ -558,7 +836,8 @@ export class DeveloperStatusPanel {
|
|
|
558
836
|
`protocol ${this.theme.fg(protocolColor(currentProtocol), currentProtocol)}` +
|
|
559
837
|
this.theme.fg("dim", " · ") +
|
|
560
838
|
`target ${this.theme.fg("muted", state.activeRoute?.owner ?? "none")}`;
|
|
561
|
-
for (const line of wrapTextWithAnsi(summary, Math.max(1, innerWidth - 2)))
|
|
839
|
+
for (const line of wrapTextWithAnsi(summary, Math.max(1, innerWidth - 2)))
|
|
840
|
+
rows.push(row(` ${line}`));
|
|
562
841
|
rows.push(row());
|
|
563
842
|
|
|
564
843
|
section("Active route");
|
|
@@ -587,7 +866,12 @@ export class DeveloperStatusPanel {
|
|
|
587
866
|
addWrapped("resolves when", question.resolutionCriteria, "dim", 2);
|
|
588
867
|
}
|
|
589
868
|
if (state.pendingQuestions.length > 4) {
|
|
590
|
-
addWrapped(
|
|
869
|
+
addWrapped(
|
|
870
|
+
"more",
|
|
871
|
+
`${state.pendingQuestions.length - 4} additional open questions`,
|
|
872
|
+
"dim",
|
|
873
|
+
1,
|
|
874
|
+
);
|
|
591
875
|
}
|
|
592
876
|
}
|
|
593
877
|
|
|
@@ -598,7 +882,9 @@ export class DeveloperStatusPanel {
|
|
|
598
882
|
} else {
|
|
599
883
|
const recentJudgments = state.judgmentHistory.slice(-10).toReversed();
|
|
600
884
|
for (const judgment of recentJudgments) {
|
|
601
|
-
const route = state.routeHistory.find(
|
|
885
|
+
const route = state.routeHistory.find(
|
|
886
|
+
(candidate) => candidate.routeId === judgment.routeId,
|
|
887
|
+
);
|
|
602
888
|
addWrapped(
|
|
603
889
|
`${judgment.owner} ${judgment.status}`,
|
|
604
890
|
`${judgment.question} → ${judgmentSummary(judgment.result)}`,
|
|
@@ -610,11 +896,21 @@ export class DeveloperStatusPanel {
|
|
|
610
896
|
addWrapped(`considered ${alternative.owner}`, alternative.reason, "dim", 2);
|
|
611
897
|
}
|
|
612
898
|
for (const update of judgment.questionUpdates ?? []) {
|
|
613
|
-
addWrapped(
|
|
899
|
+
addWrapped(
|
|
900
|
+
`question ${update.status}`,
|
|
901
|
+
`${update.questionId} → ${update.result}`,
|
|
902
|
+
"accent",
|
|
903
|
+
2,
|
|
904
|
+
);
|
|
614
905
|
}
|
|
615
906
|
}
|
|
616
907
|
if (state.judgmentHistory.length > recentJudgments.length) {
|
|
617
|
-
addWrapped(
|
|
908
|
+
addWrapped(
|
|
909
|
+
"earlier",
|
|
910
|
+
`${state.judgmentHistory.length - recentJudgments.length} earlier judgments`,
|
|
911
|
+
"dim",
|
|
912
|
+
1,
|
|
913
|
+
);
|
|
618
914
|
}
|
|
619
915
|
}
|
|
620
916
|
|
|
@@ -656,7 +952,10 @@ export class DeveloperStatusPanel {
|
|
|
656
952
|
}
|
|
657
953
|
}
|
|
658
954
|
|
|
659
|
-
export async function showDeveloperStatus(
|
|
955
|
+
export async function showDeveloperStatus(
|
|
956
|
+
ctx: ExtensionCommandContext,
|
|
957
|
+
view: DeveloperStatusView,
|
|
958
|
+
): Promise<void> {
|
|
660
959
|
let mouseTerminal: WritableTerminal | undefined;
|
|
661
960
|
try {
|
|
662
961
|
await ctx.ui.custom<void>(
|
|
@@ -685,10 +984,14 @@ export async function showDeveloperStatus(ctx: ExtensionCommandContext, view: De
|
|
|
685
984
|
|
|
686
985
|
export function questionResolutionPrompt(question: PendingQuestion): string {
|
|
687
986
|
const requestLabel = resolutionRequestLabel(question.resolutionOwner);
|
|
987
|
+
const context = question.context
|
|
988
|
+
? ["", "Decision or evidence context:", question.context, ""]
|
|
989
|
+
: [];
|
|
688
990
|
return [
|
|
689
991
|
"Resolve this open Developer question.",
|
|
690
992
|
"",
|
|
691
993
|
`Question: ${question.question}`,
|
|
994
|
+
...context,
|
|
692
995
|
`Resolution owner: ${question.resolutionOwner}`,
|
|
693
996
|
`Gate: ${question.gate}`,
|
|
694
997
|
`Resolution criteria: ${question.resolutionCriteria}`,
|
|
@@ -704,6 +1007,7 @@ export function editQuestionResolutionRequest(
|
|
|
704
1007
|
ctx: ExtensionCommandContext,
|
|
705
1008
|
question: PendingQuestion,
|
|
706
1009
|
): Promise<string | undefined> {
|
|
1010
|
+
if (question.responseSpec) return collectChoiceResponse(ctx, question);
|
|
707
1011
|
const current = ctx.ui.getEditorText();
|
|
708
1012
|
const request = questionResolutionPrompt(question);
|
|
709
1013
|
const initial = current.trim() ? `${current.trimEnd()}\n\n${request}` : request;
|