@f5-sales-demo/xcsh 21.45.3 → 21.46.2
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/package.json +8 -8
- package/src/config/settings-schema.ts +10 -0
- package/src/debug/log-viewer.ts +6 -6
- package/src/extensibility/extensions/bundled/herdr-reporter.ts +29 -0
- package/src/extensibility/extensions/types.ts +7 -0
- package/src/internal-urls/api-catalog-qmd-index.generated.ts +2 -2
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +13 -3
- package/src/modes/components/btw-panel.ts +5 -2
- package/src/modes/components/context-add-wizard.ts +5 -3
- package/src/modes/components/copy-selector.ts +6 -3
- package/src/modes/components/extensions/extension-dashboard.ts +8 -2
- package/src/modes/components/history-search.ts +2 -1
- package/src/modes/components/hook-editor.ts +8 -2
- package/src/modes/components/hook-input.ts +2 -1
- package/src/modes/components/hook-selector.ts +3 -2
- package/src/modes/components/login-dialog.ts +20 -10
- package/src/modes/components/mcp-add-wizard.ts +5 -3
- package/src/modes/components/model-selector.ts +70 -22
- package/src/modes/components/oauth-selector.ts +3 -2
- package/src/modes/components/plan-preview.ts +2 -2
- package/src/modes/components/plugin-settings.ts +2 -2
- package/src/modes/components/plugins/plugin-dashboard.ts +2 -1
- package/src/modes/components/question-transcript.ts +2 -2
- package/src/modes/components/recap-message.ts +23 -0
- package/src/modes/components/request-user-input.ts +6 -6
- package/src/modes/components/reviewed-action-dialog.ts +6 -3
- package/src/modes/components/searchable-select-list.ts +3 -2
- package/src/modes/components/selector-frame.ts +88 -21
- package/src/modes/components/session-observer-overlay.ts +2 -1
- package/src/modes/components/session-selector.ts +13 -5
- package/src/modes/components/settings-browser.ts +8 -2
- package/src/modes/components/settings-editors.ts +11 -4
- package/src/modes/components/settings-selector.ts +8 -2
- package/src/modes/components/transcript-notice.ts +15 -5
- package/src/modes/components/tree-selector.ts +11 -5
- package/src/modes/components/user-message-selector.ts +4 -2
- package/src/modes/components/welcome.ts +3 -3
- package/src/modes/controllers/event-controller.ts +5 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +10 -0
- package/src/remote-control/protocol-methods.json +3 -0
- package/src/remote-control/router.ts +2 -0
- package/src/remote-control/session.ts +24 -0
- package/src/session/agent-session.ts +73 -2
- package/src/session/recap.ts +341 -0
- package/src/slash-commands/builtin-registry.ts +19 -1
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
type Tab,
|
|
9
9
|
TabBar,
|
|
10
10
|
type TUI,
|
|
11
|
-
truncateToWidth,
|
|
12
11
|
wrapTextWithAnsi,
|
|
13
12
|
} from "@f5-sales-demo/pi-tui";
|
|
14
13
|
import { filterCurrentBrowserModels } from "../../config/model-catalog";
|
|
@@ -38,6 +37,7 @@ import {
|
|
|
38
37
|
selectorFrameContentWidth,
|
|
39
38
|
selectorKeys,
|
|
40
39
|
selectorNavigationHint,
|
|
40
|
+
selectorProse,
|
|
41
41
|
selectorRow,
|
|
42
42
|
} from "./selector-frame";
|
|
43
43
|
|
|
@@ -350,6 +350,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
350
350
|
#spinnerFrame = 0;
|
|
351
351
|
#spinnerTimer?: ReturnType<typeof setInterval>;
|
|
352
352
|
#pendingRefreshes = 0;
|
|
353
|
+
#detailOffset = 0;
|
|
354
|
+
#detailCapacity = 1;
|
|
355
|
+
#detailLength = 0;
|
|
356
|
+
#detailWidth = 0;
|
|
353
357
|
|
|
354
358
|
#disposed = false;
|
|
355
359
|
#initialProvider?: string;
|
|
@@ -383,7 +387,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
383
387
|
let selectedBodyIndex: number | undefined;
|
|
384
388
|
let stickyBodyRows = 0;
|
|
385
389
|
let minimumDetailRows = 0;
|
|
386
|
-
const details: string[] =
|
|
390
|
+
const details: string[] = [];
|
|
387
391
|
let navigation: string[] = [];
|
|
388
392
|
let title = "Choose a model";
|
|
389
393
|
let purpose = "";
|
|
@@ -392,6 +396,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
392
396
|
`Ctrl+R: refresh${this.#onLogin ? " · Ctrl+L: providers" : ""} · ${selectorCancelHint(searching ? "clear search" : "back")}`,
|
|
393
397
|
];
|
|
394
398
|
if (this.#isMenuOpen && selected) {
|
|
399
|
+
details.push(...wrapTextWithAnsi(selected.selector, inner));
|
|
395
400
|
title = getModelDisplayName(selected.model);
|
|
396
401
|
const labels =
|
|
397
402
|
this.#menuStep === "scope"
|
|
@@ -429,25 +434,42 @@ export class ModelSelectorComponent extends Container {
|
|
|
429
434
|
...this.#searchInput.render(inner),
|
|
430
435
|
];
|
|
431
436
|
if (this.#scopedModels.length) purpose = "Showing models from --models scope";
|
|
432
|
-
const status = this.#renderProviderStatus(inner)
|
|
433
|
-
.filter(line => line.trim())
|
|
434
|
-
.slice(0, 1);
|
|
435
437
|
if (selected) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
438
|
+
const providerRowLabel = getProviderDisplayName(selected.model.provider);
|
|
439
|
+
const providerLabel =
|
|
440
|
+
this.#modelRegistry.getProviderPickerMetadata?.(selected.model.provider)?.groupLabel ?? providerRowLabel;
|
|
441
|
+
const detailValues = [
|
|
442
|
+
`Model: ${getModelDisplayName(selected.model)}`,
|
|
443
|
+
`Provider: ${providerLabel}`,
|
|
444
|
+
...(providerRowLabel !== providerLabel ? [`Provider row label: ${providerRowLabel}`] : []),
|
|
445
|
+
`Selector: ${selected.selector}`,
|
|
446
|
+
`Assignments: ${assignments(selected.model, true)}`,
|
|
447
|
+
...(this.#isItemDisabled(selected) && !this.#refreshingProvider
|
|
448
|
+
? ["Status: Unavailable · reconnect or refresh this provider."]
|
|
449
|
+
: []),
|
|
450
|
+
...(selected.model.description ? [`Description: ${selected.model.description}`] : []),
|
|
451
|
+
...this.#renderProviderStatus().filter(line => line.trim()),
|
|
452
|
+
...(this.#errorMessage ? [`Error: ${String(this.#errorMessage)}`] : []),
|
|
453
|
+
];
|
|
454
|
+
const wrappedDetails = detailValues.flatMap(value => wrapTextWithAnsi(value, inner));
|
|
455
|
+
if (this.#detailWidth !== inner) this.#detailOffset = 0;
|
|
456
|
+
this.#detailWidth = inner;
|
|
457
|
+
this.#detailCapacity = Math.max(1, height - 15);
|
|
458
|
+
this.#detailLength = wrappedDetails.length;
|
|
459
|
+
const lastPageOffset =
|
|
460
|
+
Math.max(0, Math.ceil(this.#detailLength / this.#detailCapacity) - 1) * this.#detailCapacity;
|
|
461
|
+
this.#detailOffset = Math.min(this.#detailOffset, lastPageOffset);
|
|
462
|
+
details.push(...wrappedDetails.slice(this.#detailOffset, this.#detailOffset + this.#detailCapacity));
|
|
463
|
+
}
|
|
464
|
+
// Keep the selected compact row stable while status and detail pages change.
|
|
465
|
+
minimumDetailRows = this.#detailLength > this.#detailCapacity ? this.#detailCapacity : details.length + 2;
|
|
466
|
+
if (this.#detailLength > this.#detailCapacity) {
|
|
467
|
+
const first = this.#detailOffset + 1;
|
|
468
|
+
const last = Math.min(this.#detailLength, this.#detailOffset + this.#detailCapacity);
|
|
469
|
+
footer.push(
|
|
470
|
+
`${selectorKeys("pageUp")}/${selectorKeys("pageDown")}: details ${first}–${last} of ${this.#detailLength}`,
|
|
471
|
+
);
|
|
440
472
|
}
|
|
441
|
-
// Two live-status rows are intentional reserved space; spinner/error updates must not shift the table.
|
|
442
|
-
minimumDetailRows = details.length + 2;
|
|
443
|
-
const selectedStatus = selected
|
|
444
|
-
? this.#renderProviderStatus(inner).find(line =>
|
|
445
|
-
line.includes(`${getProviderDisplayName(selected.model.provider)}:`),
|
|
446
|
-
)
|
|
447
|
-
: undefined;
|
|
448
|
-
if (status[0]) details.push(status[0]);
|
|
449
|
-
if (selectedStatus && selectedStatus !== status[0]) details.push(selectedStatus);
|
|
450
|
-
if (this.#errorMessage) details.push(String(this.#errorMessage));
|
|
451
473
|
const showProvider = searching;
|
|
452
474
|
const widths = showProvider ? [inner - 42, 18, 18] : [inner - 24, 20];
|
|
453
475
|
if (wide) {
|
|
@@ -477,11 +499,13 @@ export class ModelSelectorComponent extends Container {
|
|
|
477
499
|
),
|
|
478
500
|
);
|
|
479
501
|
}
|
|
480
|
-
if (!this.#filteredModels.length)
|
|
502
|
+
if (!this.#filteredModels.length)
|
|
503
|
+
body.push(selectorProse(this.#getProviderEmptyStateMessage() ?? "No matching models"));
|
|
481
504
|
}
|
|
482
505
|
return selectorFrame(width, height, title, purpose, navigation, body, details, footer, {
|
|
483
506
|
minimumDetailRows,
|
|
484
507
|
selectedBodyIndex,
|
|
508
|
+
selectedDetail: this.#isMenuOpen ? "automatic" : "provided",
|
|
485
509
|
stickyBodyRows,
|
|
486
510
|
});
|
|
487
511
|
}
|
|
@@ -496,7 +520,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
496
520
|
this.#stopSpinner();
|
|
497
521
|
}
|
|
498
522
|
|
|
499
|
-
#renderProviderStatus(
|
|
523
|
+
#renderProviderStatus(): string[] {
|
|
500
524
|
if (this.#isMenuOpen) return [];
|
|
501
525
|
const group = this.#providerGroups[this.#activeTabIndex];
|
|
502
526
|
// Reserve the same status area for every provider. Updating status never moves the catalog.
|
|
@@ -544,7 +568,11 @@ export class ModelSelectorComponent extends Container {
|
|
|
544
568
|
message = `${label}: empty catalog · Ctrl+R: refresh`;
|
|
545
569
|
rows[index + 1] = theme.fg(color, message);
|
|
546
570
|
}
|
|
547
|
-
return rows
|
|
571
|
+
return rows;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
#resetDetailPage(): void {
|
|
575
|
+
this.#detailOffset = 0;
|
|
548
576
|
}
|
|
549
577
|
|
|
550
578
|
#menuRoleActions: MenuRoleAction[] = [];
|
|
@@ -1094,6 +1122,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1094
1122
|
const retainedIndex = this.#filteredModels.findIndex(item => item.selector === selectedSelector);
|
|
1095
1123
|
this.#selectedIndex =
|
|
1096
1124
|
retainedIndex >= 0 ? retainedIndex : Math.min(this.#selectedIndex, Math.max(0, visibleCount - 1));
|
|
1125
|
+
this.#resetDetailPage();
|
|
1097
1126
|
this.#updateList();
|
|
1098
1127
|
}
|
|
1099
1128
|
|
|
@@ -1179,6 +1208,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1179
1208
|
}
|
|
1180
1209
|
|
|
1181
1210
|
#openMenu(): void {
|
|
1211
|
+
this.#resetDetailPage();
|
|
1182
1212
|
this.#menuItem = this.#getSelectedItem();
|
|
1183
1213
|
if (!this.#menuItem || this.#isItemDisabled(this.#menuItem)) return;
|
|
1184
1214
|
this.#isMenuOpen = true;
|
|
@@ -1201,6 +1231,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1201
1231
|
}
|
|
1202
1232
|
|
|
1203
1233
|
#closeMenu(): void {
|
|
1234
|
+
this.#resetDetailPage();
|
|
1204
1235
|
this.#isMenuOpen = false;
|
|
1205
1236
|
this.#menuStep = "role";
|
|
1206
1237
|
this.#menuSelectedRole = null;
|
|
@@ -1231,6 +1262,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1231
1262
|
}
|
|
1232
1263
|
|
|
1233
1264
|
if (matchesKey(keyData, "ctrl+r")) {
|
|
1265
|
+
this.#resetDetailPage();
|
|
1234
1266
|
void this.#refreshSelectedProvider().catch(error => {
|
|
1235
1267
|
this.#errorMessage = error instanceof Error ? error.message : String(error);
|
|
1236
1268
|
this.#updateList();
|
|
@@ -1239,11 +1271,26 @@ export class ModelSelectorComponent extends Container {
|
|
|
1239
1271
|
return;
|
|
1240
1272
|
}
|
|
1241
1273
|
|
|
1274
|
+
if (matchesSelectorKey(keyData, "pageDown")) {
|
|
1275
|
+
const lastPageOffset =
|
|
1276
|
+
Math.max(0, Math.ceil(this.#detailLength / this.#detailCapacity) - 1) * this.#detailCapacity;
|
|
1277
|
+
this.#detailOffset = Math.min(lastPageOffset, this.#detailOffset + this.#detailCapacity);
|
|
1278
|
+
this.#updateList();
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
if (matchesSelectorKey(keyData, "pageUp")) {
|
|
1283
|
+
this.#detailOffset = Math.max(0, this.#detailOffset - this.#detailCapacity);
|
|
1284
|
+
this.#updateList();
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1242
1288
|
// Up arrow - navigate list (wrap to bottom when at top)
|
|
1243
1289
|
if (matchesSelectorKey(keyData, "up")) {
|
|
1244
1290
|
const itemCount = this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
|
|
1245
1291
|
if (itemCount === 0) return;
|
|
1246
1292
|
this.#selectedIndex = this.#selectedIndex === 0 ? itemCount - 1 : this.#selectedIndex - 1;
|
|
1293
|
+
this.#resetDetailPage();
|
|
1247
1294
|
this.#updateList();
|
|
1248
1295
|
return;
|
|
1249
1296
|
}
|
|
@@ -1253,6 +1300,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1253
1300
|
const itemCount = this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
|
|
1254
1301
|
if (itemCount === 0) return;
|
|
1255
1302
|
this.#selectedIndex = this.#selectedIndex === itemCount - 1 ? 0 : this.#selectedIndex + 1;
|
|
1303
|
+
this.#resetDetailPage();
|
|
1256
1304
|
this.#updateList();
|
|
1257
1305
|
return;
|
|
1258
1306
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
selectorFrameContentWidth,
|
|
14
14
|
selectorKeys,
|
|
15
15
|
selectorNavigationHint,
|
|
16
|
+
selectorProse,
|
|
16
17
|
selectorRow,
|
|
17
18
|
} from "./selector-frame";
|
|
18
19
|
/**
|
|
@@ -329,7 +330,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
329
330
|
const meta = providerPresentation(provider.id);
|
|
330
331
|
if (this.#catalogMode && !this.#searchInput.getValue() && meta.category !== category) {
|
|
331
332
|
category = meta.category;
|
|
332
|
-
body.push(theme.fg("muted", category));
|
|
333
|
+
body.push(selectorProse(theme.fg("muted", category)));
|
|
333
334
|
}
|
|
334
335
|
if (i === this.#selectedIndex) selectedBodyIndex = body.length;
|
|
335
336
|
body.push(
|
|
@@ -346,7 +347,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
346
347
|
),
|
|
347
348
|
);
|
|
348
349
|
}
|
|
349
|
-
if (!this.#filteredProviders.length) body.push("No matching providers");
|
|
350
|
+
if (!this.#filteredProviders.length) body.push(selectorProse("No matching providers"));
|
|
350
351
|
}
|
|
351
352
|
if (this.#statusMessage) details.push(this.#statusMessage);
|
|
352
353
|
return selectorFrame(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Container, Markdown } from "@f5-sales-demo/pi-tui";
|
|
2
2
|
import { getMarkdownTheme } from "../theme/theme";
|
|
3
|
-
import { selectorFrame, selectorFrameContentWidth } from "./selector-frame";
|
|
3
|
+
import { selectorFrame, selectorFrameContentWidth, selectorProse } from "./selector-frame";
|
|
4
4
|
|
|
5
5
|
/** Static transcript-adjacent plan preview; approval controls live in the separate review overlay. */
|
|
6
6
|
export class PlanPreviewComponent extends Container {
|
|
@@ -17,7 +17,7 @@ export class PlanPreviewComponent extends Container {
|
|
|
17
17
|
"Plan review",
|
|
18
18
|
"Prepared execution plan; use the active review overlay to approve, revise, or cancel.",
|
|
19
19
|
[],
|
|
20
|
-
body,
|
|
20
|
+
body.map(line => selectorProse(line)),
|
|
21
21
|
[],
|
|
22
22
|
[],
|
|
23
23
|
);
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
type PluginSettingsStorage,
|
|
17
17
|
pluginSettingsStorage,
|
|
18
18
|
} from "./plugin-settings-drafts";
|
|
19
|
-
import { matchesSelectorKey, selectorFrame } from "./selector-frame";
|
|
19
|
+
import { matchesSelectorKey, selectorFrame, selectorProse } from "./selector-frame";
|
|
20
20
|
import { SettingsBrowser } from "./settings-browser";
|
|
21
21
|
import { SettingsChoiceEditor, SettingsTextEditor } from "./settings-editors";
|
|
22
22
|
|
|
@@ -375,7 +375,7 @@ export class PluginSettingsComponent extends Container {
|
|
|
375
375
|
"Plugin settings",
|
|
376
376
|
"User defaults",
|
|
377
377
|
[],
|
|
378
|
-
[this.#loadError || "Loading plugin settings…"],
|
|
378
|
+
[selectorProse(this.#loadError || "Loading plugin settings…")],
|
|
379
379
|
[],
|
|
380
380
|
this.#loadError ? ["Ctrl+R: retry · Esc: back"] : [],
|
|
381
381
|
)
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type SelectorFrameLine,
|
|
21
21
|
selectorFrame,
|
|
22
22
|
selectorFrameContentWidth,
|
|
23
|
+
selectorProse,
|
|
23
24
|
selectorRow,
|
|
24
25
|
} from "../selector-frame";
|
|
25
26
|
import { applySearch, buildTabs, filterByTab, loadAllPlugins, pluginSelectionKey } from "./state-manager";
|
|
@@ -818,7 +819,7 @@ export class PluginDashboard extends Container {
|
|
|
818
819
|
),
|
|
819
820
|
);
|
|
820
821
|
}
|
|
821
|
-
if (!body.length || (wide && body.length === 1)) body.push(this.#emptyMessage());
|
|
822
|
+
if (!body.length || (wide && body.length === 1)) body.push(selectorProse(this.#emptyMessage()));
|
|
822
823
|
return { body, selectedBodyIndex };
|
|
823
824
|
}
|
|
824
825
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Component } from "@f5-sales-demo/pi-tui";
|
|
2
2
|
import type { AsyncInputQuestion, AsyncQuestionItem } from "../../../../chat-ui/src/interactions/contract";
|
|
3
3
|
import { theme } from "../theme/theme";
|
|
4
|
-
import { selectorFrame, selectorFrameContentWidth, selectorRow } from "./selector-frame";
|
|
4
|
+
import { selectorFrame, selectorFrameContentWidth, selectorProse, selectorRow } from "./selector-frame";
|
|
5
5
|
|
|
6
6
|
export interface AsyncInputReply {
|
|
7
7
|
type: "user_input_reply";
|
|
@@ -123,7 +123,7 @@ export class QuestionTranscriptComponent implements Component {
|
|
|
123
123
|
"Answer recorded",
|
|
124
124
|
`Question ${position + 1} of ${total}`,
|
|
125
125
|
[question.title],
|
|
126
|
-
[
|
|
126
|
+
[selectorProse(`${theme.symbol("checkbox.checked")} ${answer}`)],
|
|
127
127
|
[],
|
|
128
128
|
[],
|
|
129
129
|
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Component, padding, wrapTextWithAnsi } from "@f5-sales-demo/pi-tui";
|
|
2
|
+
import type { RecapRecord } from "../../session/recap";
|
|
3
|
+
import { theme } from "../theme/theme";
|
|
4
|
+
|
|
5
|
+
/** A compact, secondary transcript line whose wrapped text starts below its first word. */
|
|
6
|
+
export class RecapMessageComponent implements Component {
|
|
7
|
+
constructor(readonly recap: RecapRecord) {}
|
|
8
|
+
invalidate(): void {}
|
|
9
|
+
|
|
10
|
+
render(width: number): string[] {
|
|
11
|
+
const lines: string[] = [];
|
|
12
|
+
const add = (prefix: string, content: string) => {
|
|
13
|
+
const available = Math.max(1, width - prefix.length);
|
|
14
|
+
for (const [index, segment] of wrapTextWithAnsi(content, available).entries()) {
|
|
15
|
+
const lead = index === 0 ? prefix : padding(prefix.length);
|
|
16
|
+
lines.push(theme.italic(theme.fg("dim", lead + segment)));
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
add(" ↳ Recap: ", this.recap.summary);
|
|
20
|
+
if (this.recap.nextAction) add(" Next: ", this.recap.nextAction);
|
|
21
|
+
return lines;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
selectorFrame,
|
|
11
11
|
selectorFrameContentWidth,
|
|
12
12
|
selectorKeys,
|
|
13
|
+
selectorProse,
|
|
13
14
|
selectorRow,
|
|
14
15
|
} from "./selector-frame";
|
|
15
16
|
|
|
@@ -66,9 +67,9 @@ export class RequestUserInputComponent implements Component {
|
|
|
66
67
|
if (this.form.confirming) {
|
|
67
68
|
const body: SelectorFrameLine[] = [
|
|
68
69
|
selectorRow([`1. ${INPUT_COPY.proceed}`], [inner - 2], this.#confirmation === 0),
|
|
69
|
-
|
|
70
|
+
selectorProse(theme.fg("muted", ` Submit with unanswered questions.`)),
|
|
70
71
|
selectorRow([`2. ${INPUT_COPY.back}`], [inner - 2], this.#confirmation === 1),
|
|
71
|
-
|
|
72
|
+
selectorProse(theme.fg("muted", ` ${INPUT_COPY.backDescription}`)),
|
|
72
73
|
];
|
|
73
74
|
return selectorFrame(
|
|
74
75
|
width,
|
|
@@ -97,10 +98,9 @@ export class RequestUserInputComponent implements Component {
|
|
|
97
98
|
);
|
|
98
99
|
if (option.description.trim())
|
|
99
100
|
body.push(
|
|
100
|
-
...readOnlyTextLines(option.description, inner - 3).map(content =>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
})),
|
|
101
|
+
...readOnlyTextLines(option.description, inner - 3).map(content =>
|
|
102
|
+
selectorProse(theme.fg("muted", ` ${content}`)),
|
|
103
|
+
),
|
|
104
104
|
);
|
|
105
105
|
}
|
|
106
106
|
const details: string[] = [];
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
selectorFrameContentWidth,
|
|
12
12
|
selectorKeys,
|
|
13
13
|
selectorNavigationHint,
|
|
14
|
+
selectorProse,
|
|
14
15
|
selectorRow,
|
|
15
16
|
} from "./selector-frame";
|
|
16
17
|
|
|
@@ -67,9 +68,11 @@ export class ReviewedActionDialog<T> extends Container {
|
|
|
67
68
|
[],
|
|
68
69
|
this.#running
|
|
69
70
|
? [
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
selectorProse(
|
|
72
|
+
this.#abort?.signal.aborted
|
|
73
|
+
? "Interruption requested; waiting for acknowledgement…"
|
|
74
|
+
: "Operation in progress…",
|
|
75
|
+
),
|
|
73
76
|
]
|
|
74
77
|
: [
|
|
75
78
|
this.#executed ? "Close unresolved result" : "Cancel",
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
selectorFrameContentWidth,
|
|
14
14
|
selectorKeys,
|
|
15
15
|
selectorNavigationHint,
|
|
16
|
+
selectorProse,
|
|
16
17
|
selectorRow,
|
|
17
18
|
} from "./selector-frame";
|
|
18
19
|
|
|
@@ -35,7 +36,7 @@ export class SearchableSelectList extends SelectList {
|
|
|
35
36
|
private readonly maxVisibleRows: number,
|
|
36
37
|
theme: SelectListTheme,
|
|
37
38
|
) {
|
|
38
|
-
super(sourceItems, maxVisibleRows, theme);
|
|
39
|
+
super(sourceItems, maxVisibleRows, theme, { presentation: "compact-with-selected-detail" });
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
override setFilter(filter: string): void {
|
|
@@ -75,7 +76,7 @@ export class SearchableSelectList extends SelectList {
|
|
|
75
76
|
const item = this.sourceItems[index]!;
|
|
76
77
|
return selectorRow([item.label], [inner - 2], index === this.#selectedIndex);
|
|
77
78
|
})
|
|
78
|
-
: [this.sourceItems.length ? "No matching options." : "No options available."];
|
|
79
|
+
: [selectorProse(this.sourceItems.length ? "No matching options." : "No options available.")];
|
|
79
80
|
const lines = selectorFrame(
|
|
80
81
|
width,
|
|
81
82
|
rows,
|
|
@@ -10,11 +10,26 @@ import {
|
|
|
10
10
|
import { theme } from "../theme/theme";
|
|
11
11
|
|
|
12
12
|
export interface SelectorFrameRow {
|
|
13
|
+
kind: "compact-row";
|
|
13
14
|
content: string;
|
|
14
15
|
selected: boolean;
|
|
16
|
+
fullDetail: string;
|
|
17
|
+
truncated: boolean;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
export
|
|
20
|
+
export interface SelectorFrameProse {
|
|
21
|
+
kind: "wrapped-prose";
|
|
22
|
+
content: string;
|
|
23
|
+
tone: "text" | "muted";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SelectorFrameLine = SelectorFrameRow | SelectorFrameProse;
|
|
27
|
+
|
|
28
|
+
interface RenderedBodyLine {
|
|
29
|
+
content: string;
|
|
30
|
+
selected: boolean;
|
|
31
|
+
sourceIndex: number;
|
|
32
|
+
}
|
|
18
33
|
|
|
19
34
|
export interface SelectorFrameOptions {
|
|
20
35
|
/** Index of the selected row in `body`; used to retain it when the frame is height-constrained. */
|
|
@@ -27,6 +42,8 @@ export interface SelectorFrameOptions {
|
|
|
27
42
|
minimumDetailRows?: number;
|
|
28
43
|
/** Contextual paging controls, included only when the body actually overflows. */
|
|
29
44
|
overflowHint?: string;
|
|
45
|
+
/** The caller supplies and pages the selected compact row's complete detail text. */
|
|
46
|
+
selectedDetail?: "automatic" | "provided";
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
function selectorFrameColumns(width: number): number {
|
|
@@ -59,11 +76,11 @@ function wrapSection(values: string[], width: number): string[] {
|
|
|
59
76
|
}
|
|
60
77
|
|
|
61
78
|
function windowBody(
|
|
62
|
-
body:
|
|
79
|
+
body: RenderedBodyLine[],
|
|
63
80
|
visibleRows: number,
|
|
64
81
|
selectedIndex: number | undefined,
|
|
65
82
|
stickyRows: number,
|
|
66
|
-
):
|
|
83
|
+
): RenderedBodyLine[] {
|
|
67
84
|
if (visibleRows <= 0) return [];
|
|
68
85
|
if (body.length <= visibleRows) return body;
|
|
69
86
|
const stickyCount = Math.min(stickyRows, visibleRows - 1, body.length);
|
|
@@ -95,13 +112,12 @@ export function selectorFrame(
|
|
|
95
112
|
const top = border(`${box.topLeft}${box.horizontal.repeat(columns - 2)}${box.topRight}`);
|
|
96
113
|
const divider = border(`${sharp.teeRight}${box.horizontal.repeat(columns - 2)}${sharp.teeLeft}`);
|
|
97
114
|
const bottom = border(`${box.bottomLeft}${box.horizontal.repeat(columns - 2)}${box.bottomRight}`);
|
|
98
|
-
const line = (value:
|
|
99
|
-
const
|
|
100
|
-
const fitted = truncateToWidth(normalizeLine(typeof value === "string" ? value : value.content), contentWidth);
|
|
115
|
+
const line = (value: string, selected = false) => {
|
|
116
|
+
const fitted = truncateToWidth(normalizeLine(value), contentWidth);
|
|
101
117
|
const content = `${" ".repeat(padding)}${fitted}${" ".repeat(
|
|
102
118
|
Math.max(0, contentWidth - visibleWidth(fitted)),
|
|
103
119
|
)}${" ".repeat(padding)}`;
|
|
104
|
-
return
|
|
120
|
+
return selected
|
|
105
121
|
? `${edge}${theme.bg("selectedBg", theme.fg("text", content))}${edge}`
|
|
106
122
|
: `${edge}${content}${edge}`;
|
|
107
123
|
};
|
|
@@ -111,10 +127,43 @@ export function selectorFrame(
|
|
|
111
127
|
.slice(0, 2)
|
|
112
128
|
.map(value => theme.fg("muted", value));
|
|
113
129
|
const navigationLines = navigation.filter(hasVisibleContent).map(normalizeLine);
|
|
114
|
-
const
|
|
130
|
+
const selectedSourceIndex =
|
|
131
|
+
options.selectedBodyIndex ?? body.findIndex(value => value.kind === "compact-row" && value.selected);
|
|
132
|
+
const selectedRow = selectedSourceIndex >= 0 ? body[selectedSourceIndex] : undefined;
|
|
133
|
+
if (
|
|
134
|
+
options.selectedDetail === "provided" &&
|
|
135
|
+
selectedRow?.kind === "compact-row" &&
|
|
136
|
+
(selectedRow.truncated || visibleWidth(normalizeLine(selectedRow.content)) > contentWidth) &&
|
|
137
|
+
!details.some(hasVisibleContent)
|
|
138
|
+
) {
|
|
139
|
+
throw new TypeError("A truncating compact row requires complete selected detail text.");
|
|
140
|
+
}
|
|
141
|
+
const selectedDetail =
|
|
142
|
+
options.selectedDetail !== "provided" &&
|
|
143
|
+
selectedRow?.kind === "compact-row" &&
|
|
144
|
+
(selectedRow.truncated || visibleWidth(normalizeLine(selectedRow.content)) > contentWidth)
|
|
145
|
+
? [selectedRow.fullDetail]
|
|
146
|
+
: [];
|
|
147
|
+
const detailLines = wrapSection([...selectedDetail, ...details], contentWidth);
|
|
115
148
|
while (detailLines.length < (options.minimumDetailRows ?? 0)) detailLines.push("");
|
|
116
149
|
const footerLines = wrapSection(footer, contentWidth).map(value => theme.fg("muted", value));
|
|
117
|
-
const normalizedBody = body.
|
|
150
|
+
const normalizedBody = body.flatMap<RenderedBodyLine>((value, sourceIndex) => {
|
|
151
|
+
if (!value || typeof value !== "object" || !("kind" in value)) {
|
|
152
|
+
throw new TypeError("Frame body text must use selectorProse() or selectorRow().");
|
|
153
|
+
}
|
|
154
|
+
if (value.kind === "wrapped-prose") {
|
|
155
|
+
return wrapSection([value.content], contentWidth).map(content => ({
|
|
156
|
+
content: theme.fg(value.tone, content),
|
|
157
|
+
selected: false,
|
|
158
|
+
sourceIndex,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
if (value.kind !== "compact-row") {
|
|
162
|
+
throw new TypeError("Frame body text must use selectorProse() or selectorRow().");
|
|
163
|
+
}
|
|
164
|
+
return [{ content: value.content, selected: value.selected, sourceIndex }];
|
|
165
|
+
});
|
|
166
|
+
const selectedRenderedIndex = normalizedBody.findIndex(value => value.sourceIndex === selectedSourceIndex);
|
|
118
167
|
|
|
119
168
|
const gaps = {
|
|
120
169
|
headingToNavigation: navigationLines.length > 0,
|
|
@@ -143,7 +192,7 @@ export function selectorFrame(
|
|
|
143
192
|
let visibleBody = windowBody(
|
|
144
193
|
normalizedBody,
|
|
145
194
|
bodyRows,
|
|
146
|
-
|
|
195
|
+
selectedRenderedIndex >= 0 ? selectedRenderedIndex : undefined,
|
|
147
196
|
Math.max(0, options.stickyBodyRows ?? 0),
|
|
148
197
|
);
|
|
149
198
|
|
|
@@ -156,7 +205,7 @@ export function selectorFrame(
|
|
|
156
205
|
visibleBody = windowBody(
|
|
157
206
|
normalizedBody,
|
|
158
207
|
bodyRows,
|
|
159
|
-
|
|
208
|
+
selectedRenderedIndex >= 0 ? selectedRenderedIndex : undefined,
|
|
160
209
|
Math.max(0, options.stickyBodyRows ?? 0),
|
|
161
210
|
);
|
|
162
211
|
}
|
|
@@ -176,35 +225,53 @@ export function selectorFrame(
|
|
|
176
225
|
|
|
177
226
|
return [
|
|
178
227
|
top,
|
|
179
|
-
...heading.map(line),
|
|
180
|
-
...purposeLines.map(line),
|
|
228
|
+
...heading.map(value => line(value)),
|
|
229
|
+
...purposeLines.map(value => line(value)),
|
|
181
230
|
...(gaps.headingToNavigation ? [line("")] : []),
|
|
182
|
-
...navigationLines.map(line),
|
|
231
|
+
...navigationLines.map(value => line(value)),
|
|
183
232
|
divider,
|
|
184
|
-
...visibleBody.map(line),
|
|
233
|
+
...visibleBody.map(value => line(value.content, value.selected)),
|
|
185
234
|
...(gaps.bodyToDetails ? [line("")] : []),
|
|
186
|
-
...detailLines.map(line),
|
|
235
|
+
...detailLines.map(value => line(value)),
|
|
187
236
|
...(gaps.detailsToFooter ? [line("")] : []),
|
|
188
|
-
...footerLines.map(line),
|
|
237
|
+
...footerLines.map(value => line(value)),
|
|
189
238
|
bottom,
|
|
190
239
|
];
|
|
191
240
|
}
|
|
192
241
|
|
|
242
|
+
export function selectorProse(content: string, tone: "text" | "muted" = "text"): SelectorFrameProse {
|
|
243
|
+
return { kind: "wrapped-prose", content, tone };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function selectorCompactRow(
|
|
247
|
+
content: string,
|
|
248
|
+
selected = false,
|
|
249
|
+
fullDetail = content,
|
|
250
|
+
truncated = false,
|
|
251
|
+
): SelectorFrameRow {
|
|
252
|
+
if (truncated && !hasVisibleContent(fullDetail)) {
|
|
253
|
+
throw new TypeError("A truncating compact row requires complete selected detail text.");
|
|
254
|
+
}
|
|
255
|
+
return { kind: "compact-row", content, selected, fullDetail, truncated };
|
|
256
|
+
}
|
|
257
|
+
|
|
193
258
|
export function selectorRow(
|
|
194
259
|
cells: string[],
|
|
195
260
|
widths: number[],
|
|
196
261
|
selected = false,
|
|
197
262
|
tone: "text" | "muted" = "text",
|
|
198
263
|
): SelectorFrameRow {
|
|
264
|
+
let truncated = false;
|
|
199
265
|
const row = cells
|
|
200
266
|
.map((cell, index) => {
|
|
201
267
|
const width = Math.max(0, widths[index] ?? 0);
|
|
268
|
+
truncated ||= visibleWidth(cell) > width;
|
|
202
269
|
const fitted = truncateToWidth(cell, width);
|
|
203
270
|
return fitted + " ".repeat(Math.max(0, width - visibleWidth(fitted)));
|
|
204
271
|
})
|
|
205
272
|
.join(" ");
|
|
206
273
|
const text = `${selected ? theme.nav.cursor : " "} ${row}`;
|
|
207
|
-
return
|
|
274
|
+
return selectorCompactRow(theme.fg(tone, text), selected, cells.join(" "), truncated);
|
|
208
275
|
}
|
|
209
276
|
|
|
210
277
|
type SelectorAction = "up" | "down" | "confirm" | "cancel" | "pageUp" | "pageDown";
|
|
@@ -298,7 +365,7 @@ export class ReportDetailsComponent extends Container {
|
|
|
298
365
|
this.title,
|
|
299
366
|
this.purpose,
|
|
300
367
|
[],
|
|
301
|
-
lines.slice(this.#offset, this.#offset + this.#capacity).map(content => (
|
|
368
|
+
lines.slice(this.#offset, this.#offset + this.#capacity).map(content => selectorCompactRow(content)),
|
|
302
369
|
[],
|
|
303
370
|
[
|
|
304
371
|
...(overflow
|
|
@@ -388,8 +455,8 @@ export class ConnectionInputComponent extends Container {
|
|
|
388
455
|
this.title,
|
|
389
456
|
"",
|
|
390
457
|
[],
|
|
391
|
-
this.content.render(inner),
|
|
392
|
-
[
|
|
458
|
+
this.content.render(inner).map(content => selectorProse(content)),
|
|
459
|
+
[this.purpose, ...this.input.render(inner)],
|
|
393
460
|
[selectorNavigationHint("submit"), selectorCancelHint("cancel")],
|
|
394
461
|
);
|
|
395
462
|
}
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
selectorFrame,
|
|
30
30
|
selectorFrameContentWidth,
|
|
31
31
|
selectorKeys,
|
|
32
|
+
selectorProse,
|
|
32
33
|
} from "./selector-frame";
|
|
33
34
|
|
|
34
35
|
type Mode = "picker" | "viewer";
|
|
@@ -120,7 +121,7 @@ export class SessionObserverOverlayComponent extends Container {
|
|
|
120
121
|
"Session observer",
|
|
121
122
|
session ? `Live read-only view · ${session.label} · identity ${session.id}` : "Live session is unavailable.",
|
|
122
123
|
[],
|
|
123
|
-
content.slice(this.#viewerOffset, this.#viewerOffset + this.#viewerCapacity),
|
|
124
|
+
content.slice(this.#viewerOffset, this.#viewerOffset + this.#viewerCapacity).map(line => selectorProse(line)),
|
|
124
125
|
[],
|
|
125
126
|
[
|
|
126
127
|
...(this.#viewerLength > this.#viewerCapacity
|