pi-tui-footer 1.2.5 → 1.2.7
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 +15 -0
- package/extensions/config.ts +6 -22
- package/extensions/editor.ts +0 -11
- package/extensions/footer.ts +16 -3
- package/extensions/index.ts +1 -6
- package/extensions/rounded-tools.ts +147 -44
- package/extensions/settings-command.ts +8 -103
- package/extensions/utils.ts +22 -0
- package/package.json +1 -1
- package/extensions/fullscreen-scroll.ts +0 -34
package/README.md
CHANGED
|
@@ -43,6 +43,21 @@ pi update --extensions
|
|
|
43
43
|
|
|
44
44
|
也可在 pi 内通过 `/pi-tui` 命令调整。
|
|
45
45
|
|
|
46
|
+
### 圆角工具框与第三方工具
|
|
47
|
+
|
|
48
|
+
`roundedTools` 开启时,除 8 个内置工具(read、write、edit、bash、powershell、grep、find、ls)外,也会自动拦截并为第三方扩展注册的工具(如 pi-fff 的 `ffgrep`、`fffind`、`fff-multi-grep`,或其 override 模式接管的 `grep`/`find`)添加圆角边框。
|
|
49
|
+
|
|
50
|
+
**加载顺序**:需要在 `~/.pi/agent/settings.json` 的 `packages` 中将 `pi-tui-footer` 排在第三方扩展(如 `@ff-labs/pi-fff`)**之前**,以确保 proxy 在工具注册前生效:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"packages": [
|
|
55
|
+
"npm:pi-tui-footer",
|
|
56
|
+
"npm:@ff-labs/pi-fff"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
46
61
|
## 致谢
|
|
47
62
|
|
|
48
63
|
本项目是以下两个开源项目的深度融合集成:
|
package/extensions/config.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
5
|
-
DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
6
|
-
normalizeFullscreenWheelScrollLines,
|
|
7
|
-
} from "./fullscreen-scroll.ts";
|
|
8
4
|
import type { IconMode } from "./icons.ts";
|
|
9
5
|
|
|
10
6
|
export type SettingsLanguage = "en" | "zh";
|
|
@@ -42,17 +38,12 @@ export interface ThinkingPeekConfig {
|
|
|
42
38
|
lines: ThinkingPeekLines;
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
export interface FullscreenConfig {
|
|
46
|
-
wheelScrollLines: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
41
|
export interface OpenTuiConfig {
|
|
50
42
|
enabled: boolean;
|
|
51
43
|
roundedTools: boolean;
|
|
52
44
|
inlineFooter: boolean;
|
|
53
45
|
settingsLanguage: SettingsLanguage;
|
|
54
46
|
cursorStyle: CursorStyle;
|
|
55
|
-
fullscreen: FullscreenConfig;
|
|
56
47
|
icons: {
|
|
57
48
|
mode: IconMode;
|
|
58
49
|
};
|
|
@@ -67,9 +58,6 @@ export const DEFAULT_CONFIG: OpenTuiConfig = {
|
|
|
67
58
|
inlineFooter: false,
|
|
68
59
|
settingsLanguage: "en",
|
|
69
60
|
cursorStyle: "block",
|
|
70
|
-
fullscreen: {
|
|
71
|
-
wheelScrollLines: DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
72
|
-
},
|
|
73
61
|
icons: {
|
|
74
62
|
mode: "auto",
|
|
75
63
|
},
|
|
@@ -155,27 +143,23 @@ export function loadConfig(notify?: (msg: string, level: "warning" | "info") =>
|
|
|
155
143
|
const raw = readFileSync(path, "utf8");
|
|
156
144
|
const parsed: unknown = JSON.parse(raw);
|
|
157
145
|
const config = deepMerge(DEFAULT_CONFIG, parsed);
|
|
146
|
+
if (typeof config.inlineFooter !== "boolean") {
|
|
147
|
+
config.inlineFooter = DEFAULT_CONFIG.inlineFooter;
|
|
148
|
+
}
|
|
149
|
+
if (typeof config.roundedTools !== "boolean") {
|
|
150
|
+
config.roundedTools = DEFAULT_CONFIG.roundedTools;
|
|
151
|
+
}
|
|
158
152
|
if (config.settingsLanguage !== "en" && config.settingsLanguage !== "zh") {
|
|
159
153
|
config.settingsLanguage = DEFAULT_CONFIG.settingsLanguage;
|
|
160
154
|
}
|
|
161
155
|
if (config.cursorStyle !== "block" && config.cursorStyle !== "bar" && config.cursorStyle !== "underline") {
|
|
162
156
|
config.cursorStyle = DEFAULT_CONFIG.cursorStyle;
|
|
163
157
|
}
|
|
164
|
-
config.fullscreen.wheelScrollLines = normalizeFullscreenWheelScrollLines(
|
|
165
|
-
config.fullscreen.wheelScrollLines,
|
|
166
|
-
DEFAULT_CONFIG.fullscreen.wheelScrollLines,
|
|
167
|
-
);
|
|
168
158
|
if (typeof config.thinkingPeek !== "object" || config.thinkingPeek === null || Array.isArray(config.thinkingPeek)) {
|
|
169
159
|
config.thinkingPeek = structuredClone(DEFAULT_CONFIG.thinkingPeek);
|
|
170
160
|
} else {
|
|
171
161
|
config.thinkingPeek.lines = normalizeThinkingPeekLines(config.thinkingPeek.lines);
|
|
172
162
|
}
|
|
173
|
-
if (typeof config.inlineFooter !== "boolean") {
|
|
174
|
-
config.inlineFooter = DEFAULT_CONFIG.inlineFooter;
|
|
175
|
-
}
|
|
176
|
-
if (typeof config.roundedTools !== "boolean") {
|
|
177
|
-
config.roundedTools = DEFAULT_CONFIG.roundedTools;
|
|
178
|
-
}
|
|
179
163
|
return config;
|
|
180
164
|
} catch (err) {
|
|
181
165
|
notify?.(`pi-tui config parse error: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
package/extensions/editor.ts
CHANGED
|
@@ -7,10 +7,6 @@ import {
|
|
|
7
7
|
import type { EditorTheme, TuiMouseEvent, TuiMouseEventResult, TUI } from "@earendil-works/pi-tui";
|
|
8
8
|
import { CURSOR_MARKER, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { CursorStyle } from "./config.ts";
|
|
10
|
-
import {
|
|
11
|
-
applyFullscreenWheelScrollLines,
|
|
12
|
-
DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
13
|
-
} from "./fullscreen-scroll.ts";
|
|
14
10
|
import { findBottomBorderIndex, isEditorBorderLine, stripAnsi } from "./utils.ts";
|
|
15
11
|
|
|
16
12
|
function fillLine(content: string, width: number): string {
|
|
@@ -316,14 +312,12 @@ export function installEditor(
|
|
|
316
312
|
_pi: ExtensionAPI,
|
|
317
313
|
ctx: ExtensionContext,
|
|
318
314
|
cursorStyle: CursorStyle = "block",
|
|
319
|
-
wheelScrollLines = DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
320
315
|
inlineFooter?: InlineFooterRenderer,
|
|
321
316
|
) {
|
|
322
317
|
let activeTui: TUI | undefined;
|
|
323
318
|
let activeEditor: OpenTuiEditor | undefined;
|
|
324
319
|
let previousHardwareCursor: boolean | undefined;
|
|
325
320
|
let currentCursorStyle = cursorStyle;
|
|
326
|
-
let currentWheelScrollLines = wheelScrollLines;
|
|
327
321
|
let hiddenThinkingTarget: HiddenThinkingLabelComponent | undefined;
|
|
328
322
|
const getActiveTui = (): TUI => {
|
|
329
323
|
if (!activeTui) throw new Error("Open TUI editor is not mounted");
|
|
@@ -333,7 +327,6 @@ export function installEditor(
|
|
|
333
327
|
ctx.ui.setEditorComponent((tui, editorTheme, keybindings) => {
|
|
334
328
|
activeTui = tui;
|
|
335
329
|
hiddenThinkingTarget = undefined;
|
|
336
|
-
applyFullscreenWheelScrollLines(tui, currentWheelScrollLines);
|
|
337
330
|
previousHardwareCursor = tui.getShowHardwareCursor();
|
|
338
331
|
activeEditor = new OpenTuiEditor(tui, editorTheme, keybindings, currentCursorStyle, inlineFooter);
|
|
339
332
|
return activeEditor;
|
|
@@ -359,10 +352,6 @@ export function installEditor(
|
|
|
359
352
|
currentCursorStyle = nextCursorStyle;
|
|
360
353
|
activeEditor?.setCursorStyle(nextCursorStyle, previousHardwareCursor);
|
|
361
354
|
},
|
|
362
|
-
setWheelScrollLines(nextWheelScrollLines: number): void {
|
|
363
|
-
currentWheelScrollLines = nextWheelScrollLines;
|
|
364
|
-
if (activeTui) applyFullscreenWheelScrollLines(activeTui, currentWheelScrollLines);
|
|
365
|
-
},
|
|
366
355
|
cleanup(): void {
|
|
367
356
|
hiddenThinkingTarget = undefined;
|
|
368
357
|
ctx.ui.setEditorComponent(undefined);
|
package/extensions/footer.ts
CHANGED
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
formatCwd,
|
|
17
17
|
formatDuration,
|
|
18
18
|
formatInputBreakdown,
|
|
19
|
+
hasAnsiStyles,
|
|
19
20
|
providerColor,
|
|
20
|
-
|
|
21
|
+
sanitizeStatusPreservingStyles,
|
|
21
22
|
stressColor,
|
|
22
23
|
truncateBranch,
|
|
23
24
|
truncatePath,
|
|
@@ -181,6 +182,18 @@ function renderStatsBlock(
|
|
|
181
182
|
return stats.join(` ${theme.fg("dim", "|")} `);
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
const ANSI_RESET = "\x1b[0m";
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Keep the colours an extension applied to its own status text. Plain text stays
|
|
189
|
+
* muted so unstyled statuses keep the quiet footer look. The trailing reset
|
|
190
|
+
* stops an unclosed style from tinting the separator and the next status.
|
|
191
|
+
*/
|
|
192
|
+
function renderStatusText(theme: Theme, status: string): string {
|
|
193
|
+
if (!hasAnsiStyles(status)) return theme.fg("muted", status);
|
|
194
|
+
return `${status}${ANSI_RESET}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
184
197
|
function renderExtensionStatusLines(
|
|
185
198
|
theme: Theme,
|
|
186
199
|
extensionStatuses: ReadonlyMap<string, string>,
|
|
@@ -189,12 +202,12 @@ function renderExtensionStatusLines(
|
|
|
189
202
|
): string[] {
|
|
190
203
|
const statuses = Array.from(extensionStatuses.entries())
|
|
191
204
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
192
|
-
.map(([, text]) =>
|
|
205
|
+
.map(([, text]) => sanitizeStatusPreservingStyles(text))
|
|
193
206
|
.filter((text) => text.length > 0);
|
|
194
207
|
if (statuses.length === 0) return [];
|
|
195
208
|
|
|
196
209
|
const separator = ` ${theme.fg("dim", "|")} `;
|
|
197
|
-
const statusText = statuses.map((status) => theme
|
|
210
|
+
const statusText = statuses.map((status) => renderStatusText(theme, status)).join(separator);
|
|
198
211
|
const line = `${theme.fg("mdLink", glyphs.extensions)} ${statusText}`;
|
|
199
212
|
return wrapTextWithAnsi(line, width);
|
|
200
213
|
}
|
package/extensions/index.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* thinking peek preview, rounded tool frames, and turn telemetry.
|
|
5
5
|
*
|
|
6
6
|
* Integrated from:
|
|
7
|
-
* - pi-open-tui (v0.3.
|
|
7
|
+
* - pi-open-tui (v0.3.11, MIT, by OldSun, https://github.com/OldSuns/pi-open-tui)
|
|
8
8
|
* - pi-rounded-tools (v0.1.3, MIT, by OrionPax, https://github.com/orionpax1997/pi-rounded-tools)
|
|
9
9
|
*
|
|
10
10
|
* Security audit notes:
|
|
@@ -169,7 +169,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
169
169
|
pi,
|
|
170
170
|
ctx,
|
|
171
171
|
config.cursorStyle,
|
|
172
|
-
config.fullscreen.wheelScrollLines,
|
|
173
172
|
{
|
|
174
173
|
enabled: () => config.inlineFooter,
|
|
175
174
|
render: footer.renderInline,
|
|
@@ -423,7 +422,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
423
422
|
const wasEnabled = config.enabled;
|
|
424
423
|
const wasRoundedTools = config.roundedTools;
|
|
425
424
|
const cursorStyleChanged = config.cursorStyle !== newConfig.cursorStyle;
|
|
426
|
-
const wheelScrollLinesChanged = config.fullscreen.wheelScrollLines !== newConfig.fullscreen.wheelScrollLines;
|
|
427
425
|
const thinkingPeekLinesChanged = config.thinkingPeek.lines !== newConfig.thinkingPeek.lines;
|
|
428
426
|
saveConfig(newConfig);
|
|
429
427
|
config = newConfig;
|
|
@@ -436,9 +434,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
436
434
|
if (cursorStyleChanged && active && editor) {
|
|
437
435
|
editor.setCursorStyle(newConfig.cursorStyle);
|
|
438
436
|
}
|
|
439
|
-
if (wheelScrollLinesChanged && active && editor) {
|
|
440
|
-
editor.setWheelScrollLines(newConfig.fullscreen.wheelScrollLines);
|
|
441
|
-
}
|
|
442
437
|
if (lastCtx) {
|
|
443
438
|
pendingUiChange = getPendingUiChange(newConfig.enabled, active);
|
|
444
439
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Merged 2026-08 from the standalone local extension rounded-tools.ts (a
|
|
5
5
|
* localized copy of npm:pi-rounded-tools@0.1.3, MIT, by OrionPax,
|
|
6
6
|
* https://github.com/orionpax1997/pi-rounded-tools), integrated into pi-tui
|
|
7
|
-
* (based on https://github.com/OldSuns/pi-open-tui v0.3.
|
|
7
|
+
* (based on https://github.com/OldSuns/pi-open-tui v0.3.11, MIT, by
|
|
8
8
|
* OldSun) and gated behind `roundedTools` in pi-tui config
|
|
9
9
|
* (~/.pi/agent/pi-tui.json, default on).
|
|
10
10
|
*
|
|
@@ -31,10 +31,11 @@ import {
|
|
|
31
31
|
createFindToolDefinition,
|
|
32
32
|
createGrepToolDefinition,
|
|
33
33
|
createLsToolDefinition,
|
|
34
|
+
createPowerShellToolDefinition,
|
|
34
35
|
createReadToolDefinition,
|
|
35
36
|
createWriteToolDefinition,
|
|
36
37
|
} from "@earendil-works/pi-coding-agent";
|
|
37
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
38
|
+
import type { Component, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
38
39
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
39
40
|
|
|
40
41
|
// ─── Rounded-corner frame ────────────────────────────────────────────────
|
|
@@ -53,6 +54,10 @@ class RoundedFrame implements Component {
|
|
|
53
54
|
private readonly mode: FrameMode = "closed",
|
|
54
55
|
) {}
|
|
55
56
|
|
|
57
|
+
getInner(): Component {
|
|
58
|
+
return this.inner;
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
invalidate(): void {
|
|
57
62
|
this.inner.invalidate?.();
|
|
58
63
|
this.cachedWidth = undefined;
|
|
@@ -60,6 +65,32 @@ class RoundedFrame implements Component {
|
|
|
60
65
|
this.cachedLines = undefined;
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
69
|
+
if (!this.inner.handleMouse) return undefined;
|
|
70
|
+
|
|
71
|
+
const width = this.cachedWidth ?? event.width;
|
|
72
|
+
if (width < 4) {
|
|
73
|
+
return this.inner.handleMouse(event);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const innerWidth = Math.max(1, width - 4);
|
|
77
|
+
const paddingTop = this.mode !== "open-top" ? 1 : 0;
|
|
78
|
+
const contentX = event.x - 2;
|
|
79
|
+
const contentY = event.y - paddingTop;
|
|
80
|
+
const innerHeight = this.cachedInnerLines?.length ?? 0;
|
|
81
|
+
|
|
82
|
+
if (contentX < 0 || contentX >= innerWidth || contentY < 0 || contentY >= innerHeight) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return this.inner.handleMouse({
|
|
87
|
+
...event,
|
|
88
|
+
x: contentX,
|
|
89
|
+
y: contentY,
|
|
90
|
+
width: innerWidth,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
// pi's built-in tool renderers cache the previously-returned component
|
|
64
95
|
// via `context.lastComponent` and call mutating methods on it
|
|
65
96
|
// (`Text.setText`, `Container.clear`/`addChild`, `invalidate`) so they
|
|
@@ -177,8 +208,8 @@ const frame = (
|
|
|
177
208
|
function borderColorFor(
|
|
178
209
|
context: { isPartial?: boolean; isError?: boolean } | undefined,
|
|
179
210
|
): string {
|
|
180
|
-
if (context?.isPartial) return "warning";
|
|
181
211
|
if (context?.isError) return "error";
|
|
212
|
+
if (context?.isPartial) return "warning";
|
|
182
213
|
return "border";
|
|
183
214
|
}
|
|
184
215
|
|
|
@@ -189,81 +220,153 @@ type ToolDef = ToolDefinition<any, any, any>;
|
|
|
189
220
|
const EMPTY_COMPONENT: Component = { render: () => [], invalidate: () => {} };
|
|
190
221
|
|
|
191
222
|
/**
|
|
192
|
-
*
|
|
223
|
+
* Unwrap inner component from a RoundedFrame if context.lastComponent is wrapped,
|
|
224
|
+
* allowing underlying renderers (e.g. edit, bash, read) to preserve their component
|
|
225
|
+
* cache across renders instead of recreating on every tick.
|
|
226
|
+
*/
|
|
227
|
+
function unwrapInner(component: unknown): Component | undefined {
|
|
228
|
+
if (component instanceof RoundedFrame) {
|
|
229
|
+
return component.getInner();
|
|
230
|
+
}
|
|
231
|
+
return component as Component | undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Wrap a tool definition in rounded frames.
|
|
193
236
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* `renderShell` are overwritten. This keeps the tool's LLM-facing prompt
|
|
198
|
-
* data intact, edit's argument pre-processing intact, etc.
|
|
237
|
+
* Preserves all metadata (promptSnippet, promptGuidelines, parameters, etc.)
|
|
238
|
+
* while wrapping renderCall and renderResult with rounded borders.
|
|
239
|
+
* Uses closed mode for tools without renderResult to avoid open bottom borders.
|
|
199
240
|
*/
|
|
200
241
|
function wrapBuiltin(def: ToolDef) {
|
|
242
|
+
const callMode: FrameMode = def.renderResult ? "open-bottom" : "closed";
|
|
201
243
|
return {
|
|
202
244
|
...def,
|
|
203
245
|
renderShell: "self" as const,
|
|
204
246
|
renderCall: (args: any, theme: any, context: any) => {
|
|
205
|
-
|
|
206
|
-
// doesn't treat a RoundedFrame as its own lastComponent — that
|
|
207
|
-
// would either throw (`Text.setText` is missing) or nest frames
|
|
208
|
-
// (`inner = previous RoundedFrame` → we re-wrap it). Forcing the
|
|
209
|
-
// inner to allocate fresh costs one Text/Container per render but
|
|
210
|
-
// keeps every render independent and CC-protect against future
|
|
211
|
-
// tool changes.
|
|
247
|
+
const unwrapped = unwrapInner(context?.lastComponent);
|
|
212
248
|
const inner: Component = def.renderCall
|
|
213
|
-
? def.renderCall(args, theme, { ...context, lastComponent:
|
|
249
|
+
? def.renderCall(args, theme, { ...context, lastComponent: unwrapped })
|
|
214
250
|
: EMPTY_COMPONENT;
|
|
215
|
-
|
|
216
|
-
// timer). Skip the frame then — keeping its borders stable avoids
|
|
217
|
-
// redraw flicker — and only wrap it once the tool settles.
|
|
218
|
-
if (context?.isPartial) return inner;
|
|
219
|
-
return frame(inner, theme, "open-bottom", borderColorFor(context));
|
|
251
|
+
return frame(inner, theme, callMode, borderColorFor(context));
|
|
220
252
|
},
|
|
221
253
|
renderResult: (result: any, options: any, theme: any, context: any) => {
|
|
254
|
+
const unwrapped = unwrapInner(context?.lastComponent);
|
|
222
255
|
const inner: Component = def.renderResult
|
|
223
|
-
? def.renderResult(result, options, theme, { ...context, lastComponent:
|
|
256
|
+
? def.renderResult(result, options, theme, { ...context, lastComponent: unwrapped })
|
|
224
257
|
: EMPTY_COMPONENT;
|
|
225
|
-
if (context?.isPartial) return inner;
|
|
226
258
|
return frame(inner, theme, "open-top", borderColorFor(context));
|
|
227
259
|
},
|
|
228
260
|
};
|
|
229
261
|
}
|
|
230
262
|
|
|
231
263
|
/**
|
|
232
|
-
* Register
|
|
233
|
-
* (`enabled`) or with
|
|
234
|
-
*
|
|
235
|
-
*
|
|
264
|
+
* Register built-in tools (read, write, edit, bash, powershell, grep, find, ls),
|
|
265
|
+
* either with rounded frames (`enabled`) or with stock definitions (`!enabled`).
|
|
266
|
+
*
|
|
267
|
+
* If a third-party extension has already replaced a built-in tool (e.g. pi-fff
|
|
268
|
+
* overriding grep/find), that tool is skipped here to prevent clobbering.
|
|
236
269
|
*/
|
|
237
|
-
export function registerRoundedTools(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
270
|
+
export function registerRoundedTools(
|
|
271
|
+
target: ExtensionAPI | ((tool: ToolDef) => void),
|
|
272
|
+
enabled: boolean,
|
|
273
|
+
cwd: string,
|
|
274
|
+
captured?: Map<string, ToolDef>,
|
|
275
|
+
): void {
|
|
276
|
+
const register = typeof target === "function" ? target : target.registerTool.bind(target);
|
|
277
|
+
|
|
278
|
+
const builtins: Record<string, () => ToolDef> = {
|
|
279
|
+
read: () => createReadToolDefinition(cwd),
|
|
280
|
+
write: () => createWriteToolDefinition(cwd),
|
|
281
|
+
edit: () => createEditToolDefinition(cwd),
|
|
282
|
+
bash: () => createBashToolDefinition(cwd),
|
|
283
|
+
powershell: () => createPowerShellToolDefinition(cwd),
|
|
284
|
+
ls: () => createLsToolDefinition(cwd),
|
|
285
|
+
grep: () => createGrepToolDefinition(cwd),
|
|
286
|
+
find: () => createFindToolDefinition(cwd),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
for (const [name, factory] of Object.entries(builtins)) {
|
|
290
|
+
if (!captured?.has(name)) {
|
|
291
|
+
register(enabled ? wrapBuiltin(factory()) : factory());
|
|
292
|
+
}
|
|
250
293
|
}
|
|
251
294
|
}
|
|
252
295
|
|
|
253
296
|
/**
|
|
254
297
|
* Manager class encapsulating rounded tools registration and lifecycle updates.
|
|
298
|
+
*
|
|
299
|
+
* In addition to wrapping built-in tools, it installs a `pi.registerTool` proxy
|
|
300
|
+
* so that any tool registered by third-party extensions (e.g. pi-fff's ffgrep,
|
|
301
|
+
* fffind, fff-multi-grep, or overridden grep/find) automatically receives rounded
|
|
302
|
+
* frames when `roundedTools` is enabled, without maintaining a fragile hardcoded
|
|
303
|
+
* name whitelist.
|
|
304
|
+
*
|
|
305
|
+
* Load-order note: For extensions that register tools at load time, place
|
|
306
|
+
* `pi-tui-footer` before those packages in `~/.pi/agent/settings.json` so the
|
|
307
|
+
* proxy is active when they load.
|
|
255
308
|
*/
|
|
256
309
|
export class RoundedToolsManager {
|
|
257
|
-
|
|
310
|
+
private enabled = false;
|
|
311
|
+
private proxyInstalled = false;
|
|
312
|
+
/** The real `pi.registerTool` before we installed the proxy. */
|
|
313
|
+
private originalRegisterTool!: (tool: ToolDef) => void;
|
|
314
|
+
/** Original (unwrapped) definitions captured from third-party tools. */
|
|
315
|
+
private readonly captured = new Map<string, ToolDef>();
|
|
316
|
+
|
|
317
|
+
constructor(private readonly pi: ExtensionAPI) {
|
|
318
|
+
this.installProxy();
|
|
319
|
+
}
|
|
258
320
|
|
|
259
321
|
/** Register initial tools before session_start with fallback cwd. */
|
|
260
322
|
init(enabled: boolean, cwd = process.cwd()): void {
|
|
261
|
-
|
|
323
|
+
this.enabled = enabled;
|
|
324
|
+
this.installProxy();
|
|
325
|
+
registerRoundedTools(this.originalRegisterTool, enabled, cwd, this.captured);
|
|
326
|
+
this.reRegisterCaptured();
|
|
262
327
|
}
|
|
263
328
|
|
|
264
329
|
/** Apply tools with current session cwd. */
|
|
265
330
|
apply(enabled: boolean, cwd: string): void {
|
|
266
|
-
|
|
331
|
+
this.enabled = enabled;
|
|
332
|
+
this.installProxy();
|
|
333
|
+
registerRoundedTools(this.originalRegisterTool, enabled, cwd, this.captured);
|
|
334
|
+
this.reRegisterCaptured();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Install a one-time proxy on `pi.registerTool` to intercept third-party
|
|
339
|
+
* tool registrations.
|
|
340
|
+
*/
|
|
341
|
+
private installProxy(): void {
|
|
342
|
+
if (this.proxyInstalled) return;
|
|
343
|
+
this.proxyInstalled = true;
|
|
344
|
+
|
|
345
|
+
this.originalRegisterTool = this.pi.registerTool.bind(this.pi);
|
|
346
|
+
const original = this.originalRegisterTool;
|
|
347
|
+
const self = this;
|
|
348
|
+
|
|
349
|
+
this.pi.registerTool = function proxyRegisterTool(tool: ToolDef) {
|
|
350
|
+
// Don't intercept tools that already render their own shell
|
|
351
|
+
if (tool.renderShell === "self") {
|
|
352
|
+
original(tool);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Capture the unwrapped original definition
|
|
357
|
+
self.captured.set(tool.name, tool);
|
|
358
|
+
original(self.enabled ? wrapBuiltin(tool) : tool);
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Re-register all captured third-party tools with or without rounded
|
|
364
|
+
* frames based on the current `enabled` state.
|
|
365
|
+
*/
|
|
366
|
+
private reRegisterCaptured(): void {
|
|
367
|
+
for (const def of this.captured.values()) {
|
|
368
|
+
this.originalRegisterTool(this.enabled ? wrapBuiltin(def) : def);
|
|
369
|
+
}
|
|
267
370
|
}
|
|
268
371
|
}
|
|
269
372
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
Box,
|
|
4
|
-
Input,
|
|
5
4
|
Key,
|
|
6
5
|
matchesKey,
|
|
7
6
|
SelectList,
|
|
@@ -11,10 +10,6 @@ import {
|
|
|
11
10
|
Text,
|
|
12
11
|
} from "@earendil-works/pi-tui";
|
|
13
12
|
import type { CursorStyle, IconMode, OpenTuiConfig, SettingsLanguage, ThinkingPeekLines } from "./config.ts";
|
|
14
|
-
import {
|
|
15
|
-
DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
16
|
-
normalizeFullscreenWheelScrollLines,
|
|
17
|
-
} from "./fullscreen-scroll.ts";
|
|
18
13
|
|
|
19
14
|
interface SettingItem {
|
|
20
15
|
id: string;
|
|
@@ -30,14 +25,13 @@ const COPY = {
|
|
|
30
25
|
en: {
|
|
31
26
|
title: "Pi TUI Settings",
|
|
32
27
|
tabs: { features: "General", icons: "Appearance", segments: "Footer", telemetry: "Telemetry" },
|
|
33
|
-
hint: "Tab/Shift+Tab/←/→: tabs · ↑/↓: move · Enter/Space: change ·
|
|
28
|
+
hint: "Tab/Shift+Tab/←/→: tabs · ↑/↓: move · Enter/Space: change · Esc/q: close",
|
|
34
29
|
labels: {
|
|
35
30
|
enabled: "Enabled",
|
|
36
31
|
roundedTools: "Rounded tool frames",
|
|
37
32
|
inlineFooter: "Inline footer",
|
|
38
33
|
thinkingPeek: "Thinking peek",
|
|
39
34
|
language: "Language",
|
|
40
|
-
wheelScrollLines: "Mouse wheel speed",
|
|
41
35
|
cursorStyle: "Cursor style",
|
|
42
36
|
iconMode: "Icon mode",
|
|
43
37
|
cwd: "CWD",
|
|
@@ -62,8 +56,6 @@ const COPY = {
|
|
|
62
56
|
off: "Off",
|
|
63
57
|
thinkingPeek: { off: "Off", one: "1 line", two: "2 lines" },
|
|
64
58
|
languages: { en: "English", zh: "简体中文" },
|
|
65
|
-
wheelLines: (count: number) => `${count} ${count === 1 ? "line" : "lines"} / notch`,
|
|
66
|
-
wheelPrompt: (count: number) => `Wheel scroll lines per notch, 1-10 (current: ${count}). Enter: apply · Esc: cancel`,
|
|
67
59
|
cursorStyles: { block: "Block", bar: "Bar", underline: "Underline" },
|
|
68
60
|
icons: { auto: "Auto", nerd: "Nerd", unicode: "Unicode", ascii: "ASCII" },
|
|
69
61
|
},
|
|
@@ -71,14 +63,13 @@ const COPY = {
|
|
|
71
63
|
zh: {
|
|
72
64
|
title: "Pi TUI 设置",
|
|
73
65
|
tabs: { features: "常规", icons: "外观", segments: "Footer", telemetry: "遥测" },
|
|
74
|
-
hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 ·
|
|
66
|
+
hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · Esc/q:关闭",
|
|
75
67
|
labels: {
|
|
76
68
|
enabled: "启用",
|
|
77
69
|
roundedTools: "圆角工具边框",
|
|
78
70
|
inlineFooter: "内联底栏",
|
|
79
71
|
thinkingPeek: "思考预览",
|
|
80
72
|
language: "语言",
|
|
81
|
-
wheelScrollLines: "鼠标滚轮速度",
|
|
82
73
|
cursorStyle: "光标样式",
|
|
83
74
|
iconMode: "图标模式",
|
|
84
75
|
cwd: "当前目录",
|
|
@@ -103,8 +94,6 @@ const COPY = {
|
|
|
103
94
|
off: "关闭",
|
|
104
95
|
thinkingPeek: { off: "关闭", one: "单行", two: "双行" },
|
|
105
96
|
languages: { en: "English", zh: "简体中文" },
|
|
106
|
-
wheelLines: (count: number) => `每格 ${count} 行`,
|
|
107
|
-
wheelPrompt: (count: number) => `滚轮每格滚动行数(当前 ${count},范围 1-10),输入后 Enter 应用 · Esc 取消`,
|
|
108
97
|
cursorStyles: { block: "块", bar: "竖线", underline: "下划线" },
|
|
109
98
|
icons: { auto: "自动", nerd: "Nerd", unicode: "Unicode", ascii: "ASCII" },
|
|
110
99
|
},
|
|
@@ -150,19 +139,6 @@ function cycleCursorStyle(config: OpenTuiConfig): OpenTuiConfig {
|
|
|
150
139
|
return { ...config, cursorStyle: next };
|
|
151
140
|
}
|
|
152
141
|
|
|
153
|
-
function setWheelScrollLines(config: OpenTuiConfig, raw: string): OpenTuiConfig | undefined {
|
|
154
|
-
if (!/^\d+$/.test(raw)) return undefined;
|
|
155
|
-
const parsed = Number(raw);
|
|
156
|
-
const bounded = Number.isFinite(parsed) ? parsed : Number.MAX_SAFE_INTEGER;
|
|
157
|
-
return {
|
|
158
|
-
...config,
|
|
159
|
-
fullscreen: {
|
|
160
|
-
...config.fullscreen,
|
|
161
|
-
wheelScrollLines: normalizeFullscreenWheelScrollLines(bounded, DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES),
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
142
|
function toggleTelemetry(config: OpenTuiConfig, key: keyof OpenTuiConfig["telemetry"]): OpenTuiConfig {
|
|
167
143
|
return {
|
|
168
144
|
...config,
|
|
@@ -184,11 +160,6 @@ function buildFeaturesItems(config: OpenTuiConfig, copy: SettingsCopy): SettingI
|
|
|
184
160
|
{ id: "enabled", label: copy.labels.enabled, currentValue: flag(config.enabled) },
|
|
185
161
|
{ id: "roundedTools", label: copy.labels.roundedTools, currentValue: flag(config.roundedTools) },
|
|
186
162
|
{ id: "settingsLanguage", label: copy.labels.language, currentValue: copy.values.languages[config.settingsLanguage] },
|
|
187
|
-
{
|
|
188
|
-
id: "wheelScrollLines",
|
|
189
|
-
label: copy.labels.wheelScrollLines,
|
|
190
|
-
currentValue: copy.values.wheelLines(config.fullscreen.wheelScrollLines),
|
|
191
|
-
},
|
|
192
163
|
{ id: "thinkingPeek", label: copy.labels.thinkingPeek, currentValue: formatThinkingPeekLines(config.thinkingPeek.lines, copy) },
|
|
193
164
|
{ id: "inlineFooter", label: copy.labels.inlineFooter, currentValue: flag(config.inlineFooter) },
|
|
194
165
|
];
|
|
@@ -275,23 +246,6 @@ interface SettingsUiHandle {
|
|
|
275
246
|
handleInput: (data: string) => void;
|
|
276
247
|
}
|
|
277
248
|
|
|
278
|
-
function insertComponentAfter(list: Component, child: Component, index: () => number): Component {
|
|
279
|
-
// SelectList renders one line per item; splice the editor into that output.
|
|
280
|
-
return {
|
|
281
|
-
render(width: number): string[] {
|
|
282
|
-
const lines = list.render(width);
|
|
283
|
-
const insertAt = index();
|
|
284
|
-
if (insertAt < 0 || insertAt >= lines.length) return lines;
|
|
285
|
-
const childLines = child.render(width);
|
|
286
|
-
return [...lines.slice(0, insertAt + 1), ...childLines, ...lines.slice(insertAt + 1)];
|
|
287
|
-
},
|
|
288
|
-
invalidate(): void {
|
|
289
|
-
list.invalidate();
|
|
290
|
-
child.invalidate();
|
|
291
|
-
},
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
|
|
295
249
|
class SettingsUi implements SettingsUiHandle {
|
|
296
250
|
private tab: Tab = "features";
|
|
297
251
|
private config: OpenTuiConfig;
|
|
@@ -304,7 +258,6 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
304
258
|
private cachedWidth: number | undefined;
|
|
305
259
|
private cachedLines: string[] | undefined;
|
|
306
260
|
private compact = false;
|
|
307
|
-
private wheelInput: Input | undefined;
|
|
308
261
|
|
|
309
262
|
constructor(
|
|
310
263
|
theme: Theme,
|
|
@@ -329,35 +282,11 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
329
282
|
|
|
330
283
|
private applySetting(itemId: string): void {
|
|
331
284
|
this.selectedItemByTab[this.tab] = itemId;
|
|
332
|
-
if (this.tab === "features" && itemId === "wheelScrollLines") {
|
|
333
|
-
this.openWheelInput();
|
|
334
|
-
this.invalidate();
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
285
|
this.config = handleSettingChange(this.tab, itemId, this.config);
|
|
338
286
|
this.onChange(this.config);
|
|
339
287
|
this.rebuild(itemId);
|
|
340
288
|
}
|
|
341
289
|
|
|
342
|
-
private openWheelInput(): void {
|
|
343
|
-
const input = new Input();
|
|
344
|
-
input.onSubmit = (value) => {
|
|
345
|
-
const next = setWheelScrollLines(this.config, value);
|
|
346
|
-
this.wheelInput = undefined;
|
|
347
|
-
if (next) {
|
|
348
|
-
this.config = next;
|
|
349
|
-
this.onChange(this.config);
|
|
350
|
-
}
|
|
351
|
-
this.rebuild("wheelScrollLines");
|
|
352
|
-
};
|
|
353
|
-
input.onEscape = () => {
|
|
354
|
-
this.wheelInput = undefined;
|
|
355
|
-
this.rebuild("wheelScrollLines");
|
|
356
|
-
};
|
|
357
|
-
this.wheelInput = input;
|
|
358
|
-
this.rebuild("wheelScrollLines");
|
|
359
|
-
}
|
|
360
|
-
|
|
361
290
|
private switchTab(offset: number): void {
|
|
362
291
|
const idx = TABS.indexOf(this.tab);
|
|
363
292
|
this.tab = TABS[(idx + offset + TABS.length) % TABS.length]!;
|
|
@@ -377,17 +306,11 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
377
306
|
this.container.addChild(new Text(tabBar, 1, 0));
|
|
378
307
|
this.container.addChild(new Text(this.theme.fg("dim", copy.hint), 1, 0));
|
|
379
308
|
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
label: editing
|
|
386
|
-
? (this.compact ? `${item.label}:` : item.label)
|
|
387
|
-
: (this.compact ? `${item.label}: ${item.currentValue}` : item.label),
|
|
388
|
-
description: editing || this.compact ? undefined : item.currentValue,
|
|
389
|
-
} as SelectItem;
|
|
390
|
-
});
|
|
309
|
+
const items = buildItems(this.tab, this.config).map((item) => ({
|
|
310
|
+
value: item.id,
|
|
311
|
+
label: this.compact ? `${item.label}: ${item.currentValue}` : item.label,
|
|
312
|
+
description: this.compact ? undefined : item.currentValue,
|
|
313
|
+
} as SelectItem));
|
|
391
314
|
this.selectList = new SelectList(items, Math.min(items.length, 12), {
|
|
392
315
|
selectedPrefix: (t) => this.theme.fg("accent", t),
|
|
393
316
|
selectedText: (t) => this.theme.fg("accent", t),
|
|
@@ -409,31 +332,13 @@ class SettingsUi implements SettingsUiHandle {
|
|
|
409
332
|
this.selectList.onCancel = () => {
|
|
410
333
|
this.onClose();
|
|
411
334
|
};
|
|
412
|
-
|
|
413
|
-
this.wheelInput!.focused = true;
|
|
414
|
-
const wheelInputGroup = new Box(4, 0);
|
|
415
|
-
wheelInputGroup.addChild(new Text(
|
|
416
|
-
this.theme.fg("muted", copy.values.wheelPrompt(this.config.fullscreen.wheelScrollLines)),
|
|
417
|
-
0,
|
|
418
|
-
0,
|
|
419
|
-
));
|
|
420
|
-
wheelInputGroup.addChild(this.wheelInput!);
|
|
421
|
-
const selectedIndex = () => items.findIndex((item) => item.value === this.selectList.getSelectedItem()?.value);
|
|
422
|
-
this.container.addChild(insertComponentAfter(this.selectList, wheelInputGroup, selectedIndex));
|
|
423
|
-
} else {
|
|
424
|
-
this.container.addChild(this.selectList);
|
|
425
|
-
}
|
|
335
|
+
this.container.addChild(this.selectList);
|
|
426
336
|
|
|
427
337
|
this.cachedWidth = undefined;
|
|
428
338
|
this.cachedLines = undefined;
|
|
429
339
|
}
|
|
430
340
|
|
|
431
341
|
handleInput(data: string): void {
|
|
432
|
-
if (this.wheelInput && this.tab === "features") {
|
|
433
|
-
this.wheelInput.handleInput(data);
|
|
434
|
-
this.invalidate();
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
342
|
if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
|
|
438
343
|
this.switchTab(1);
|
|
439
344
|
this.invalidate();
|
package/extensions/utils.ts
CHANGED
|
@@ -296,6 +296,28 @@ export function sanitizeStatus(text: string): string {
|
|
|
296
296
|
.trim();
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
// One status token is an SGR sequence, another escape sequence, or one control
|
|
300
|
+
// character. SGR carries colours. Everything else moves the cursor or rewrites
|
|
301
|
+
// the terminal, so it must never reach the footer row.
|
|
302
|
+
const STATUS_TOKEN = /(?:\x1b\[|\x9b)[0-?]*[ -/]*m|(?:\x1b\[|\x9b)[0-?]*[ -/]*[@-~]|(?:\x1bP|\x90|\x1bX|\x98|\x1b\^|\x9e|\x1b_|\x9f)[\s\S]*?(?:\x1b\\|\x9c|$)|(?:\x1b\]|\x9d)[\s\S]*?(?:\x07|\x1b\\|\x9c|$)|\x1b[@-Z\\_]|[\u0000-\u001f\u007f-\u009f]/g;
|
|
303
|
+
const SGR_TOKEN = /^(?:\x1b\[|\x9b)[0-9;:]*m$/;
|
|
304
|
+
const SGR_SEQUENCE = /(?:\x1b\[|\x9b)[0-9;:]*m/;
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Sanitize a status text the same way {@link sanitizeStatus} does, but keep the
|
|
308
|
+
* SGR sequences the extension set. Extensions colour their own status text with
|
|
309
|
+
* `ctx.ui.theme.fg()`. Stripping those codes renders every status grey.
|
|
310
|
+
*/
|
|
311
|
+
export function sanitizeStatusPreservingStyles(text: string): string {
|
|
312
|
+
const cleaned = text.replace(STATUS_TOKEN, (token) => (SGR_TOKEN.test(token) ? token : " "));
|
|
313
|
+
return cleaned.replace(/ {2,}/g, " ").trim();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** True when the text carries SGR colour or style sequences. */
|
|
317
|
+
export function hasAnsiStyles(text: string): boolean {
|
|
318
|
+
return SGR_SEQUENCE.test(text);
|
|
319
|
+
}
|
|
320
|
+
|
|
299
321
|
export function formatThinkingLabel(level: string): string {
|
|
300
322
|
if (level === "off") return "thinking off";
|
|
301
323
|
return `${level} effort`;
|
package/package.json
CHANGED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { TUI } from "@earendil-works/pi-tui";
|
|
2
|
-
|
|
3
|
-
export const MIN_FULLSCREEN_WHEEL_SCROLL_LINES = 1;
|
|
4
|
-
export const MAX_FULLSCREEN_WHEEL_SCROLL_LINES = 10;
|
|
5
|
-
export const DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES = 4;
|
|
6
|
-
|
|
7
|
-
export function normalizeFullscreenWheelScrollLines(
|
|
8
|
-
value: unknown,
|
|
9
|
-
fallback = DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
10
|
-
): number {
|
|
11
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
12
|
-
return Math.min(
|
|
13
|
-
MAX_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
14
|
-
Math.max(MIN_FULLSCREEN_WHEEL_SCROLL_LINES, Math.floor(value)),
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type FullscreenWheelTui = TUI & { wheelScrollLines?: unknown };
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Pi 0.84.2 keeps this constructor option in a private runtime field. Remove
|
|
22
|
-
* this shim once Pi exposes a public setter; incompatible versions are no-ops.
|
|
23
|
-
*/
|
|
24
|
-
export function applyFullscreenWheelScrollLines(tui: TUI, value: number): boolean {
|
|
25
|
-
try {
|
|
26
|
-
const fullscreenTui = tui as FullscreenWheelTui;
|
|
27
|
-
if (fullscreenTui.mode !== "fullscreen" || typeof fullscreenTui.wheelScrollLines !== "number") {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
return Reflect.set(fullscreenTui, "wheelScrollLines", normalizeFullscreenWheelScrollLines(value));
|
|
31
|
-
} catch {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
}
|