@oh-my-pi/pi-tui 18.3.1 → 18.3.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.
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
*
|
|
20
20
|
* Keys: Up/Down step through rendered items in transcript order (within the
|
|
21
21
|
* active column when a strip is open), Left/Right slide between branch
|
|
22
|
-
* variants at a fork and jump between user turns elsewhere,
|
|
23
|
-
* the
|
|
22
|
+
* variants at a fork and jump between user turns elsewhere, `f` opens a
|
|
23
|
+
* filter (typing narrows the current path to matching items; Esc leaves the
|
|
24
|
+
* filter with the selection kept), Enter rewinds to the outlined item, Esc
|
|
25
|
+
* cancels.
|
|
24
26
|
*/
|
|
25
27
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
26
28
|
import { type Component, type TUI } from "../index.js";
|
|
@@ -63,7 +63,7 @@ export interface StatusLineSession {
|
|
|
63
63
|
} | undefined;
|
|
64
64
|
autoResolvedThinkingLevel(): string | undefined;
|
|
65
65
|
isFastModeActive(): boolean;
|
|
66
|
-
/** Anthropic
|
|
66
|
+
/** Anthropic usage-limit label (`limit reached · wrapping up · resets 14:30`, `low priority until 14:30 · 62% left`). */
|
|
67
67
|
getAnthropicSlowModeLabel?(): string | undefined;
|
|
68
68
|
getPrewalkState?(): unknown;
|
|
69
69
|
getAsyncJobSnapshot(): {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.3.
|
|
4
|
+
"version": "18.3.2",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -40,14 +40,14 @@
|
|
|
40
40
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@oh-my-pi/omptype": "18.3.
|
|
44
|
-
"@oh-my-pi/pi-agent-core": "18.3.
|
|
45
|
-
"@oh-my-pi/pi-ai": "18.3.
|
|
46
|
-
"@oh-my-pi/pi-catalog": "18.3.
|
|
47
|
-
"@oh-my-pi/pi-natives": "18.3.
|
|
48
|
-
"@oh-my-pi/pi-utils": "18.3.
|
|
49
|
-
"@oh-my-pi/pi-wire": "18.3.
|
|
50
|
-
"@oh-my-pi/snapcompact": "18.3.
|
|
43
|
+
"@oh-my-pi/omptype": "18.3.2",
|
|
44
|
+
"@oh-my-pi/pi-agent-core": "18.3.2",
|
|
45
|
+
"@oh-my-pi/pi-ai": "18.3.2",
|
|
46
|
+
"@oh-my-pi/pi-catalog": "18.3.2",
|
|
47
|
+
"@oh-my-pi/pi-natives": "18.3.2",
|
|
48
|
+
"@oh-my-pi/pi-utils": "18.3.2",
|
|
49
|
+
"@oh-my-pi/pi-wire": "18.3.2",
|
|
50
|
+
"@oh-my-pi/snapcompact": "18.3.2"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"kitty-vt-wasm": "^0.2.0"
|
|
@@ -1630,6 +1630,13 @@ export class ModelHubComponent implements Component {
|
|
|
1630
1630
|
return;
|
|
1631
1631
|
}
|
|
1632
1632
|
|
|
1633
|
+
// Enter on the sidebar is a pane switch, like →: it lands on the model
|
|
1634
|
+
// rows instead of acting on a row the user cannot see is selected.
|
|
1635
|
+
if (this.#focus === "scope" && (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n")) {
|
|
1636
|
+
this.#focus = "list";
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1633
1640
|
const beforeQuery = this.#browser.query;
|
|
1634
1641
|
const isPrintable = extractPrintableText(data) !== undefined;
|
|
1635
1642
|
this.#browser.handleInput(data);
|
|
@@ -2274,19 +2281,22 @@ export class ModelHubComponent implements Component {
|
|
|
2274
2281
|
return "←/→ thinking level · Enter apply · Esc keep";
|
|
2275
2282
|
}
|
|
2276
2283
|
if (this.#assigning !== null) {
|
|
2284
|
+
if (this.#focus === "scope") {
|
|
2285
|
+
return "Enter/→ models · ↑/↓ providers · type to search · Alt+←/→ kind · Esc cancel";
|
|
2286
|
+
}
|
|
2277
2287
|
switch (this.#assigning.kind) {
|
|
2278
2288
|
case "fallback":
|
|
2279
|
-
return "Enter pick fallback · ↑/↓ providers · type to search · Alt+←/→ kind · Esc cancel";
|
|
2289
|
+
return "Enter pick fallback · ↑/↓ models · ← providers · type to search · Alt+←/→ kind · Esc cancel";
|
|
2280
2290
|
case "fallbackKey":
|
|
2281
|
-
return "Enter pick the protected model · ↑/↓ providers · type to search · Alt+←/→ kind · Esc cancel";
|
|
2291
|
+
return "Enter pick the protected model · ↑/↓ models · ← providers · type to search · Alt+←/→ kind · Esc cancel";
|
|
2282
2292
|
default:
|
|
2283
|
-
return "Enter assign · ↑/↓ providers · type to search · Alt+←/→ kind · Esc cancel";
|
|
2293
|
+
return "Enter assign · ↑/↓ models · ← providers · type to search · Alt+←/→ kind · Esc cancel";
|
|
2284
2294
|
}
|
|
2285
2295
|
}
|
|
2286
2296
|
const entry = this.#activeEntry();
|
|
2287
2297
|
if (entry.kind === "roles") {
|
|
2288
2298
|
if (this.#focus !== "list") {
|
|
2289
|
-
return "↑/↓ providers ·
|
|
2299
|
+
return "↑/↓ providers · Enter/→ roles · Alt+←/→ tabs · Esc close";
|
|
2290
2300
|
}
|
|
2291
2301
|
const row = this.#rolesRows[this.#roleIndex];
|
|
2292
2302
|
if (row?.kind === "fallback") {
|
|
@@ -2308,9 +2318,11 @@ export class ModelHubComponent implements Component {
|
|
|
2308
2318
|
if (entry.kind === "provider" && entry.locked) {
|
|
2309
2319
|
return entry.oauth ? "Enter log in · ↑/↓ providers · Esc close" : "↑/↓ providers · Esc close";
|
|
2310
2320
|
}
|
|
2311
|
-
const arrows = this.#focus === "scope" ? "↑/↓ providers · → models" : "↑/↓ models · ← providers";
|
|
2312
2321
|
const refresh = entry.kind === "provider" ? " · F5 refresh" : "";
|
|
2313
|
-
|
|
2322
|
+
if (this.#focus === "scope") {
|
|
2323
|
+
return `Enter/→ models · ↑/↓ providers · type to search · Alt+←/→ kind${refresh} · Esc close`;
|
|
2324
|
+
}
|
|
2325
|
+
return `Enter assign roles · ↑/↓ models · ← providers · type to search · Alt+←/→ kind${refresh} · Esc close`;
|
|
2314
2326
|
}
|
|
2315
2327
|
|
|
2316
2328
|
#renderFooter(width: number): string {
|
|
@@ -19,12 +19,15 @@
|
|
|
19
19
|
*
|
|
20
20
|
* Keys: Up/Down step through rendered items in transcript order (within the
|
|
21
21
|
* active column when a strip is open), Left/Right slide between branch
|
|
22
|
-
* variants at a fork and jump between user turns elsewhere,
|
|
23
|
-
* the
|
|
22
|
+
* variants at a fork and jump between user turns elsewhere, `f` opens a
|
|
23
|
+
* filter (typing narrows the current path to matching items; Esc leaves the
|
|
24
|
+
* filter with the selection kept), Enter rewinds to the outlined item, Esc
|
|
25
|
+
* cancels.
|
|
24
26
|
*/
|
|
25
27
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
26
28
|
import {
|
|
27
29
|
type Component,
|
|
30
|
+
extractPrintableText,
|
|
28
31
|
matchesKey,
|
|
29
32
|
padding,
|
|
30
33
|
routeSgrMouseInput,
|
|
@@ -110,6 +113,13 @@ export class RewindSelectorComponent implements Component {
|
|
|
110
113
|
#slide: { from: number; to: number; startedAt: number } | undefined;
|
|
111
114
|
#slideTimer: NodeJS.Timeout | undefined;
|
|
112
115
|
|
|
116
|
+
/** Filter query while the filter prompt is open; undefined shows the full transcript. */
|
|
117
|
+
#filter: string | undefined;
|
|
118
|
+
/** Main transcript rows from the last frame; the filter matches what is on screen. */
|
|
119
|
+
#mainRows: readonly (readonly string[])[] = [];
|
|
120
|
+
/** Lowercased plain text per rendered child row array (row arrays are cached, so identity is stable). */
|
|
121
|
+
#rowText = new WeakMap<readonly string[], string>();
|
|
122
|
+
|
|
113
123
|
constructor(
|
|
114
124
|
entries: TranscriptEntry[],
|
|
115
125
|
private readonly deps: RewindSelectorDeps,
|
|
@@ -235,19 +245,26 @@ export class RewindSelectorComponent implements Component {
|
|
|
235
245
|
});
|
|
236
246
|
return;
|
|
237
247
|
}
|
|
248
|
+
if (this.#filter !== undefined) {
|
|
249
|
+
this.#handleFilterInput(data);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
238
252
|
if (matchesSelectCancel(data) || matchesKey(data, "escape")) {
|
|
239
253
|
this.deps.onCancel();
|
|
240
254
|
return;
|
|
241
255
|
}
|
|
242
|
-
if (
|
|
243
|
-
this.#
|
|
244
|
-
this.#
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
256
|
+
if (matchesKey(data, "f")) {
|
|
257
|
+
this.#filter = "";
|
|
258
|
+
this.#activeVariant = 0;
|
|
259
|
+
this.#siblingSelected = 0;
|
|
260
|
+
this.#stopSlide();
|
|
248
261
|
this.deps.requestRender();
|
|
249
262
|
return;
|
|
250
263
|
}
|
|
264
|
+
if (matchesAppToolsExpand(data)) {
|
|
265
|
+
this.#toggleExpanded();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
251
268
|
if (matchesSelectUp(data)) {
|
|
252
269
|
this.#moveVertical(-1);
|
|
253
270
|
return;
|
|
@@ -282,6 +299,115 @@ export class RewindSelectorComponent implements Component {
|
|
|
282
299
|
}
|
|
283
300
|
}
|
|
284
301
|
|
|
302
|
+
#toggleExpanded(): void {
|
|
303
|
+
this.#expanded = !this.#expanded;
|
|
304
|
+
this.#builder.setExpanded(this.#expanded);
|
|
305
|
+
for (const columns of this.#variantCache.values()) {
|
|
306
|
+
for (const column of columns) column.builder.setExpanded(this.#expanded);
|
|
307
|
+
}
|
|
308
|
+
this.deps.requestRender();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ========================================================================
|
|
312
|
+
// Filter
|
|
313
|
+
// ========================================================================
|
|
314
|
+
|
|
315
|
+
#handleFilterInput(data: string): void {
|
|
316
|
+
if (matchesSelectCancel(data) || matchesKey(data, "escape")) {
|
|
317
|
+
this.#closeFilter();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
|
|
321
|
+
const target = this.#filterMatches().includes(this.#selected) ? this.#targets[this.#selected] : undefined;
|
|
322
|
+
if (target) this.deps.onSelect(target.entryId);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (matchesAppToolsExpand(data)) {
|
|
326
|
+
this.#toggleExpanded();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (matchesSelectUp(data) || matchesKey(data, "left")) {
|
|
330
|
+
const userTurnsOnly = !matchesSelectUp(data);
|
|
331
|
+
this.#stepFiltered(-1, userTurnsOnly);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (matchesSelectDown(data) || matchesKey(data, "right")) {
|
|
335
|
+
const userTurnsOnly = !matchesSelectDown(data);
|
|
336
|
+
this.#stepFiltered(1, userTurnsOnly);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (matchesKey(data, "backspace")) {
|
|
340
|
+
if (this.#filter!.length === 0) {
|
|
341
|
+
this.#closeFilter();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this.#setFilter(this.#filter!.slice(0, -1));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const printable = extractPrintableText(data);
|
|
348
|
+
if (printable) {
|
|
349
|
+
this.#setFilter(this.#filter! + printable);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (this.#browser.handleScrollKey(data)) {
|
|
353
|
+
this.deps.requestRender();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Leave the filter, keeping the selected item outlined in the full transcript. */
|
|
358
|
+
#closeFilter(): void {
|
|
359
|
+
this.#filter = undefined;
|
|
360
|
+
this.deps.requestRender();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Update the query; keep the selection when it still matches, else rest on the newest match above it. */
|
|
364
|
+
#setFilter(query: string): void {
|
|
365
|
+
this.#filter = query;
|
|
366
|
+
const matches = this.#filterMatches();
|
|
367
|
+
if (!matches.includes(this.#selected)) {
|
|
368
|
+
this.#selected = matches.findLast(index => index < this.#selected) ?? matches.at(-1) ?? this.#selected;
|
|
369
|
+
}
|
|
370
|
+
this.deps.requestRender();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#stepFiltered(delta: -1 | 1, userTurnsOnly: boolean): void {
|
|
374
|
+
const matches = this.#filterMatches().filter(index => !userTurnsOnly || this.#targets[index]!.isUserTurn);
|
|
375
|
+
const next =
|
|
376
|
+
delta < 0 ? matches.findLast(index => index < this.#selected) : matches.find(index => index > this.#selected);
|
|
377
|
+
if (next === undefined) return;
|
|
378
|
+
this.#selected = next;
|
|
379
|
+
this.deps.requestRender();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Visible main-path target indices whose rendered text contains every
|
|
384
|
+
* whitespace-separated query word as a whole word (case-insensitive: `ls`
|
|
385
|
+
* matches `ls -la` but not `tools`); all visible targets for an empty query.
|
|
386
|
+
* Matching the rendered rows keeps results honest: collapsed tool output
|
|
387
|
+
* only matches once Ctrl+O expands it.
|
|
388
|
+
*/
|
|
389
|
+
#filterMatches(): number[] {
|
|
390
|
+
const words = (this.#filter ?? "").toLowerCase().split(/\s+/).filter(Boolean);
|
|
391
|
+
const patterns = words.map(
|
|
392
|
+
word => new RegExp(`(?<![\\p{L}\\p{N}_])${RegExp.escape(word)}(?![\\p{L}\\p{N}_])`, "u"),
|
|
393
|
+
);
|
|
394
|
+
const matches: number[] = [];
|
|
395
|
+
for (let index = 0; index < this.#targets.length; index++) {
|
|
396
|
+
if (!this.#isMainSelectable(index)) continue;
|
|
397
|
+
const target = this.#targets[index]!;
|
|
398
|
+
const texts = this.#mainRows.slice(target.start, target.end).map(rows => {
|
|
399
|
+
let text = this.#rowText.get(rows);
|
|
400
|
+
if (text === undefined) {
|
|
401
|
+
text = Bun.stripANSI(rows.join("\n")).toLowerCase();
|
|
402
|
+
this.#rowText.set(rows, text);
|
|
403
|
+
}
|
|
404
|
+
return text;
|
|
405
|
+
});
|
|
406
|
+
if (patterns.every(pattern => texts.some(text => pattern.test(text)))) matches.push(index);
|
|
407
|
+
}
|
|
408
|
+
return matches;
|
|
409
|
+
}
|
|
410
|
+
|
|
285
411
|
/** Up/Down: step within the active column; leaving a sibling column's top exits the strip. */
|
|
286
412
|
#moveVertical(delta: -1 | 1): void {
|
|
287
413
|
if (this.#activeVariant > 0) {
|
|
@@ -337,6 +463,7 @@ export class RewindSelectorComponent implements Component {
|
|
|
337
463
|
const children = this.#builder.container.children;
|
|
338
464
|
const prepared = this.#browser.prepareOutline(children, this.#targets, this.#selected, contentWidth);
|
|
339
465
|
this.#mainVisible = prepared.visible;
|
|
466
|
+
this.#mainRows = prepared.childRows;
|
|
340
467
|
if (prepared.selected !== this.#selected) {
|
|
341
468
|
// The current target collapsed (e.g. expansion toggle): rest on the
|
|
342
469
|
// nearest visible one and leave the strip.
|
|
@@ -345,6 +472,8 @@ export class RewindSelectorComponent implements Component {
|
|
|
345
472
|
this.#siblingSelected = 0;
|
|
346
473
|
}
|
|
347
474
|
|
|
475
|
+
if (this.#filter !== undefined) return this.#filterFrame(this.#filter, prepared.childRows, contentWidth);
|
|
476
|
+
|
|
348
477
|
const columns = this.#stripColumns();
|
|
349
478
|
const composed =
|
|
350
479
|
columns.length > 0
|
|
@@ -359,14 +488,59 @@ export class RewindSelectorComponent implements Component {
|
|
|
359
488
|
const position = this.#targets.length > 0 ? `${this.#selected + 1}/${this.#targets.length} ` : "";
|
|
360
489
|
const lateral = columns.length > 0 ? "←/→ branches" : "←/→ user turns";
|
|
361
490
|
return {
|
|
362
|
-
header: [
|
|
363
|
-
`${theme.icon.rewind} ${theme.bold("Rewind")}${theme.sep.dot}${theme.fg("dim", "pick the point to continue from")}`,
|
|
364
|
-
],
|
|
491
|
+
header: [this.#header()],
|
|
365
492
|
body: {
|
|
366
493
|
lines: composed.lines,
|
|
367
494
|
anchor: this.#outlineAnchor(composed),
|
|
368
495
|
},
|
|
369
|
-
footer: [
|
|
496
|
+
footer: [
|
|
497
|
+
theme.fg("dim", `${position}↑/↓ step ${lateral} f filter enter rewind ctrl+o expand esc cancel`),
|
|
498
|
+
],
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
#header(): string {
|
|
503
|
+
return `${theme.icon.rewind} ${theme.bold("Rewind")}${theme.sep.dot}${theme.fg("dim", "pick the point to continue from")}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Only the matching items of the current path, concatenated; no branch strip. */
|
|
507
|
+
#filterFrame(
|
|
508
|
+
query: string,
|
|
509
|
+
childRows: readonly (readonly string[])[],
|
|
510
|
+
contentWidth: number,
|
|
511
|
+
): TranscriptBrowserFrame {
|
|
512
|
+
const matches = this.#filterMatches();
|
|
513
|
+
const rows: (readonly string[])[] = [];
|
|
514
|
+
const targets: OutlineTarget[] = [];
|
|
515
|
+
for (const index of matches) {
|
|
516
|
+
const target = this.#targets[index]!;
|
|
517
|
+
const start = rows.length;
|
|
518
|
+
rows.push(...childRows.slice(target.start, target.end));
|
|
519
|
+
targets.push({ ...target, start, end: rows.length });
|
|
520
|
+
}
|
|
521
|
+
const selected = matches.indexOf(this.#selected);
|
|
522
|
+
const composed = composeOutlineColumn(rows, 0, rows.length, targets, selected, contentWidth, undefined);
|
|
523
|
+
const lines = matches.length > 0 ? composed.lines : [theme.fg("muted", ` No items match "${query}"`)];
|
|
524
|
+
const count =
|
|
525
|
+
matches.length === 0
|
|
526
|
+
? theme.fg("error", "no matches")
|
|
527
|
+
: theme.fg("dim", `${selected >= 0 ? selected + 1 : "-"}/${matches.length}`);
|
|
528
|
+
return {
|
|
529
|
+
header: [this.#header()],
|
|
530
|
+
body: {
|
|
531
|
+
lines,
|
|
532
|
+
anchor:
|
|
533
|
+
composed.selStart >= 0
|
|
534
|
+
? {
|
|
535
|
+
id: `rewind:filter:${query}:${this.#targets[this.#selected]?.turnId ?? this.#selected}`,
|
|
536
|
+
start: composed.selStart,
|
|
537
|
+
end: composed.selEnd,
|
|
538
|
+
}
|
|
539
|
+
: undefined,
|
|
540
|
+
},
|
|
541
|
+
footer: [
|
|
542
|
+
`${theme.fg("accent", "filter:")} ${query}${theme.fg("accent", "▏")} ${count} ${theme.fg("dim", "↑/↓ step ←/→ user turns enter rewind esc show all")}`,
|
|
543
|
+
],
|
|
370
544
|
};
|
|
371
545
|
}
|
|
372
546
|
|
package/src/status-line/host.ts
CHANGED
|
@@ -53,7 +53,7 @@ export interface StatusLineSession {
|
|
|
53
53
|
getContextUsage(): { tokens: number; contextWindow: number; percent: number | null } | undefined;
|
|
54
54
|
autoResolvedThinkingLevel(): string | undefined;
|
|
55
55
|
isFastModeActive(): boolean;
|
|
56
|
-
/** Anthropic
|
|
56
|
+
/** Anthropic usage-limit label (`limit reached · wrapping up · resets 14:30`, `low priority until 14:30 · 62% left`). */
|
|
57
57
|
getAnthropicSlowModeLabel?(): string | undefined;
|
|
58
58
|
getPrewalkState?(): unknown;
|
|
59
59
|
getAsyncJobSnapshot(): { running: readonly { type: string; agentId?: string }[] } | null | undefined;
|
|
@@ -293,8 +293,8 @@ const modelSegment: StatusLineSegment = {
|
|
|
293
293
|
content += accentFg(ctx, "statusLineModel", tail);
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
// Anthropic
|
|
297
|
-
// mistaken for normal
|
|
296
|
+
// Anthropic usage-limit stage (wrap-up allowance or low priority): a
|
|
297
|
+
// warning-colored badge so past-the-limit service is never mistaken for normal.
|
|
298
298
|
const slowModeLabel = ctx.session.getAnthropicSlowModeLabel?.();
|
|
299
299
|
if (slowModeLabel) content += theme.fg("warning", `${theme.sep.dot}${slowModeLabel}`);
|
|
300
300
|
return { content, visible: true };
|