pi-open-tui 0.3.7 → 0.3.9
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 +5 -1
- package/README.zh-CN.md +5 -1
- package/extensions/open-tui/config.ts +5 -0
- package/extensions/open-tui/editor.ts +86 -6
- package/extensions/open-tui/footer.ts +211 -111
- package/extensions/open-tui/index.ts +12 -2
- package/extensions/open-tui/settings-command.ts +4 -0
- package/extensions/open-tui/utils.ts +28 -5
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ A polished terminal interface for the [Pi](https://pi.dev) coding agent. It brin
|
|
|
19
19
|
|
|
20
20
|
## Requirements
|
|
21
21
|
|
|
22
|
-
- Pi 0.
|
|
22
|
+
- Pi 0.85 or later
|
|
23
23
|
- A terminal with UTF-8 and color support
|
|
24
24
|
- A [Nerd Font](https://www.nerdfonts.com/font-downloads) for the full icon set (optional; ASCII icons are built in)
|
|
25
25
|
|
|
@@ -56,6 +56,7 @@ Run `/open-tui` to open the settings dialog. It provides **General**, **Appearan
|
|
|
56
56
|
```json
|
|
57
57
|
{
|
|
58
58
|
"enabled": true,
|
|
59
|
+
"inlineFooter": false,
|
|
59
60
|
"settingsLanguage": "en",
|
|
60
61
|
"cursorStyle": "block",
|
|
61
62
|
"fullscreen": {
|
|
@@ -98,6 +99,7 @@ Key options:
|
|
|
98
99
|
| Option | Values | Notes |
|
|
99
100
|
| --- | --- | --- |
|
|
100
101
|
| `settingsLanguage` | `en`, `zh` | Changes the `/open-tui` interface language |
|
|
102
|
+
| `inlineFooter` | `true`, `false` | Moves the two main Footer rows into the editor's top and bottom borders; defaults to `false`. Extension status rows remain below the editor |
|
|
101
103
|
| `cursorStyle` | `block`, `bar`, `underline` | `bar` and `underline` require terminal cursor-shape support |
|
|
102
104
|
| `fullscreen.wheelScrollLines` | `1`-`10` | Lines scrolled per mouse-wheel notch in fullscreen mode; defaults to `4`. In `/open-tui`, press Enter on this item and type a number (values are clamped to `1`-`10`) |
|
|
103
105
|
| `icons.mode` | `auto`, `nerd`, `ascii` | Controls footer and telemetry icons |
|
|
@@ -108,6 +110,8 @@ Key options:
|
|
|
108
110
|
|
|
109
111
|
`sessionName` appears only when the session has a name. `hostname` shows the short host name (first label of the machine's host name, e.g. `mba` from `mba.example.com`) with a server icon. `gitCommit` shows the short hash and tag in detached HEAD state. Disabling `extensionStatuses` hides the entire extension status line, including MCP status.
|
|
110
112
|
|
|
113
|
+
With `inlineFooter` enabled, the two normal Footer rows are rendered inside the editor frame to save vertical space. The top border places the Git branch on the left and CWD first in the right-hand group; the session title appears on the left too when `sessionName` is enabled. The Header and extension status rows remain separate; narrow terminals truncate lower-priority Footer data first, keeping the right-hand statistics and the border corner.
|
|
114
|
+
|
|
111
115
|
Fullscreen wheel speed uses an isolated compatibility shim for Pi 0.84.2's runtime field because Pi does not yet expose a public setter. On Pi versions without a compatible field, the setting is ignored and Pi's default scrolling remains active.
|
|
112
116
|
|
|
113
117
|
## Turn telemetry
|
package/README.zh-CN.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
## 环境要求
|
|
21
21
|
|
|
22
|
-
- Pi 0.
|
|
22
|
+
- Pi 0.85 或更高版本
|
|
23
23
|
- 支持 UTF-8 和彩色输出的终端
|
|
24
24
|
- 使用完整图标集时需要 [Nerd Font](https://www.nerdfonts.com/font-downloads)(可选;内置 ASCII 图标)
|
|
25
25
|
|
|
@@ -56,6 +56,7 @@ pi -e npm:pi-open-tui
|
|
|
56
56
|
```json
|
|
57
57
|
{
|
|
58
58
|
"enabled": true,
|
|
59
|
+
"inlineFooter": false,
|
|
59
60
|
"settingsLanguage": "zh",
|
|
60
61
|
"cursorStyle": "block",
|
|
61
62
|
"fullscreen": {
|
|
@@ -98,6 +99,7 @@ pi -e npm:pi-open-tui
|
|
|
98
99
|
| 选项 | 可选值 | 说明 |
|
|
99
100
|
| --- | --- | --- |
|
|
100
101
|
| `settingsLanguage` | `en`、`zh` | 切换 `/open-tui` 设置界面的语言 |
|
|
102
|
+
| `inlineFooter` | `true`、`false` | 将两条主要 Footer 信息行移入编辑器上下边框以节省垂直空间,默认关闭;扩展状态行仍显示在编辑器外 |
|
|
101
103
|
| `cursorStyle` | `block`、`bar`、`underline` | `bar` 和 `underline` 需要终端支持光标形状转义序列 |
|
|
102
104
|
| `fullscreen.wheelScrollLines` | `1`-`10` | 全屏模式下滚轮每格滚动的行数,默认值为 `4`;`/open-tui` 中在该项上按 Enter 后直接输入数字(超出范围会自动钳制到 `1`-`10`) |
|
|
103
105
|
| `icons.mode` | `auto`、`nerd`、`ascii` | 控制底栏和遥测通知使用的图标 |
|
|
@@ -108,6 +110,8 @@ pi -e npm:pi-open-tui
|
|
|
108
110
|
|
|
109
111
|
`sessionName` 仅在会话有名称时显示;`hostname` 会显示主机名的短名称(主机名的第一个标签,例如从 `mba.example.com` 显示为 `mba`),并使用服务器图标;`gitCommit` 会在 detached HEAD 状态下显示短哈希和标签;关闭 `extensionStatuses` 会隐藏整行扩展状态,其中也包括 MCP 状态。
|
|
110
112
|
|
|
113
|
+
开启 `inlineFooter` 后,两条常规 Footer 信息行会移入编辑器边框,从而节省垂直空间。顶部边框左侧显示 Git 分支,右侧信息组以当前目录开头;开启 `sessionName` 时,会话标题也会显示在左侧。Header 和扩展状态行保持独立显示;终端较窄时优先截断低优先级 Footer 数据,保留右侧统计信息和边框角。
|
|
114
|
+
|
|
111
115
|
全屏滚轮速度通过隔离的兼容层写入 Pi 0.84.2 的运行时字段,因为 Pi 尚未提供公开 setter。若后续 Pi 版本不再包含兼容字段,该设置会被忽略并继续使用 Pi 的默认滚动行为。
|
|
112
116
|
|
|
113
117
|
## 单轮遥测
|
|
@@ -48,6 +48,7 @@ export interface FullscreenConfig {
|
|
|
48
48
|
|
|
49
49
|
export interface OpenTuiConfig {
|
|
50
50
|
enabled: boolean;
|
|
51
|
+
inlineFooter: boolean;
|
|
51
52
|
settingsLanguage: SettingsLanguage;
|
|
52
53
|
cursorStyle: CursorStyle;
|
|
53
54
|
fullscreen: FullscreenConfig;
|
|
@@ -61,6 +62,7 @@ export interface OpenTuiConfig {
|
|
|
61
62
|
|
|
62
63
|
export const DEFAULT_CONFIG: OpenTuiConfig = {
|
|
63
64
|
enabled: true,
|
|
65
|
+
inlineFooter: false,
|
|
64
66
|
settingsLanguage: "en",
|
|
65
67
|
cursorStyle: "block",
|
|
66
68
|
fullscreen: {
|
|
@@ -151,6 +153,9 @@ export function loadConfig(notify?: (msg: string, level: "warning" | "info") =>
|
|
|
151
153
|
const raw = readFileSync(path, "utf8");
|
|
152
154
|
const parsed: unknown = JSON.parse(raw);
|
|
153
155
|
const config = deepMerge(DEFAULT_CONFIG, parsed);
|
|
156
|
+
if (typeof config.inlineFooter !== "boolean") {
|
|
157
|
+
config.inlineFooter = DEFAULT_CONFIG.inlineFooter;
|
|
158
|
+
}
|
|
154
159
|
if (config.settingsLanguage !== "en" && config.settingsLanguage !== "zh") {
|
|
155
160
|
config.settingsLanguage = DEFAULT_CONFIG.settingsLanguage;
|
|
156
161
|
}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type ExtensionContext,
|
|
5
5
|
type KeybindingsManager,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
|
|
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
10
|
import {
|
|
@@ -24,12 +24,24 @@ const CURSOR_STYLE_SEQUENCES: Partial<Record<CursorStyle, string>> = {
|
|
|
24
24
|
underline: "\x1b[4 q",
|
|
25
25
|
};
|
|
26
26
|
const DEFAULT_CURSOR_STYLE_SEQUENCE = "\x1b[0 q";
|
|
27
|
+
const EDITOR_FRAME_LEFT_INSET = 2;
|
|
28
|
+
const EDITOR_FRAME_HORIZONTAL_CHROME = EDITOR_FRAME_LEFT_INSET * 2;
|
|
27
29
|
|
|
28
30
|
interface WorkingStatusIndicator {
|
|
29
31
|
renderInBorder(width: number): string;
|
|
30
32
|
renderSpinnerInBorder(width: number): string;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export interface InlineFooterLine {
|
|
36
|
+
left: string;
|
|
37
|
+
right: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface InlineFooterRenderer {
|
|
41
|
+
enabled(): boolean;
|
|
42
|
+
render(width: number): { top: InlineFooterLine; bottom: InlineFooterLine } | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
interface HiddenThinkingLabelComponent {
|
|
34
46
|
children: unknown[];
|
|
35
47
|
setHiddenThinkingLabel(label: string): void;
|
|
@@ -132,11 +144,59 @@ function roundedBorder(
|
|
|
132
144
|
return paint(`${corners[0]}${"─".repeat(Math.max(0, width - 2))}${corners[1]}`);
|
|
133
145
|
}
|
|
134
146
|
|
|
147
|
+
function trimTrailingSpaces(text: string): string {
|
|
148
|
+
const ansiSuffix = text.match(/(?:\x1b\[[0-?]*[ -/]*[@-~])+$/)?.[0] ?? "";
|
|
149
|
+
const content = ansiSuffix ? text.slice(0, -ansiSuffix.length) : text;
|
|
150
|
+
return `${content.replace(/ +$/, "")}${ansiSuffix}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function inlineBorder(
|
|
154
|
+
width: number,
|
|
155
|
+
kind: "top" | "bottom",
|
|
156
|
+
paint: (s: string) => string,
|
|
157
|
+
renderLine: (width: number) => InlineFooterLine | undefined,
|
|
158
|
+
sourceLine?: string,
|
|
159
|
+
indicator?: WorkingStatusIndicator,
|
|
160
|
+
): string {
|
|
161
|
+
if (width < 2) return paint(truncateToWidth(kind === "top" ? "╭╮" : "╰╯", width, ""));
|
|
162
|
+
|
|
163
|
+
const corners = kind === "top" ? (["╭", "╮"] as const) : (["╰", "╯"] as const);
|
|
164
|
+
const contentWidth = width - 2;
|
|
165
|
+
const plain = sourceLine ? stripAnsi(sourceLine) : "";
|
|
166
|
+
const scrollMatch = plain.match(/([↑↓]\s+\d+\s+more)/);
|
|
167
|
+
const right = scrollMatch ? paint(` ${scrollMatch[1]} `) : "";
|
|
168
|
+
let left = paint("─");
|
|
169
|
+
|
|
170
|
+
if (kind === "top" && indicator) {
|
|
171
|
+
const statusBudget = Math.max(1, contentWidth - visibleWidth(right) - 7);
|
|
172
|
+
let status = indicator.renderInBorder(statusBudget);
|
|
173
|
+
if (visibleWidth(status) > statusBudget) status = indicator.renderSpinnerInBorder(statusBudget);
|
|
174
|
+
const leftBudget = Math.max(0, contentWidth - visibleWidth(right) - 1);
|
|
175
|
+
status = truncateToWidth(status, Math.max(0, leftBudget - 4), "");
|
|
176
|
+
if (visibleWidth(status) > 0) left = `${paint("── ")}${status}${paint(" ")}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const lineBudget = Math.max(0, contentWidth - visibleWidth(left) - visibleWidth(right) - 6);
|
|
180
|
+
const line = renderLine(lineBudget);
|
|
181
|
+
if (!line) return roundedBorder(width, kind, paint, sourceLine, indicator);
|
|
182
|
+
const leftContent = trimTrailingSpaces(line.left);
|
|
183
|
+
const rightContent = trimTrailingSpaces(line.right);
|
|
184
|
+
if (visibleWidth(leftContent) === 0 && visibleWidth(rightContent) === 0) {
|
|
185
|
+
return roundedBorder(width, kind, paint, sourceLine, indicator);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const leftCell = leftContent ? ` ${leftContent} ` : "";
|
|
189
|
+
const rightCell = rightContent ? ` ${rightContent} ` : "";
|
|
190
|
+
const fill = Math.max(1, contentWidth - visibleWidth(left) - visibleWidth(leftCell) - visibleWidth(rightCell) - visibleWidth(right) - 1);
|
|
191
|
+
return `${paint(corners[0])}${left}${leftCell}${paint("─".repeat(fill))}${rightCell}${right}${paint("─")}${paint(corners[1])}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
135
194
|
export class OpenTuiEditor extends CustomEditor {
|
|
136
195
|
readonly embedWorkingStatus = true;
|
|
137
196
|
private readonly getRail: () => string;
|
|
138
197
|
private readonly getBorder: (s: string) => string;
|
|
139
198
|
private embeddedWorkingStatusIndicator: WorkingStatusIndicator | undefined;
|
|
199
|
+
private readonly inlineFooter: InlineFooterRenderer | undefined;
|
|
140
200
|
private cursorStyle: CursorStyle;
|
|
141
201
|
private previewHardwareCursor = false;
|
|
142
202
|
|
|
@@ -145,9 +205,11 @@ export class OpenTuiEditor extends CustomEditor {
|
|
|
145
205
|
editorTheme: EditorTheme,
|
|
146
206
|
keybindings: KeybindingsManager,
|
|
147
207
|
cursorStyle: CursorStyle = "block",
|
|
208
|
+
inlineFooter?: InlineFooterRenderer,
|
|
148
209
|
) {
|
|
149
210
|
super(tui, editorTheme, keybindings, { paddingX: 0 });
|
|
150
211
|
this.cursorStyle = cursorStyle;
|
|
212
|
+
this.inlineFooter = inlineFooter;
|
|
151
213
|
configureCursor(tui, cursorStyle);
|
|
152
214
|
// ponytail: route the frame through this.borderColor so Pi can recolor it
|
|
153
215
|
// via updateEditorBorderColor() — bash mode ("! " prefix → green) and
|
|
@@ -161,6 +223,16 @@ export class OpenTuiEditor extends CustomEditor {
|
|
|
161
223
|
super.setPaddingX(0);
|
|
162
224
|
}
|
|
163
225
|
|
|
226
|
+
override handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
227
|
+
if (event.width < EDITOR_FRAME_HORIZONTAL_CHROME) return super.handleMouse(event);
|
|
228
|
+
|
|
229
|
+
return super.handleMouse({
|
|
230
|
+
...event,
|
|
231
|
+
x: event.x - EDITOR_FRAME_LEFT_INSET,
|
|
232
|
+
width: event.width - EDITOR_FRAME_HORIZONTAL_CHROME,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
164
236
|
setWorkingStatusIndicator(indicator: WorkingStatusIndicator | undefined): void {
|
|
165
237
|
this.embeddedWorkingStatusIndicator = indicator;
|
|
166
238
|
this.tui.requestRender();
|
|
@@ -202,17 +274,22 @@ export class OpenTuiEditor extends CustomEditor {
|
|
|
202
274
|
}
|
|
203
275
|
|
|
204
276
|
render(width: number): string[] {
|
|
205
|
-
if (width <
|
|
277
|
+
if (width < EDITOR_FRAME_HORIZONTAL_CHROME) return this.renderBase(width);
|
|
206
278
|
|
|
207
279
|
const rail = this.getRail();
|
|
208
280
|
const borderPaint = this.getBorder;
|
|
209
281
|
// ponytail: 1-char rail + 1-char gap on each side = 4 chars of chrome.
|
|
210
|
-
const innerWidth = Math.max(0, width -
|
|
282
|
+
const innerWidth = Math.max(0, width - EDITOR_FRAME_HORIZONTAL_CHROME);
|
|
211
283
|
const baseLines = this.renderBase(innerWidth);
|
|
212
284
|
const bottomIdx = findBottomBorderIndex(baseLines);
|
|
213
285
|
|
|
214
286
|
const result: string[] = [];
|
|
215
|
-
|
|
287
|
+
const inlineFooter = this.inlineFooter?.enabled() === true;
|
|
288
|
+
const renderInlineLine = (kind: "top" | "bottom", budget: number): InlineFooterLine | undefined =>
|
|
289
|
+
this.inlineFooter?.render(budget)?.[kind];
|
|
290
|
+
result.push(inlineFooter
|
|
291
|
+
? inlineBorder(width, "top", borderPaint, (budget) => renderInlineLine("top", budget), baseLines[0], this.embeddedWorkingStatusIndicator)
|
|
292
|
+
: roundedBorder(width, "top", borderPaint, baseLines[0], this.embeddedWorkingStatusIndicator));
|
|
216
293
|
|
|
217
294
|
for (let i = 1; i < bottomIdx; i++) {
|
|
218
295
|
const line = baseLines[i] ?? "";
|
|
@@ -223,7 +300,9 @@ export class OpenTuiEditor extends CustomEditor {
|
|
|
223
300
|
}
|
|
224
301
|
}
|
|
225
302
|
|
|
226
|
-
result.push(
|
|
303
|
+
result.push(inlineFooter
|
|
304
|
+
? inlineBorder(width, "bottom", borderPaint, (budget) => renderInlineLine("bottom", budget), baseLines[bottomIdx])
|
|
305
|
+
: roundedBorder(width, "bottom", borderPaint, baseLines[bottomIdx]));
|
|
227
306
|
|
|
228
307
|
for (let i = bottomIdx + 1; i < baseLines.length; i++) {
|
|
229
308
|
result.push(baseLines[i]!);
|
|
@@ -238,6 +317,7 @@ export function installEditor(
|
|
|
238
317
|
ctx: ExtensionContext,
|
|
239
318
|
cursorStyle: CursorStyle = "block",
|
|
240
319
|
wheelScrollLines = DEFAULT_FULLSCREEN_WHEEL_SCROLL_LINES,
|
|
320
|
+
inlineFooter?: InlineFooterRenderer,
|
|
241
321
|
) {
|
|
242
322
|
let activeTui: TUI | undefined;
|
|
243
323
|
let activeEditor: OpenTuiEditor | undefined;
|
|
@@ -255,7 +335,7 @@ export function installEditor(
|
|
|
255
335
|
hiddenThinkingTarget = undefined;
|
|
256
336
|
applyFullscreenWheelScrollLines(tui, currentWheelScrollLines);
|
|
257
337
|
previousHardwareCursor = tui.getShowHardwareCursor();
|
|
258
|
-
activeEditor = new OpenTuiEditor(tui, editorTheme, keybindings, currentCursorStyle);
|
|
338
|
+
activeEditor = new OpenTuiEditor(tui, editorTheme, keybindings, currentCursorStyle, inlineFooter);
|
|
259
339
|
return activeEditor;
|
|
260
340
|
});
|
|
261
341
|
return {
|
|
@@ -202,141 +202,241 @@ export interface FooterHooks {
|
|
|
202
202
|
scheduleGitRefresh: () => void;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
interface FooterDataLike {
|
|
206
|
+
getExtensionStatuses(): ReadonlyMap<string, string>;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface InlineFooterLine {
|
|
210
|
+
left: string;
|
|
211
|
+
right: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface InlineFooterLines {
|
|
215
|
+
top: InlineFooterLine;
|
|
216
|
+
bottom: InlineFooterLine;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Split the bottom border content the way `alignRight` splits the plain row:
|
|
221
|
+
* the right block (statistics) survives, the left one (model) shrinks to an
|
|
222
|
+
* ellipsis and only then disappears. Keeps the assembled border inside its
|
|
223
|
+
* budget so editor-side truncation never eats the right corner.
|
|
224
|
+
*/
|
|
225
|
+
function fitInlineLine(left: string, right: string, width: number, theme: Theme): InlineFooterLine {
|
|
226
|
+
const rightWidth = visibleWidth(right);
|
|
227
|
+
if (rightWidth > width) {
|
|
228
|
+
return { left: "", right: truncateToWidth(right, width, theme.fg("dim", "...")) };
|
|
229
|
+
}
|
|
230
|
+
const availableForLeft = width - rightWidth - 1;
|
|
231
|
+
if (availableForLeft <= 0) return { left: "", right };
|
|
232
|
+
return { left: truncateToWidth(left, availableForLeft, theme.fg("dim", "...")), right };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface FooterHandle {
|
|
236
|
+
renderInline(width: number): InlineFooterLines | undefined;
|
|
237
|
+
cleanup(): void;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function fitInlineSegments(
|
|
241
|
+
parts: readonly PrioritizedSegment[],
|
|
242
|
+
width: number,
|
|
243
|
+
theme: Theme,
|
|
244
|
+
): string {
|
|
245
|
+
const separator = theme.fg("dim", " · ");
|
|
246
|
+
const separatorReserve = Math.max(0, parts.length - 1) * Math.max(0, visibleWidth(separator) - 1);
|
|
247
|
+
return fitSegmentsByPriority(
|
|
248
|
+
parts,
|
|
249
|
+
Math.max(0, width - separatorReserve),
|
|
250
|
+
theme.fg("dim", "..."),
|
|
251
|
+
).join(separator);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function renderFooterContent(
|
|
255
|
+
ctx: ExtensionContext,
|
|
256
|
+
getState: () => FooterState,
|
|
257
|
+
getConfig: () => OpenTuiConfig,
|
|
258
|
+
getModelMeta: () => ModelMeta,
|
|
259
|
+
theme: Theme,
|
|
260
|
+
footerData: FooterDataLike,
|
|
261
|
+
width: number,
|
|
262
|
+
): { mainLines: [string, string]; inlineLines: InlineFooterLines; extensionLines: string[] } {
|
|
263
|
+
if (width <= 0) {
|
|
264
|
+
return {
|
|
265
|
+
mainLines: ["", ""],
|
|
266
|
+
inlineLines: {
|
|
267
|
+
top: { left: "", right: "" },
|
|
268
|
+
bottom: { left: "", right: "" },
|
|
269
|
+
},
|
|
270
|
+
extensionLines: [],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const state = getState();
|
|
275
|
+
const config = getConfig();
|
|
276
|
+
const glyphs = resolveGlyphs(config.icons.mode);
|
|
277
|
+
const segments = config.footerSegments;
|
|
278
|
+
const meta = getModelMeta();
|
|
279
|
+
const totals = getUsageTotals(ctx);
|
|
280
|
+
|
|
281
|
+
const leftParts: PrioritizedSegment[] = [];
|
|
282
|
+
const inlineTopLeftParts: PrioritizedSegment[] = [];
|
|
283
|
+
const inlineTopRightParts: PrioritizedSegment[] = [];
|
|
284
|
+
if (segments.cwd) {
|
|
285
|
+
const maxCwd = Math.min(30, Math.max(10, Math.floor(width * 0.4)));
|
|
286
|
+
const cwd = formatCwd(ctx.sessionManager.getCwd());
|
|
287
|
+
const cwdPrefix = `${theme.fg("mdLink", glyphs.cwd)} `;
|
|
288
|
+
const accent = (text: string) => theme.fg("accent", text);
|
|
289
|
+
leftParts.push({
|
|
290
|
+
text: `${cwdPrefix}${accent(truncatePath(cwd, maxCwd))}`,
|
|
291
|
+
compactText: `${cwdPrefix}${accent(truncatePath(basenamePath(cwd), maxCwd))}`,
|
|
292
|
+
priority: 0,
|
|
293
|
+
truncate: (_text, maxWidth, ellipsis) => {
|
|
294
|
+
const pathWidth = maxWidth - visibleWidth(cwdPrefix);
|
|
295
|
+
if (pathWidth <= visibleWidth(ellipsis)) {
|
|
296
|
+
return truncateToWidth(`${cwdPrefix}${accent(basenamePath(cwd))}`, maxWidth, ellipsis);
|
|
297
|
+
}
|
|
298
|
+
return `${cwdPrefix}${accent(truncatePath(basenamePath(cwd), pathWidth))}`;
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
inlineTopRightParts.push(leftParts.at(-1)!);
|
|
302
|
+
}
|
|
303
|
+
if (segments.hostname) {
|
|
304
|
+
const shortHost = shortHostname(osHostname());
|
|
305
|
+
if (shortHost) {
|
|
306
|
+
leftParts.push({
|
|
307
|
+
text: `${theme.fg("dim", glyphs.host)} ${theme.fg("accent", shortHost)}`,
|
|
308
|
+
priority: 1,
|
|
309
|
+
});
|
|
310
|
+
inlineTopRightParts.push(leftParts.at(-1)!);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const sessionName = ctx.sessionManager.getSessionName();
|
|
314
|
+
if (sessionName) {
|
|
315
|
+
const sessionPart: PrioritizedSegment = {
|
|
316
|
+
text: `${theme.fg("dim", glyphs.session)} ${theme.fg("text", truncateToWidth(sessionName, 24, theme.fg("dim", "...")))}`,
|
|
317
|
+
priority: 2,
|
|
318
|
+
};
|
|
319
|
+
if (segments.sessionName) {
|
|
320
|
+
leftParts.push(sessionPart);
|
|
321
|
+
inlineTopLeftParts.push(sessionPart);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const gitSeg = renderGitSegment(theme, state.git, glyphs, segments);
|
|
325
|
+
if (gitSeg) {
|
|
326
|
+
leftParts.push({ text: gitSeg, priority: 3 });
|
|
327
|
+
inlineTopLeftParts.push(leftParts.at(-1)!);
|
|
328
|
+
}
|
|
329
|
+
if (segments.runtime) {
|
|
330
|
+
const runtimeSeg = renderRuntimeSegment(theme, state.runtime, config.icons.mode);
|
|
331
|
+
if (runtimeSeg) {
|
|
332
|
+
leftParts.push({ text: runtimeSeg, priority: 4 });
|
|
333
|
+
inlineTopRightParts.push(leftParts.at(-1)!);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const timerSeg = renderTimerSegment(theme, state, glyphs);
|
|
337
|
+
if (timerSeg) {
|
|
338
|
+
leftParts.push({ text: timerSeg, priority: 1 });
|
|
339
|
+
inlineTopRightParts.push(leftParts.at(-1)!);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// The context bar competes with the left segments for the same row:
|
|
343
|
+
// full bar first, then the compact icon+pct form, then dropped.
|
|
344
|
+
let contextText = "";
|
|
345
|
+
let contextCompact: string | undefined;
|
|
346
|
+
if (segments.context) {
|
|
347
|
+
contextText = renderContextBar(theme, ctx, width, glyphs, config.icons.mode);
|
|
348
|
+
const compact = renderContextCompact(theme, ctx, glyphs);
|
|
349
|
+
if (compact && visibleWidth(compact) < visibleWidth(contextText)) {
|
|
350
|
+
contextCompact = compact;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const allParts: PrioritizedSegment[] = [...leftParts];
|
|
354
|
+
if (contextText) {
|
|
355
|
+
// ponytail: priority 4 = sheds with runtime, before git/timer/cwd.
|
|
356
|
+
allParts.push({ text: contextText, compactText: contextCompact, priority: 4 });
|
|
357
|
+
// Context stays at the far right; cwd is the first item in this group.
|
|
358
|
+
inlineTopRightParts.push({ text: contextText, compactText: contextCompact, priority: 4 });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const fitted = fitSegmentsByPriority(allParts, width, theme.fg("dim", "..."));
|
|
362
|
+
const fittedContext = contextText ? fitted.pop() ?? "" : "";
|
|
363
|
+
const line1 = alignRight(fitted.join(" "), fittedContext, width, theme);
|
|
364
|
+
|
|
365
|
+
const inlineLeftBudget = Math.floor(width * 0.45);
|
|
366
|
+
const inlineTopLeft = fitInlineSegments(inlineTopLeftParts, inlineLeftBudget, theme);
|
|
367
|
+
const inlineRightBudget = Math.max(0, width - visibleWidth(inlineTopLeft) - (inlineTopLeft ? 1 : 0));
|
|
368
|
+
const inlineTop: InlineFooterLine = {
|
|
369
|
+
left: inlineTopLeft,
|
|
370
|
+
right: fitInlineSegments(inlineTopRightParts, inlineRightBudget, theme),
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const modelParts: string[] = [];
|
|
374
|
+
modelParts.push(theme.fg("mdLink", glyphs.model));
|
|
375
|
+
if (meta.provider && meta.provider !== "Unknown") {
|
|
376
|
+
modelParts.push(theme.fg(providerColor(ctx.model?.provider ?? "none"), meta.provider));
|
|
377
|
+
}
|
|
378
|
+
modelParts.push(theme.fg("text", meta.model));
|
|
379
|
+
if (meta.effort && meta.effort !== "off") {
|
|
380
|
+
modelParts.push(theme.fg(effortColor(meta.effort), `${glyphs.thinking} ${meta.effort}`));
|
|
381
|
+
}
|
|
382
|
+
const modelBlock = modelParts.join(theme.fg("dim", " · "));
|
|
383
|
+
const statsBlock = renderStatsBlock(theme, totals, glyphs, segments);
|
|
384
|
+
const inlineBottom = fitInlineLine(modelBlock, statsBlock, width, theme);
|
|
385
|
+
const line2 = alignRight(inlineBottom.left, inlineBottom.right, width, theme);
|
|
386
|
+
const mainLines: [string, string] = [line1, line2]
|
|
387
|
+
.map((line) => truncateToWidth(line, width, theme.fg("dim", "..."))) as [string, string];
|
|
388
|
+
const extensionLines = segments.extensionStatuses
|
|
389
|
+
? renderExtensionStatusLines(theme, footerData.getExtensionStatuses(), glyphs, width)
|
|
390
|
+
: [];
|
|
391
|
+
return { mainLines, inlineLines: { top: inlineTop, bottom: inlineBottom }, extensionLines };
|
|
392
|
+
}
|
|
393
|
+
|
|
205
394
|
export function installFooter(
|
|
206
395
|
ctx: ExtensionContext,
|
|
207
396
|
getState: () => FooterState,
|
|
208
397
|
getConfig: () => OpenTuiConfig,
|
|
209
398
|
getModelMeta: () => ModelMeta,
|
|
210
399
|
hooks: FooterHooks,
|
|
211
|
-
):
|
|
400
|
+
): FooterHandle {
|
|
401
|
+
let renderInline: (width: number) => InlineFooterLines | undefined = () => undefined;
|
|
402
|
+
|
|
212
403
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
213
404
|
hooks.setRequestRender(() => tui.requestRender());
|
|
214
405
|
const unsubBranch = footerData.onBranchChange(() => {
|
|
215
406
|
hooks.scheduleGitRefresh();
|
|
216
407
|
tui.requestRender();
|
|
217
408
|
});
|
|
409
|
+
const getContent = (width: number) => renderFooterContent(
|
|
410
|
+
ctx,
|
|
411
|
+
getState,
|
|
412
|
+
getConfig,
|
|
413
|
+
getModelMeta,
|
|
414
|
+
theme,
|
|
415
|
+
footerData,
|
|
416
|
+
width,
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
renderInline = (width) => getContent(width).inlineLines;
|
|
218
420
|
|
|
219
421
|
return {
|
|
220
422
|
dispose() {
|
|
221
423
|
unsubBranch();
|
|
222
424
|
hooks.setRequestRender(undefined);
|
|
425
|
+
renderInline = () => undefined;
|
|
223
426
|
},
|
|
224
427
|
invalidate() {},
|
|
225
428
|
render(width: number): string[] {
|
|
226
429
|
if (width <= 0) return [""];
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
const glyphs = resolveGlyphs(config.icons.mode);
|
|
230
|
-
const segments = config.footerSegments;
|
|
231
|
-
const meta = getModelMeta();
|
|
232
|
-
|
|
233
|
-
const totals = getUsageTotals(ctx);
|
|
234
|
-
|
|
235
|
-
const leftParts: PrioritizedSegment[] = [];
|
|
236
|
-
if (segments.cwd) {
|
|
237
|
-
const maxCwd = Math.min(30, Math.max(10, Math.floor(width * 0.4)));
|
|
238
|
-
const cwd = formatCwd(ctx.sessionManager.getCwd());
|
|
239
|
-
const cwdPrefix = `${theme.fg("mdLink", glyphs.cwd)} `;
|
|
240
|
-
const accent = (text: string) => theme.fg("accent", text);
|
|
241
|
-
leftParts.push({
|
|
242
|
-
text: `${cwdPrefix}${accent(truncatePath(cwd, maxCwd))}`,
|
|
243
|
-
compactText: `${cwdPrefix}${accent(truncatePath(basenamePath(cwd), maxCwd))}`,
|
|
244
|
-
priority: 0,
|
|
245
|
-
truncate: (_text, maxWidth, ellipsis) => {
|
|
246
|
-
const pathWidth = maxWidth - visibleWidth(cwdPrefix);
|
|
247
|
-
if (pathWidth <= visibleWidth(ellipsis)) {
|
|
248
|
-
return truncateToWidth(`${cwdPrefix}${accent(basenamePath(cwd))}`, maxWidth, ellipsis);
|
|
249
|
-
}
|
|
250
|
-
return `${cwdPrefix}${accent(truncatePath(basenamePath(cwd), pathWidth))}`;
|
|
251
|
-
},
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
if (segments.hostname) {
|
|
255
|
-
const shortHost = shortHostname(osHostname());
|
|
256
|
-
if (shortHost) {
|
|
257
|
-
leftParts.push({
|
|
258
|
-
text: `${theme.fg("dim", glyphs.host)} ${theme.fg("accent", shortHost)}`,
|
|
259
|
-
priority: 1,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if (segments.sessionName) {
|
|
264
|
-
const sessionName = ctx.sessionManager.getSessionName();
|
|
265
|
-
if (sessionName) {
|
|
266
|
-
leftParts.push({
|
|
267
|
-
text: `${theme.fg("dim", glyphs.session)} ${theme.fg("text", truncateToWidth(sessionName, 24, theme.fg("dim", "...")))}`,
|
|
268
|
-
priority: 2,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
const gitSeg = renderGitSegment(theme, state.git, glyphs, segments);
|
|
273
|
-
if (gitSeg) leftParts.push({ text: gitSeg, priority: 3 });
|
|
274
|
-
if (segments.runtime) {
|
|
275
|
-
const runtimeSeg = renderRuntimeSegment(theme, state.runtime, config.icons.mode);
|
|
276
|
-
if (runtimeSeg) leftParts.push({ text: runtimeSeg, priority: 4 });
|
|
277
|
-
}
|
|
278
|
-
const timerSeg = renderTimerSegment(theme, state, glyphs);
|
|
279
|
-
if (timerSeg) leftParts.push({ text: timerSeg, priority: 1 });
|
|
280
|
-
|
|
281
|
-
// The context bar competes with the left segments for the same row:
|
|
282
|
-
// full bar first, then the compact icon+pct form, then dropped.
|
|
283
|
-
let contextText = "";
|
|
284
|
-
let contextCompact: string | undefined;
|
|
285
|
-
if (segments.context) {
|
|
286
|
-
contextText = renderContextBar(theme, ctx, width, glyphs, config.icons.mode);
|
|
287
|
-
const compact = renderContextCompact(theme, ctx, glyphs);
|
|
288
|
-
if (compact && visibleWidth(compact) < visibleWidth(contextText)) {
|
|
289
|
-
contextCompact = compact;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
const allParts: PrioritizedSegment[] = [...leftParts];
|
|
293
|
-
if (contextText) {
|
|
294
|
-
// ponytail: priority 4 = sheds with runtime, before git/timer/cwd.
|
|
295
|
-
allParts.push({ text: contextText, compactText: contextCompact, priority: 4 });
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const fitted = fitSegmentsByPriority(allParts, width, theme.fg("dim", "..."));
|
|
299
|
-
const fittedContext = contextText ? fitted.pop() ?? "" : "";
|
|
300
|
-
const line1 = alignRight(fitted.join(" "), fittedContext, width, theme);
|
|
301
|
-
|
|
302
|
-
const modelParts: string[] = [];
|
|
303
|
-
modelParts.push(theme.fg("mdLink", glyphs.model));
|
|
304
|
-
if (meta.provider && meta.provider !== "Unknown") {
|
|
305
|
-
modelParts.push(theme.fg(providerColor(ctx.model?.provider ?? "none"), meta.provider));
|
|
306
|
-
}
|
|
307
|
-
modelParts.push(theme.fg("text", meta.model));
|
|
308
|
-
if (meta.effort && meta.effort !== "off") {
|
|
309
|
-
modelParts.push(theme.fg(effortColor(meta.effort), `${glyphs.thinking} ${meta.effort}`));
|
|
310
|
-
}
|
|
311
|
-
const modelBlock = modelParts.join(theme.fg("dim", " · "));
|
|
312
|
-
|
|
313
|
-
const statsBlock = renderStatsBlock(
|
|
314
|
-
theme,
|
|
315
|
-
totals,
|
|
316
|
-
glyphs,
|
|
317
|
-
segments,
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
const line2 = alignRight(modelBlock, statsBlock, width, theme);
|
|
321
|
-
|
|
322
|
-
const mainLines = [line1, line2]
|
|
323
|
-
.map((line) => truncateToWidth(line, width, theme.fg("dim", "...")));
|
|
324
|
-
return segments.extensionStatuses
|
|
325
|
-
? [
|
|
326
|
-
...mainLines,
|
|
327
|
-
...renderExtensionStatusLines(
|
|
328
|
-
theme,
|
|
329
|
-
footerData.getExtensionStatuses(),
|
|
330
|
-
glyphs,
|
|
331
|
-
width,
|
|
332
|
-
),
|
|
333
|
-
]
|
|
334
|
-
: mainLines;
|
|
430
|
+
const { mainLines, extensionLines } = getContent(width);
|
|
431
|
+
return getConfig().inlineFooter ? extensionLines : [...mainLines, ...extensionLines];
|
|
335
432
|
},
|
|
336
433
|
};
|
|
337
434
|
});
|
|
338
435
|
|
|
339
|
-
return
|
|
340
|
-
|
|
436
|
+
return {
|
|
437
|
+
renderInline: (width) => renderInline(width),
|
|
438
|
+
cleanup() {
|
|
439
|
+
ctx.ui.setFooter(undefined);
|
|
440
|
+
},
|
|
341
441
|
};
|
|
342
442
|
}
|
|
@@ -131,7 +131,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
131
131
|
}
|
|
132
132
|
if (!active) {
|
|
133
133
|
cleanupHeader = installHeader(pi, ctx);
|
|
134
|
-
|
|
134
|
+
const footer = installFooter(
|
|
135
135
|
ctx,
|
|
136
136
|
() => state,
|
|
137
137
|
() => config,
|
|
@@ -145,7 +145,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
145
145
|
},
|
|
146
146
|
},
|
|
147
147
|
);
|
|
148
|
-
|
|
148
|
+
cleanupFooter = footer.cleanup;
|
|
149
|
+
editor = installEditor(
|
|
150
|
+
pi,
|
|
151
|
+
ctx,
|
|
152
|
+
config.cursorStyle,
|
|
153
|
+
config.fullscreen.wheelScrollLines,
|
|
154
|
+
{
|
|
155
|
+
enabled: () => config.inlineFooter,
|
|
156
|
+
render: footer.renderInline,
|
|
157
|
+
},
|
|
158
|
+
);
|
|
149
159
|
active = true;
|
|
150
160
|
}
|
|
151
161
|
};
|
|
@@ -33,6 +33,7 @@ const COPY = {
|
|
|
33
33
|
hint: "Tab/Shift+Tab/←/→: tabs · ↑/↓: move · Enter/Space: change · Enter on wheel speed: type 1-10 · Esc/q: close",
|
|
34
34
|
labels: {
|
|
35
35
|
enabled: "Enabled",
|
|
36
|
+
inlineFooter: "Inline footer",
|
|
36
37
|
thinkingPeek: "Thinking peek",
|
|
37
38
|
language: "Language",
|
|
38
39
|
wheelScrollLines: "Mouse wheel speed",
|
|
@@ -72,6 +73,7 @@ const COPY = {
|
|
|
72
73
|
hint: "Tab/Shift+Tab/←/→:切页 · ↑/↓:移动 · Enter/Space:更改 · 滚轮速度项 Enter 输入 1-10 · Esc/q:关闭",
|
|
73
74
|
labels: {
|
|
74
75
|
enabled: "启用",
|
|
76
|
+
inlineFooter: "内联底栏",
|
|
75
77
|
thinkingPeek: "思考预览",
|
|
76
78
|
language: "语言",
|
|
77
79
|
wheelScrollLines: "鼠标滚轮速度",
|
|
@@ -185,6 +187,7 @@ function buildFeaturesItems(config: OpenTuiConfig, copy: SettingsCopy): SettingI
|
|
|
185
187
|
currentValue: copy.values.wheelLines(config.fullscreen.wheelScrollLines),
|
|
186
188
|
},
|
|
187
189
|
{ id: "thinkingPeek", label: copy.labels.thinkingPeek, currentValue: formatThinkingPeekLines(config.thinkingPeek.lines, copy) },
|
|
190
|
+
{ id: "inlineFooter", label: copy.labels.inlineFooter, currentValue: flag(config.inlineFooter) },
|
|
188
191
|
];
|
|
189
192
|
}
|
|
190
193
|
|
|
@@ -245,6 +248,7 @@ function handleSettingChange(
|
|
|
245
248
|
): OpenTuiConfig {
|
|
246
249
|
if (tab === "features") {
|
|
247
250
|
if (itemId === "enabled") return toggleEnabled(config);
|
|
251
|
+
if (itemId === "inlineFooter") return { ...config, inlineFooter: !config.inlineFooter };
|
|
248
252
|
if (itemId === "settingsLanguage") return toggleLanguage(config);
|
|
249
253
|
if (itemId === "thinkingPeek") return cycleThinkingPeek(config);
|
|
250
254
|
}
|
|
@@ -43,23 +43,28 @@ export function truncateBranch(branch: string, maxLen: number): string {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function truncatePath(path: string, maxLen: number): string {
|
|
46
|
-
|
|
46
|
+
// `maxLen` is a terminal-column budget, so every comparison here must use
|
|
47
|
+
// display width. Comparing String.length let wide (CJK) paths stay wider than
|
|
48
|
+
// the budget: for a basename of 4 characters but 8 columns, `truncatePath`
|
|
49
|
+
// returned it unchanged, so fitSegmentsByPriority's `while (totalW() > maxW)`
|
|
50
|
+
// loop never made progress and livelocked the renderer at 100% CPU.
|
|
51
|
+
if (visibleWidth(path) <= maxLen) return path;
|
|
47
52
|
if (maxLen <= 3) return "...".slice(0, maxLen);
|
|
48
53
|
const sepChar = path.includes("/") ? "/" : "\\";
|
|
49
54
|
const parts = path.split(/[\\/]/);
|
|
50
|
-
if (parts.length <= 2) return path
|
|
55
|
+
if (parts.length <= 2) return truncateToWidth(path, maxLen, "...");
|
|
51
56
|
// Keep first segment (e.g. ~) and as many trailing segments as fit.
|
|
52
57
|
const tail: string[] = [];
|
|
53
58
|
let tailLen = 0;
|
|
54
59
|
for (let i = parts.length - 1; i >= 1; i--) {
|
|
55
60
|
const seg = parts[i]!;
|
|
56
|
-
if (tailLen + seg
|
|
61
|
+
if (tailLen + visibleWidth(seg) + 4 > maxLen) break;
|
|
57
62
|
tail.unshift(seg);
|
|
58
|
-
tailLen += seg
|
|
63
|
+
tailLen += visibleWidth(seg) + 1;
|
|
59
64
|
}
|
|
60
65
|
const head = parts[0]!;
|
|
61
66
|
const result = `${head}${sepChar}...${sepChar}${tail.join(sepChar)}`;
|
|
62
|
-
return result
|
|
67
|
+
return visibleWidth(result) > maxLen ? truncateToWidth(result, maxLen, "...") : result;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
export function finiteOrZero(value: unknown): number {
|
|
@@ -158,7 +163,25 @@ export function fitSegmentsByPriority(
|
|
|
158
163
|
if (totalW() <= maxW) break;
|
|
159
164
|
}
|
|
160
165
|
}
|
|
166
|
+
// A segment whose `truncate` cannot actually shrink would otherwise spin here
|
|
167
|
+
// forever and block the render timer. Require each pass to make progress, and
|
|
168
|
+
// drop the worst segment when it does not, so layout always terminates.
|
|
169
|
+
let lastTotal = Infinity;
|
|
161
170
|
while (totalW() > maxW) {
|
|
171
|
+
const nowTotal = totalW();
|
|
172
|
+
if (nowTotal >= lastTotal) {
|
|
173
|
+
let worst = -1;
|
|
174
|
+
for (let i = 0; i < items.length; i++) {
|
|
175
|
+
if (items[i].text !== "" && (worst === -1 || items[i].priority < items[worst].priority)) {
|
|
176
|
+
worst = i;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (worst === -1) break;
|
|
180
|
+
items[worst].text = "";
|
|
181
|
+
items[worst].w = 0;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
lastTotal = nowTotal;
|
|
162
185
|
let target = -1;
|
|
163
186
|
for (let i = 0; i < items.length; i++) {
|
|
164
187
|
if (items[i].text !== "" && (target === -1 || items[i].priority < items[target].priority)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-open-tui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "A polished TUI for Pi coding agent: animated logo header, Starship-style footer, rounded editor with model metadata, and prompt-box user messages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"image": "https://raw.githubusercontent.com/OldSuns/pi-open-tui/main/assets/preview_dashboard_1.png"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
39
|
-
"@earendil-works/pi-ai": ">=0.
|
|
40
|
-
"@earendil-works/pi-tui": ">=0.
|
|
38
|
+
"@earendil-works/pi-coding-agent": ">=0.85",
|
|
39
|
+
"@earendil-works/pi-ai": ">=0.85",
|
|
40
|
+
"@earendil-works/pi-tui": ">=0.85"
|
|
41
41
|
},
|
|
42
42
|
"peerDependenciesMeta": {
|
|
43
43
|
"@earendil-works/pi-coding-agent": {
|