@oh-my-pi/pi-tui 18.4.9 → 18.4.10
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/CHANGELOG.md +23 -0
- package/dist/types/chat/assistant-message.d.ts +4 -3
- package/dist/types/chat/messages.d.ts +4 -0
- package/dist/types/chrome/transcript-container.d.ts +7 -0
- package/dist/types/components/markdown.d.ts +0 -4
- package/dist/types/overlays/advisor-config.d.ts +8 -0
- package/dist/types/overlays/ask-dialog.d.ts +1 -0
- package/dist/types/prompt/composer.d.ts +3 -1
- package/dist/types/status-line/context-usage.d.ts +2 -0
- package/dist/types/terminal-capabilities.d.ts +5 -0
- package/dist/types/tui.d.ts +3 -0
- package/package.json +9 -9
- package/src/chat/advisor-message.ts +4 -2
- package/src/chat/assistant-message.ts +7 -9
- package/src/chat/chat-transcript-builder.ts +1 -1
- package/src/chat/messages.ts +4 -0
- package/src/chrome/transcript-container.ts +13 -0
- package/src/components/markdown.ts +90 -130
- package/src/keys.ts +2 -4
- package/src/native/encode.ts +2 -0
- package/src/overlays/advisor-config.ts +228 -2
- package/src/overlays/ask-dialog.ts +1 -0
- package/src/overlays/model-hub.ts +25 -4
- package/src/overlays/session-info-overlay.ts +55 -11
- package/src/prompt/composer.ts +32 -7
- package/src/status-line/context-usage.ts +18 -2
- package/src/terminal-capabilities.ts +8 -0
- package/src/terminal.ts +8 -8
- package/src/tools/wait.ts +7 -19
- package/src/tui.ts +4 -0
|
@@ -44,6 +44,12 @@ import { col, node, span, text } from "../native/describe";
|
|
|
44
44
|
import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
45
45
|
import { actionHint, hintsRow, type NativeHint, overlayCard } from "../native/overlay";
|
|
46
46
|
|
|
47
|
+
export const ADVISOR_REVIEW_MODES = ["turn", "agent-end"] as const;
|
|
48
|
+
export const ADVISOR_SYNC_BACKLOG_MODES = ["off", "1", "3", "5", "strict"] as const;
|
|
49
|
+
|
|
50
|
+
export type AdvisorReviewMode = (typeof ADVISOR_REVIEW_MODES)[number];
|
|
51
|
+
export type AdvisorSyncBacklog = (typeof ADVISOR_SYNC_BACKLOG_MODES)[number];
|
|
52
|
+
|
|
47
53
|
/** One advisor declared in `WATCHDOG.yml`; its instructions specialize the shared baseline. */
|
|
48
54
|
export interface AdvisorConfig {
|
|
49
55
|
name: string;
|
|
@@ -56,6 +62,9 @@ export interface AdvisorConfig {
|
|
|
56
62
|
enabled?: boolean;
|
|
57
63
|
/** Maximum non-blocker notes per advisor prompt update (default 4); blockers are exempt. */
|
|
58
64
|
maxNotesPerUpdate?: number;
|
|
65
|
+
reviewMode?: AdvisorReviewMode;
|
|
66
|
+
reviewInterval?: number;
|
|
67
|
+
syncBacklog?: AdvisorSyncBacklog;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
/** Which level a `WATCHDOG.yml` lives at: the project root or the user agent dir. */
|
|
@@ -115,6 +124,7 @@ export interface AdvisorConfigDeps {
|
|
|
115
124
|
externalEditor?: (text: string) => Promise<string | null>;
|
|
116
125
|
scopedModels: ReadonlyArray<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
117
126
|
availableToolNames: string[];
|
|
127
|
+
syncBacklog?: AdvisorSyncBacklog;
|
|
118
128
|
/** Formatted advisor-role model shown on the seeded default row (e.g. "anthropic/claude-..."). */
|
|
119
129
|
defaultModelLabel?: string;
|
|
120
130
|
}
|
|
@@ -192,13 +202,44 @@ function formatAdvisorTools(tools: readonly string[] | undefined, emptyLabel: st
|
|
|
192
202
|
return tools.length > 0 ? tools.join(", ") : emptyLabel;
|
|
193
203
|
}
|
|
194
204
|
|
|
205
|
+
/** Choice value for "no per-advisor override"; matches no {@link ADVISOR_SYNC_BACKLOG_MODES} entry. */
|
|
206
|
+
const SYNC_BACKLOG_INHERIT = "inherit";
|
|
207
|
+
|
|
208
|
+
/** One-line cadence label: mode plus interval. */
|
|
209
|
+
function formatReviewCadence(advisor: AdvisorConfig): string {
|
|
210
|
+
const reviewInterval = advisor.reviewInterval ?? 1;
|
|
211
|
+
return `${advisor.reviewMode ?? "turn"} · every ${reviewInterval} eligible update${reviewInterval === 1 ? "" : "s"}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Picker description for one explicit catch-up policy value. */
|
|
215
|
+
function describeSyncBacklogMode(mode: AdvisorSyncBacklog): string {
|
|
216
|
+
switch (mode) {
|
|
217
|
+
case "off":
|
|
218
|
+
return "Never wait for this advisor's backlog.";
|
|
219
|
+
case "strict":
|
|
220
|
+
return "Wait for all of this advisor's scheduled reviews; no wall-clock cap.";
|
|
221
|
+
default:
|
|
222
|
+
return `Wait until fewer than ${mode} scheduled reviews remain (30s cap).`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
195
226
|
/** Soft-wrap plain text to `width`, returning at least one (possibly empty) line. */
|
|
196
227
|
function wrap(text: string, width: number): string[] {
|
|
197
228
|
if (!text) return [""];
|
|
198
229
|
return Bun.wrapAnsi(text, Math.max(1, width), { trim: false }).split("\n");
|
|
199
230
|
}
|
|
200
231
|
|
|
201
|
-
type Screen =
|
|
232
|
+
type Screen =
|
|
233
|
+
| "list"
|
|
234
|
+
| "detail"
|
|
235
|
+
| "name"
|
|
236
|
+
| "model"
|
|
237
|
+
| "tools"
|
|
238
|
+
| "thinking"
|
|
239
|
+
| "review-mode"
|
|
240
|
+
| "review-interval"
|
|
241
|
+
| "sync-backlog"
|
|
242
|
+
| "instructions";
|
|
202
243
|
|
|
203
244
|
/** Footer key hints per screen for the described overlay (the ANSI footer strings carry the same keys). */
|
|
204
245
|
function screenHints(screen: Screen): (NativeHint | undefined)[] {
|
|
@@ -220,7 +261,11 @@ function screenHints(screen: Screen): (NativeHint | undefined)[] {
|
|
|
220
261
|
actionHint("tui.select.cancel", "back"),
|
|
221
262
|
];
|
|
222
263
|
case "thinking":
|
|
264
|
+
case "review-mode":
|
|
265
|
+
case "sync-backlog":
|
|
223
266
|
return [actionHint("tui.select.confirm", "pick"), actionHint("tui.select.cancel", "back")];
|
|
267
|
+
case "review-interval":
|
|
268
|
+
return [actionHint("tui.input.submit", "save"), actionHint("tui.select.cancel", "cancel")];
|
|
224
269
|
case "tools":
|
|
225
270
|
return [
|
|
226
271
|
actionHint("tui.select.confirm", "toggle"),
|
|
@@ -432,6 +477,44 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
432
477
|
defaultLabel: model ? (this.#defaultModelLabel ?? "advisor role default") : undefined,
|
|
433
478
|
control: { k: "action", label: model || this.#defaultModelLabel || "advisor role default", act: "edit" },
|
|
434
479
|
},
|
|
480
|
+
{
|
|
481
|
+
id: "reviewMode",
|
|
482
|
+
label: "Review mode",
|
|
483
|
+
hint: "turn reviews every primary turn; agent-end reviews only final yields.",
|
|
484
|
+
changed: advisor.reviewMode !== undefined ? true : undefined,
|
|
485
|
+
control: {
|
|
486
|
+
k: "choice",
|
|
487
|
+
value: advisor.reviewMode ?? "turn",
|
|
488
|
+
options: ADVISOR_REVIEW_MODES.map(mode => ({ value: mode, label: mode })),
|
|
489
|
+
style: "segmented",
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
id: "reviewInterval",
|
|
494
|
+
label: "Review interval",
|
|
495
|
+
hint: "Review every Nth eligible update; skipped updates join the next review.",
|
|
496
|
+
changed: advisor.reviewInterval !== undefined ? true : undefined,
|
|
497
|
+
control: { k: "number", value: advisor.reviewInterval ?? 1, min: 1, step: 1 },
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
id: "syncBacklog",
|
|
501
|
+
label: "Sync backlog",
|
|
502
|
+
hint: `Catch-up policy; inherit follows advisor.syncBacklog (currently ${this.#deps.syncBacklog ?? "off"}).`,
|
|
503
|
+
changed: advisor.syncBacklog !== undefined ? true : undefined,
|
|
504
|
+
control: {
|
|
505
|
+
k: "choice",
|
|
506
|
+
value: advisor.syncBacklog ?? SYNC_BACKLOG_INHERIT,
|
|
507
|
+
options: [
|
|
508
|
+
{ value: SYNC_BACKLOG_INHERIT, label: "inherit" },
|
|
509
|
+
...ADVISOR_SYNC_BACKLOG_MODES.map(mode => ({
|
|
510
|
+
value: mode,
|
|
511
|
+
label: mode,
|
|
512
|
+
detail: describeSyncBacklogMode(mode),
|
|
513
|
+
})),
|
|
514
|
+
],
|
|
515
|
+
style: "menu",
|
|
516
|
+
},
|
|
517
|
+
},
|
|
435
518
|
{
|
|
436
519
|
id: "tools",
|
|
437
520
|
label: "Tools",
|
|
@@ -499,6 +582,9 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
499
582
|
const overlay =
|
|
500
583
|
this.#screen === "model" ||
|
|
501
584
|
this.#screen === "thinking" ||
|
|
585
|
+
this.#screen === "review-mode" ||
|
|
586
|
+
this.#screen === "review-interval" ||
|
|
587
|
+
this.#screen === "sync-backlog" ||
|
|
502
588
|
this.#screen === "tools" ||
|
|
503
589
|
this.#screen === "instructions";
|
|
504
590
|
const editor = overlay ? this.#active : undefined;
|
|
@@ -573,6 +659,24 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
573
659
|
advisor.name = event.value.trim();
|
|
574
660
|
this.#dirty = true;
|
|
575
661
|
this.#showDetail(index);
|
|
662
|
+
} else if (event.item === "reviewMode" && (typeof event.value === "string" || event.value === null)) {
|
|
663
|
+
// `turn` is the default, stored as an omitted field; `null` resets to it.
|
|
664
|
+
const mode = event.value === null ? "turn" : ADVISOR_REVIEW_MODES.find(m => m === event.value);
|
|
665
|
+
if (!mode) return;
|
|
666
|
+
advisor.reviewMode = mode === "turn" ? undefined : mode;
|
|
667
|
+
this.#dirty = true;
|
|
668
|
+
this.#showDetail(index);
|
|
669
|
+
} else if (event.item === "reviewInterval" && (typeof event.value === "number" || event.value === null)) {
|
|
670
|
+
const interval = event.value ?? 1;
|
|
671
|
+
if (!Number.isSafeInteger(interval) || interval < 1) return;
|
|
672
|
+
advisor.reviewInterval = interval === 1 ? undefined : interval;
|
|
673
|
+
this.#dirty = true;
|
|
674
|
+
this.#showDetail(index);
|
|
675
|
+
} else if (event.item === "syncBacklog" && (typeof event.value === "string" || event.value === null)) {
|
|
676
|
+
// The inherit sentinel and `null` match no mode, so the override clears.
|
|
677
|
+
advisor.syncBacklog = ADVISOR_SYNC_BACKLOG_MODES.find(mode => mode === event.value);
|
|
678
|
+
this.#dirty = true;
|
|
679
|
+
this.#showDetail(index);
|
|
576
680
|
}
|
|
577
681
|
return;
|
|
578
682
|
}
|
|
@@ -689,6 +793,8 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
689
793
|
{ k: "Enabled", v: advisor.enabled === false ? "○ off" : "● on" },
|
|
690
794
|
{ k: "Model", v: model },
|
|
691
795
|
{ k: "Tools", v: formatAdvisorTools(advisor.tools, "no tools") },
|
|
796
|
+
{ k: "Review", v: formatReviewCadence(advisor) },
|
|
797
|
+
{ k: "Sync backlog", v: this.#syncBacklogLabel(advisor) },
|
|
692
798
|
],
|
|
693
799
|
}),
|
|
694
800
|
text([span("Instructions:", "dim")]),
|
|
@@ -822,12 +928,15 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
822
928
|
#advisorPreview(advisor: AdvisorConfig, bodyWidth: number): string[] {
|
|
823
929
|
const model = advisor.model?.trim() || this.#defaultModelLabel || "advisor role default";
|
|
824
930
|
const tools = formatAdvisorTools(advisor.tools, "no tools");
|
|
931
|
+
const syncBacklog = this.#syncBacklogLabel(advisor);
|
|
825
932
|
const lines = [
|
|
826
933
|
theme.bold(advisor.name || "(unnamed)"),
|
|
827
934
|
"",
|
|
828
935
|
`${theme.fg("dim", "Enabled:")} ${advisor.enabled === false ? "○ off" : "● on"}`,
|
|
829
936
|
`${theme.fg("dim", "Model:")} ${model}`,
|
|
830
937
|
`${theme.fg("dim", "Tools:")} ${tools}`,
|
|
938
|
+
`${theme.fg("dim", "Review:")} ${formatReviewCadence(advisor)}`,
|
|
939
|
+
`${theme.fg("dim", "Sync backlog:")} ${syncBacklog}`,
|
|
831
940
|
"",
|
|
832
941
|
theme.fg("dim", "Instructions:"),
|
|
833
942
|
];
|
|
@@ -893,6 +1002,9 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
893
1002
|
advisor?.name === "default" &&
|
|
894
1003
|
!advisor.model?.trim() &&
|
|
895
1004
|
advisor.tools === undefined &&
|
|
1005
|
+
advisor.reviewMode === undefined &&
|
|
1006
|
+
advisor.reviewInterval === undefined &&
|
|
1007
|
+
advisor.syncBacklog === undefined &&
|
|
896
1008
|
!advisor.instructions?.trim() &&
|
|
897
1009
|
advisor.enabled !== false &&
|
|
898
1010
|
advisor.maxNotesPerUpdate === undefined
|
|
@@ -902,7 +1014,12 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
902
1014
|
#advisorSummary(advisor: AdvisorConfig): string {
|
|
903
1015
|
const model = advisor.model?.trim() || this.#defaultModelLabel || "advisor role default";
|
|
904
1016
|
const tools = formatAdvisorTools(advisor.tools, "no tools");
|
|
905
|
-
return `${model} · ${tools}`;
|
|
1017
|
+
return `${model} · ${tools} · review ${advisor.reviewMode ?? "turn"}/${advisor.reviewInterval ?? 1}`;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/** The advisor's explicit catch-up override, else the inherited global policy. */
|
|
1021
|
+
#syncBacklogLabel(advisor: AdvisorConfig): string {
|
|
1022
|
+
return advisor.syncBacklog ?? `${this.#deps.syncBacklog ?? "off"} (inherited)`;
|
|
906
1023
|
}
|
|
907
1024
|
|
|
908
1025
|
#showList(): void {
|
|
@@ -1000,6 +1117,8 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
1000
1117
|
this.#detailIndex = index;
|
|
1001
1118
|
const modelDescription = advisor.model?.trim() || this.#defaultModelLabel || "advisor role default";
|
|
1002
1119
|
const toolsDescription = formatAdvisorTools(advisor.tools, "no tools");
|
|
1120
|
+
const reviewMode = advisor.reviewMode ?? "turn";
|
|
1121
|
+
const reviewInterval = advisor.reviewInterval ?? 1;
|
|
1003
1122
|
const items: SelectItem[] = [
|
|
1004
1123
|
{ value: "name", label: "Name", description: advisor.name },
|
|
1005
1124
|
{
|
|
@@ -1008,6 +1127,13 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
1008
1127
|
description: advisor.enabled === false ? "○ off" : "● on",
|
|
1009
1128
|
},
|
|
1010
1129
|
{ value: "model", label: "Model", description: modelDescription },
|
|
1130
|
+
{ value: "reviewMode", label: "Review mode", description: reviewMode },
|
|
1131
|
+
{ value: "reviewInterval", label: "Review interval", description: String(reviewInterval) },
|
|
1132
|
+
{
|
|
1133
|
+
value: "syncBacklog",
|
|
1134
|
+
label: "Sync backlog",
|
|
1135
|
+
description: this.#syncBacklogLabel(advisor),
|
|
1136
|
+
},
|
|
1011
1137
|
];
|
|
1012
1138
|
if (advisor.model?.trim()) {
|
|
1013
1139
|
items.push({ value: "resetModel", label: "Reset model to advisor-role default" });
|
|
@@ -1043,6 +1169,15 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
1043
1169
|
case "model":
|
|
1044
1170
|
this.#showModelPicker(index);
|
|
1045
1171
|
return;
|
|
1172
|
+
case "reviewMode":
|
|
1173
|
+
this.#showReviewModePicker(index);
|
|
1174
|
+
return;
|
|
1175
|
+
case "reviewInterval":
|
|
1176
|
+
this.#showReviewIntervalEditor(index);
|
|
1177
|
+
return;
|
|
1178
|
+
case "syncBacklog":
|
|
1179
|
+
this.#showSyncBacklogPicker(index);
|
|
1180
|
+
return;
|
|
1046
1181
|
case "tools":
|
|
1047
1182
|
this.#showToolsEditor(index, new Set(this.#doc.advisors[index].tools ?? this.#deps.defaultToolNames), 0);
|
|
1048
1183
|
return;
|
|
@@ -1137,6 +1272,97 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
1137
1272
|
`Thinking effort for ${selector} · ${editorKey("tui.select.confirm")} / click pick · ${editorKey("tui.select.cancel")} back`,
|
|
1138
1273
|
);
|
|
1139
1274
|
}
|
|
1275
|
+
#showReviewModePicker(index: number): void {
|
|
1276
|
+
const advisor = this.#doc.advisors[index];
|
|
1277
|
+
if (!advisor) {
|
|
1278
|
+
this.#showList();
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
const current = advisor.reviewMode ?? ADVISOR_REVIEW_MODES[0];
|
|
1282
|
+
const items: SelectItem[] = ADVISOR_REVIEW_MODES.map(mode => ({
|
|
1283
|
+
value: mode,
|
|
1284
|
+
label: mode === current ? `${mode} (current)` : mode,
|
|
1285
|
+
description:
|
|
1286
|
+
mode === "turn"
|
|
1287
|
+
? "Review every primary turn (tool-call round)."
|
|
1288
|
+
: "Review only at agent end (once per run).",
|
|
1289
|
+
}));
|
|
1290
|
+
const list = new SelectList(items, Math.max(1, items.length), getSelectListTheme());
|
|
1291
|
+
list.setSelectedIndex(ADVISOR_REVIEW_MODES.indexOf(current));
|
|
1292
|
+
list.onSelect = item => {
|
|
1293
|
+
advisor.reviewMode = item.value === "turn" ? undefined : "agent-end";
|
|
1294
|
+
this.#dirty = true;
|
|
1295
|
+
this.#showDetail(index);
|
|
1296
|
+
};
|
|
1297
|
+
list.onCancel = () => this.#showDetail(index);
|
|
1298
|
+
this.#setScreen(
|
|
1299
|
+
"review-mode",
|
|
1300
|
+
list,
|
|
1301
|
+
`${editorKey("tui.select.confirm")} / click choose review mode · ${editorKey("tui.select.cancel")} back`,
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
#showReviewIntervalEditor(index: number): void {
|
|
1306
|
+
const advisor = this.#doc.advisors[index];
|
|
1307
|
+
if (!advisor) {
|
|
1308
|
+
this.#showList();
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
const input = new Input();
|
|
1312
|
+
input.setValue(String(advisor.reviewInterval ?? 1));
|
|
1313
|
+
input.onSubmit = value => {
|
|
1314
|
+
const interval = Number(value.trim());
|
|
1315
|
+
if (!Number.isSafeInteger(interval) || interval < 1) {
|
|
1316
|
+
this.#cb.notify("Review interval must be a positive integer.");
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
advisor.reviewInterval = interval === 1 ? undefined : interval;
|
|
1320
|
+
this.#dirty = true;
|
|
1321
|
+
this.#showDetail(index);
|
|
1322
|
+
};
|
|
1323
|
+
input.onEscape = () => this.#showDetail(index);
|
|
1324
|
+
this.#setScreen(
|
|
1325
|
+
"review-interval",
|
|
1326
|
+
input,
|
|
1327
|
+
`Type a positive integer · ${editorKey("tui.input.submit")} save · ${editorKey("tui.select.cancel")} cancel`,
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
#showSyncBacklogPicker(index: number): void {
|
|
1332
|
+
const advisor = this.#doc.advisors[index];
|
|
1333
|
+
if (!advisor) {
|
|
1334
|
+
this.#showList();
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const global = this.#deps.syncBacklog ?? "off";
|
|
1338
|
+
const current = advisor.syncBacklog;
|
|
1339
|
+
const items: SelectItem[] = [
|
|
1340
|
+
{
|
|
1341
|
+
value: SYNC_BACKLOG_INHERIT,
|
|
1342
|
+
label: current === undefined ? "inherit (current)" : "inherit",
|
|
1343
|
+
description: `Follow the global advisor.syncBacklog setting (currently "${global}").`,
|
|
1344
|
+
},
|
|
1345
|
+
...ADVISOR_SYNC_BACKLOG_MODES.map(mode => ({
|
|
1346
|
+
value: mode,
|
|
1347
|
+
label: mode === current ? `${mode} (current)` : mode,
|
|
1348
|
+
description: describeSyncBacklogMode(mode),
|
|
1349
|
+
})),
|
|
1350
|
+
];
|
|
1351
|
+
const list = new SelectList(items, Math.max(1, items.length), getSelectListTheme());
|
|
1352
|
+
list.setSelectedIndex(current === undefined ? 0 : ADVISOR_SYNC_BACKLOG_MODES.indexOf(current) + 1);
|
|
1353
|
+
list.onSelect = item => {
|
|
1354
|
+
// The inherit sentinel matches no mode, so `find` yields undefined — the override clears.
|
|
1355
|
+
advisor.syncBacklog = ADVISOR_SYNC_BACKLOG_MODES.find(mode => mode === item.value);
|
|
1356
|
+
this.#dirty = true;
|
|
1357
|
+
this.#showDetail(index);
|
|
1358
|
+
};
|
|
1359
|
+
list.onCancel = () => this.#showDetail(index);
|
|
1360
|
+
this.#setScreen(
|
|
1361
|
+
"sync-backlog",
|
|
1362
|
+
list,
|
|
1363
|
+
`${editorKey("tui.select.confirm")} / click choose catch-up policy · ${editorKey("tui.select.cancel")} back`,
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1140
1366
|
|
|
1141
1367
|
#showToolsEditor(index: number, selected: Set<string>, cursor: number): void {
|
|
1142
1368
|
const all = this.#availableToolNames;
|
|
@@ -528,6 +528,7 @@ export function normalizeDialogQuestions(questions: ExtensionAskDialogQuestion[]
|
|
|
528
528
|
}
|
|
529
529
|
|
|
530
530
|
export class AskDialogComponent implements Component {
|
|
531
|
+
readonly retireDisplacedTranscript = true;
|
|
531
532
|
#states: QuestionState[];
|
|
532
533
|
#activeTabIndex = 0;
|
|
533
534
|
#submitScrollOffset = 0;
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
import type { KeysApi, Model } from "@oh-my-pi/pi-ai";
|
|
24
24
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
25
25
|
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
26
|
+
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
26
27
|
import { providerEntry } from "@oh-my-pi/pi-catalog/compat/providers";
|
|
27
28
|
import { MODEL_KINDS, modelKind, type ModelKind } from "@oh-my-pi/pi-catalog/types";
|
|
28
29
|
import type { Component, TUI } from "../tui";
|
|
@@ -483,6 +484,22 @@ export class ModelHubComponent implements Component {
|
|
|
483
484
|
return this.#settings.knownRoleIds.filter(role => !this.#settings.getRoleInfo(role).hidden);
|
|
484
485
|
}
|
|
485
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Models a `--models`/`enabledModels` scope exposes. The scope only resolves
|
|
489
|
+
* chat models (it feeds Ctrl+P cycling), so available non-chat runners
|
|
490
|
+
* (judge, search, image, …) join from the registry — runtime role
|
|
491
|
+
* resolution ignores the scope for them as well.
|
|
492
|
+
*/
|
|
493
|
+
#scopedPool(): Model[] {
|
|
494
|
+
const pool = this.#scopedModels.map(scoped => scoped.model);
|
|
495
|
+
for (const model of this.#registry.getAvailable("all")) {
|
|
496
|
+
if (modelKind(model) === "chat") continue;
|
|
497
|
+
if (this.#scopedModels.some(scoped => modelsAreEqual(scoped.model, model))) continue;
|
|
498
|
+
pool.push(model);
|
|
499
|
+
}
|
|
500
|
+
return pool;
|
|
501
|
+
}
|
|
502
|
+
|
|
486
503
|
/** Resolve every known role: configured values first, auto-selection for the rest. */
|
|
487
504
|
#reloadRoles(autoCandidates: ReadonlyArray<Model>): void {
|
|
488
505
|
const allModels = this.#scopedModels.length > 0 ? autoCandidates : this.#registry.getAll("all");
|
|
@@ -499,9 +516,14 @@ export class ModelHubComponent implements Component {
|
|
|
499
516
|
let allModels: ReadonlyArray<Model>;
|
|
500
517
|
let availableModels: ReadonlyArray<Model>;
|
|
501
518
|
if (this.#scopedModels.length > 0) {
|
|
502
|
-
allModels = this.#scopedModels.map(scoped => scoped.model);
|
|
503
|
-
availableModels = allModels;
|
|
504
519
|
this.#configError = undefined;
|
|
520
|
+
try {
|
|
521
|
+
allModels = this.#scopedPool();
|
|
522
|
+
} catch (error) {
|
|
523
|
+
this.#configError = error instanceof Error ? error.message : String(error);
|
|
524
|
+
allModels = this.#scopedModels.map(scoped => scoped.model);
|
|
525
|
+
}
|
|
526
|
+
availableModels = allModels;
|
|
505
527
|
} else {
|
|
506
528
|
const loadError = this.#registry.getError();
|
|
507
529
|
this.#configError = loadError ? String(loadError) : undefined;
|
|
@@ -1081,8 +1103,7 @@ export class ModelHubComponent implements Component {
|
|
|
1081
1103
|
#roleForScope(role: string, scope: ModelRoleSelectionScope): ResolvedModelRoleValue {
|
|
1082
1104
|
const roleValue =
|
|
1083
1105
|
scope === "project" ? this.#settings.getProjectModelRole(role) : this.#settings.getGlobalModelRole(role);
|
|
1084
|
-
const allModels =
|
|
1085
|
-
this.#scopedModels.length > 0 ? this.#scopedModels.map(scoped => scoped.model) : this.#registry.getAll("all");
|
|
1106
|
+
const allModels = this.#scopedModels.length > 0 ? this.#scopedPool() : this.#registry.getAll("all");
|
|
1086
1107
|
const roleLookup: ModelRoleLookup = {
|
|
1087
1108
|
getModelRole: scopedRole =>
|
|
1088
1109
|
scope === "project"
|
|
@@ -4,9 +4,9 @@ import { matchesSelectCancel } from "../keybinding-matchers";
|
|
|
4
4
|
import { OverlayPanel, PanelDivider, PanelRows } from "../chrome/overlay-box";
|
|
5
5
|
import { formatKeyHints } from "../app-keybindings";
|
|
6
6
|
import { editorKey } from "../chrome/keybinding-hints";
|
|
7
|
-
import type { TspSpan } from "@oh-my-pi/pi-wire";
|
|
7
|
+
import type { TspProps, TspSpan } from "@oh-my-pi/pi-wire";
|
|
8
8
|
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
9
|
-
import { col, node, span, text } from "../native/describe";
|
|
9
|
+
import { col, node, row, span, text } from "../native/describe";
|
|
10
10
|
import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
11
11
|
import { actionBar, actionButton } from "../native/overlay";
|
|
12
12
|
import { plainText, styledSpans } from "../native/spans";
|
|
@@ -52,6 +52,39 @@ function parseKvReport(report: string): KvReportSection[] {
|
|
|
52
52
|
return sections.filter(section => section.head !== undefined || section.entries.length > 0);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/** The plain text of `spans`. */
|
|
56
|
+
function joined(spans: readonly TspSpan[]): string {
|
|
57
|
+
return spans.map(part => part.t).join("");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A value with a copy button after it: a click anywhere on the line copies its
|
|
62
|
+
* text (the terminal-local `copy` action takes the leaf text; the icon has none).
|
|
63
|
+
*/
|
|
64
|
+
function copyLine(
|
|
65
|
+
spans: TspSpan[],
|
|
66
|
+
title: string,
|
|
67
|
+
key: string,
|
|
68
|
+
props: Omit<TspProps<"text">, "text" | "spans"> = {},
|
|
69
|
+
): NativeNode {
|
|
70
|
+
return node(
|
|
71
|
+
"row",
|
|
72
|
+
{
|
|
73
|
+
gap: "sm",
|
|
74
|
+
align: "center",
|
|
75
|
+
grow: 1,
|
|
76
|
+
role: "omp.info.copy",
|
|
77
|
+
actions: { click: "copy" },
|
|
78
|
+
title,
|
|
79
|
+
},
|
|
80
|
+
[
|
|
81
|
+
text(spans, { ...props, grow: 1, shrink: 1 }),
|
|
82
|
+
node("icon", { name: "copy", role: "omp.info.copy.ic", aria: title }),
|
|
83
|
+
],
|
|
84
|
+
key,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
55
88
|
/** Terminal surface needed to size the session info viewport. */
|
|
56
89
|
export interface SessionInfoOverlayHost {
|
|
57
90
|
readonly terminal: {
|
|
@@ -155,16 +188,21 @@ export class SessionInfoOverlay implements Component {
|
|
|
155
188
|
} else if (section.head === undefined && entry.k === "ID") {
|
|
156
189
|
// The id leads the sheet, once, as a copyable mono line.
|
|
157
190
|
body.push(
|
|
158
|
-
|
|
159
|
-
|
|
191
|
+
copyLine([span(joined(entry.v), "mono muted")], "Copy session ID", "id", { truncate: "middle" }),
|
|
192
|
+
);
|
|
193
|
+
} else if (section.head === undefined && entry.k === "File") {
|
|
194
|
+
flush();
|
|
195
|
+
children.push(
|
|
196
|
+
row(
|
|
197
|
+
[
|
|
198
|
+
text([span("File", "muted")], { wrap: "none", shrink: 0 }),
|
|
199
|
+
copyLine([span(joined(entry.v), "mono")], "Copy file path", "path", { wrap: "char" }),
|
|
200
|
+
],
|
|
160
201
|
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
truncate: "middle",
|
|
202
|
+
gap: "md",
|
|
203
|
+
align: "start",
|
|
204
|
+
role: "omp.info.file",
|
|
165
205
|
},
|
|
166
|
-
undefined,
|
|
167
|
-
"id",
|
|
168
206
|
),
|
|
169
207
|
);
|
|
170
208
|
} else {
|
|
@@ -195,7 +233,13 @@ export class SessionInfoOverlay implements Component {
|
|
|
195
233
|
[
|
|
196
234
|
text([span("Context", "muted")]),
|
|
197
235
|
meter
|
|
198
|
-
? node("meter", {
|
|
236
|
+
? node("meter", {
|
|
237
|
+
value,
|
|
238
|
+
style: "bar",
|
|
239
|
+
size: "md",
|
|
240
|
+
thresholds: { warn: 0.7, bad: 0.9 },
|
|
241
|
+
grow: 1,
|
|
242
|
+
})
|
|
199
243
|
: node("progress", { value, grow: 1 }),
|
|
200
244
|
text([span(figure, "mono")]),
|
|
201
245
|
],
|
package/src/prompt/composer.ts
CHANGED
|
@@ -98,7 +98,9 @@ export interface RuntimeChildrenOptions {
|
|
|
98
98
|
* multi-line editor). They are billed at their smallest height so expansion
|
|
99
99
|
* clips the live tail instead of retiring rows a later shrink could not
|
|
100
100
|
* reclaim (#11007). Every other root is billed at its current height, so
|
|
101
|
-
* settled rows it displaces retire to native scrollback.
|
|
101
|
+
* settled rows it displaces retire to native scrollback. Decision panels
|
|
102
|
+
* declaring `retireDisplacedTranscript` opt out of this floor even when
|
|
103
|
+
* mounted as a child of a transient editor container.
|
|
102
104
|
*/
|
|
103
105
|
readonly transient?: readonly Component[];
|
|
104
106
|
/**
|
|
@@ -266,7 +268,7 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
266
268
|
#lastNormalRows = 0;
|
|
267
269
|
// Roots from `RuntimeChildrenOptions.transient`, and the smallest height
|
|
268
270
|
// they have rendered at since mount. Retirement bills transient roots at
|
|
269
|
-
// this floor, never their peak, so a dialog or tall editor that later
|
|
271
|
+
// this floor, never their peak, so a transient dialog or tall editor that later
|
|
270
272
|
// shrinks never leaves committed transcript rows the live viewport cannot
|
|
271
273
|
// reclaim (#11007). Persistent roots (loader, todo/subagent HUDs) bill at
|
|
272
274
|
// their current height: they stay up for a whole turn, and billing them at
|
|
@@ -276,6 +278,7 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
276
278
|
// rediscovered from whatever chrome is expanded when the height changes.
|
|
277
279
|
#transientChrome: ReadonlySet<Component> = new Set();
|
|
278
280
|
#transientChromeFloor: number | undefined;
|
|
281
|
+
#anchorAfterInlineRetirement = false;
|
|
279
282
|
#lastInterruptAt = 0;
|
|
280
283
|
/** Last described surface; its arrays are reused while their children are unchanged. */
|
|
281
284
|
#nativeSurface: NativeSurface = { main: [], dock: [] };
|
|
@@ -368,29 +371,43 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
368
371
|
const after: string[] = [];
|
|
369
372
|
const afterSpans: ViewportClickSpan[] = [];
|
|
370
373
|
let transientRows = 0;
|
|
374
|
+
let displacingRows = 0;
|
|
375
|
+
let decisionPanelOpen = false;
|
|
371
376
|
for (const root of afterRoots) {
|
|
377
|
+
const chrome: Component = root;
|
|
372
378
|
const start = after.length;
|
|
373
379
|
this.#renderBelowRoot(root, width, after, afterSpans);
|
|
374
380
|
if (this.#transientChrome.has(root)) transientRows += after.length - start;
|
|
381
|
+
if (
|
|
382
|
+
chrome.retireDisplacedTranscript ||
|
|
383
|
+
(root instanceof Container && root.children.some(child => child.retireDisplacedTranscript))
|
|
384
|
+
) {
|
|
385
|
+
if (this.#transientChrome.has(root)) displacingRows += after.length - start;
|
|
386
|
+
decisionPanelOpen = true;
|
|
387
|
+
}
|
|
375
388
|
}
|
|
376
389
|
// Offer history under capacity pressure only: blocks stay live (and keep
|
|
377
390
|
// reflowing to the current width) while the screen has room. A batch
|
|
378
391
|
// leaves the mutable viewport in the same frame it is appended, so its
|
|
379
392
|
// rows are never painted twice.
|
|
380
393
|
//
|
|
381
|
-
// Retirement bills transient roots at their floor, not their
|
|
382
|
-
// confirmation dialog or a tall multi-line editor clips the live tail for
|
|
394
|
+
// Retirement bills ordinary transient roots at their floor, not their
|
|
395
|
+
// peak: a confirmation dialog or a tall multi-line editor clips the live tail for
|
|
383
396
|
// its lifetime, but must not permanently commit transcript rows to native
|
|
384
397
|
// history — otherwise a later shrink cannot refill the freed rows and the
|
|
385
398
|
// editor drifts up above a band of blank rows (#11007).
|
|
386
399
|
this.#transientChromeFloor = Math.min(this.#transientChromeFloor ?? transientRows, transientRows);
|
|
387
|
-
|
|
400
|
+
// An ask panel remains open while the user reads the preceding response.
|
|
401
|
+
// Its displaced settled rows must be reachable in scrollback now, rather
|
|
402
|
+
// than clipped until the panel closes. Ordinary drafts retain their floor.
|
|
403
|
+
const belowFloor = after.length - transientRows + Math.max(this.#transientChromeFloor, displacingRows);
|
|
388
404
|
const now = performance.now();
|
|
389
405
|
const frame: AnimationFrame = { now, tick: Math.floor(now / 80) };
|
|
390
406
|
// Retirement measures the same live blocks the viewport lays out below;
|
|
391
407
|
// one open frame renders each of them once for both.
|
|
392
408
|
transcript.beginFrame(frame);
|
|
393
409
|
const history = this.#offerHistory(transcript, width, rows, preRoots.length + belowFloor);
|
|
410
|
+
if (decisionPanelOpen && history !== undefined) this.#anchorAfterInlineRetirement = true;
|
|
394
411
|
const headerVisible = !this.#headerRetired && this.#offeredHistory?.source !== "header";
|
|
395
412
|
const headerRows = headerVisible ? this.#header.render(width) : [];
|
|
396
413
|
const before = [...headerRows, ...preRoots];
|
|
@@ -408,6 +425,13 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
408
425
|
}
|
|
409
426
|
const drop = Math.max(0, before.length + active.length + after.length - rows);
|
|
410
427
|
const mutable = [...before, ...active, ...after].slice(drop);
|
|
428
|
+
// Once live rows fill the screen again, the retired gap is gone.
|
|
429
|
+
if (!decisionPanelOpen && mutable.length >= rows) this.#anchorAfterInlineRetirement = false;
|
|
430
|
+
// Rows retired during a decision panel cannot be pulled back from native
|
|
431
|
+
// history when it closes. Keep the input pinned to the bottom without
|
|
432
|
+
// replaying those rows (which would duplicate them) or clearing history.
|
|
433
|
+
const topPadding = this.#anchorAfterInlineRetirement ? Math.max(0, rows - mutable.length) : 0;
|
|
434
|
+
if (topPadding > 0) mutable.unshift(...Array<string>(topPadding).fill(""));
|
|
411
435
|
const viewportLength = mutable.length;
|
|
412
436
|
const spans: ViewportClickSpan[] = [];
|
|
413
437
|
const shift = (span: ViewportClickSpan, base: number): void => {
|
|
@@ -421,8 +445,8 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
421
445
|
spans.push({ start: clamped, end, candidates: (local: number) => span.candidates(local + skew) });
|
|
422
446
|
}
|
|
423
447
|
};
|
|
424
|
-
for (const span of activeSpans) shift(span, before.length - drop);
|
|
425
|
-
for (const span of afterSpans) shift(span, before.length + active.length - drop);
|
|
448
|
+
for (const span of activeSpans) shift(span, topPadding + before.length - drop);
|
|
449
|
+
for (const span of afterSpans) shift(span, topPadding + before.length + active.length - drop);
|
|
426
450
|
this.#lastClickSpans = spans;
|
|
427
451
|
if (history !== undefined && this.#offeredHistory?.source === "header") {
|
|
428
452
|
const visibleHeaderRows = Math.max(0, rows - (mutable.length + drop));
|
|
@@ -901,6 +925,7 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
|
|
|
901
925
|
if (this.#stopped) return;
|
|
902
926
|
this.#transientChrome = new Set(options.transient);
|
|
903
927
|
this.#transientChromeFloor = undefined;
|
|
928
|
+
this.#anchorAfterInlineRetirement = false;
|
|
904
929
|
this.#nativeDock = options.nativeDock;
|
|
905
930
|
this.ui.removeChild(this.#statusHost);
|
|
906
931
|
if (this.#runtimeMounted) {
|
|
@@ -859,16 +859,32 @@ export function describeContextUsage(breakdown: ContextBreakdown): NativeNode {
|
|
|
859
859
|
* has them and as the glyph grid otherwise.
|
|
860
860
|
*/
|
|
861
861
|
export class ContextUsageView extends Container {
|
|
862
|
-
|
|
862
|
+
#breakdown: ContextBreakdown;
|
|
863
863
|
#native: { meter: boolean; node: NativeNode } | undefined;
|
|
864
|
+
readonly #theme: Theme;
|
|
864
865
|
|
|
865
866
|
constructor(breakdown: ContextBreakdown, theme: Theme) {
|
|
866
867
|
super();
|
|
867
868
|
this.#breakdown = breakdown;
|
|
869
|
+
this.#theme = theme;
|
|
870
|
+
this.#build();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** Replace the shown breakdown in place, refreshing both the ANSI and native views. */
|
|
874
|
+
setBreakdown(breakdown: ContextBreakdown): void {
|
|
875
|
+
this.#breakdown = breakdown;
|
|
876
|
+
this.#native = undefined;
|
|
877
|
+
this.clear();
|
|
878
|
+
this.#build();
|
|
879
|
+
this.invalidate();
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
#build(): void {
|
|
883
|
+
const theme = this.#theme;
|
|
868
884
|
this.addChild(new DynamicBorder());
|
|
869
885
|
this.addChild(new Text(theme.bold(theme.fg("accent", "Context Usage")), 1, 0));
|
|
870
886
|
this.addChild(new Spacer(1));
|
|
871
|
-
this.addChild(new Text(renderContextUsage(breakdown, theme), 1, 0));
|
|
887
|
+
this.addChild(new Text(renderContextUsage(this.#breakdown, theme), 1, 0));
|
|
872
888
|
this.addChild(new DynamicBorder());
|
|
873
889
|
}
|
|
874
890
|
|
|
@@ -283,6 +283,14 @@ export function isInsideZellij(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
|
283
283
|
return Boolean(env.ZELLIJ);
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Whether the agent process runs in an SSH session, so the terminal emulator
|
|
288
|
+
* is remote and host-local input/keyboard modes cannot be assumed.
|
|
289
|
+
*/
|
|
290
|
+
export function isSshSession(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
291
|
+
return Boolean(env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT);
|
|
292
|
+
}
|
|
293
|
+
|
|
286
294
|
export function isNotificationSuppressed(): boolean {
|
|
287
295
|
const value = $env.PI_NOTIFICATIONS;
|
|
288
296
|
if (!value) return false;
|