pi-tidy-footer 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +121 -0
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/extensions/pi-tidy-footer.ts +735 -0
- package/package.json +32 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
[中文](#中文)
|
|
4
|
+
|
|
5
|
+
## [0.4.0] - 2026-07-22
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `/se` command: show or set extension status sort order, persisted across sessions
|
|
10
|
+
- Extension statuses now sorted by user-defined order instead of alphabetically; unlisted extensions appear at the end
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Ponytail and Caveman status formatting switched from regex reconstruction to replace-based approach, preserving native colours
|
|
15
|
+
- Ponytail status: removed ⚡ emoji, level text lowercased, 🐴 emoji kept
|
|
16
|
+
- Caveman status: label capitalised to `Caveman:`, level text lowercased
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- `writePersistedEnabled` no longer overwrites other state keys (balance thresholds, extension order)
|
|
21
|
+
- Duplicate `clearTimeout` call in `dispose()` removed
|
|
22
|
+
- Unreachable fallback in `formatToolActivity` removed
|
|
23
|
+
- `!== null` → `!= null` in context usage check (handles undefined correctly)
|
|
24
|
+
- `fetchBalance` for DeepSeek: added null guard before `parseFloat` to avoid implicit exception handling
|
|
25
|
+
- Removed redundant `existsSync` calls in `readPersistedEnabled` and `readExtensionOrder`
|
|
26
|
+
|
|
27
|
+
## [0.3.0] — 2026-07-19
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- Account balance after the model name on line 2 (`¤¥` format, DeepSeek and Kimi), auto-refreshed after each turn (30s cooldown); yellow under warn threshold, red under alert threshold
|
|
32
|
+
- `/bt <warn> <alert>` command to set custom yellow/red balance thresholds, persisted, warn > alert required
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Tool activity indicator simplified to show only the running tool with a brief hold for fast tools; removed thinking and completed states
|
|
37
|
+
- README: repositioned cost removal as "from the left stats area, keeping it token-focused"; added "Tweaks to other extensions" section
|
|
38
|
+
|
|
39
|
+
## [0.2.0] — 2026-07-19
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- Git status tokens next to the branch name on line 1: `⇡` ahead, `⇣` behind, `+` staged, `~` modified, `?` untracked, `!` conflicts; hidden when clean; fetched asynchronously (250ms debounce + 30s polling + refresh after tool execution)
|
|
44
|
+
- Tool activity indicator before the model name on line 2: running tool, streaming (thinking), last completed tool; hidden when idle
|
|
45
|
+
- Colored indicator: accent while running, success when completed, dim while thinking
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- Thinking level stuck at `off`: now reads the latest `thinking_level_change` session entry, correctly following Shift+Tab (same data source as the built-in footer)
|
|
50
|
+
- MCP extension status color: preserves the original accent color with a dimmed `MCP:` label, no longer loses color on theme changes
|
|
51
|
+
- `/tf` command description corrected to "Toggle pi-tidy-footer on / off"; unified to standard hyphens in UI text
|
|
52
|
+
|
|
53
|
+
## [0.1.0] — 2026-07-18
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- First release
|
|
58
|
+
- `/tf` command to toggle footer without cost ($)
|
|
59
|
+
- Persistent toggle state across sessions
|
|
60
|
+
- Preserves all other built-in footer info
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 中文
|
|
65
|
+
|
|
66
|
+
## [0.4.0] - 2026-07-22
|
|
67
|
+
|
|
68
|
+
### 新增
|
|
69
|
+
|
|
70
|
+
- `/se` 命令:查看或设置扩展状态排序,持久化存储
|
|
71
|
+
- 扩展状态按用户自定义顺序排列,未列出的扩展自动排至末尾
|
|
72
|
+
|
|
73
|
+
### 变更
|
|
74
|
+
|
|
75
|
+
- Ponytail 和 Caveman 状态格式化从正则重组改为 replace 方式,保留原生颜色
|
|
76
|
+
- Ponytail 状态:移除 ⚡ 符号,级别字母全小写,🐴 符号保留
|
|
77
|
+
- Caveman 状态:标签首字母大写为 `Caveman:`,级别字母全小写
|
|
78
|
+
|
|
79
|
+
### 修复
|
|
80
|
+
|
|
81
|
+
- `writePersistedEnabled` 不再覆盖其他状态键(余额阈值、扩展顺序)
|
|
82
|
+
- 删除 `dispose()` 中的重复 `clearTimeout` 调用
|
|
83
|
+
- 删除 `formatToolActivity` 中不可达的兜底逻辑
|
|
84
|
+
- 上下文用量检查 `!== null` → `!= null`(正确处置 undefined)
|
|
85
|
+
- DeepSeek 余额查询:`parseFloat` 前添加空值保护,避免隐式依赖异常流
|
|
86
|
+
- 删除 `readPersistedEnabled` 和 `readExtensionOrder` 中的冗余 `existsSync` 调用
|
|
87
|
+
|
|
88
|
+
## [0.3.0] — 2026-07-19
|
|
89
|
+
|
|
90
|
+
### 新增
|
|
91
|
+
|
|
92
|
+
- 第 2 行模型名后显示当前 provider 的账户余额(`¤¥` 格式,支持 DeepSeek 和 Kimi),每个 turn 结束后自动刷新(30s 冷却);余额低于警告线变黄、低于红线变红
|
|
93
|
+
- `/bt <warn> <alert>` 命令:自定义余额黄/红阈值,持久化存储,warn 必须大于 alert
|
|
94
|
+
|
|
95
|
+
### 变更
|
|
96
|
+
|
|
97
|
+
- 工具活动指示精简为仅显示运行中的工具,快工具至少保留 150ms 驻留;移除 thinking 和已完成状态
|
|
98
|
+
- README:去除费用表述改为"从左侧统计模块移除 $0.604,让统计专注 token";新增"其他扩展微调"小节
|
|
99
|
+
|
|
100
|
+
## [0.2.0] — 2026-07-19
|
|
101
|
+
|
|
102
|
+
### 新增
|
|
103
|
+
|
|
104
|
+
- 第 1 行分支名旁显示 Git 状态标记:`⇡` 领先、`⇣` 落后、`+` 已暂存、`~` 已修改、`?` 未跟踪、`!` 冲突;干净仓库不显示;异步获取(250ms 防抖 + 30s 轮询 + 工具结束后刷新)
|
|
105
|
+
- 第 2 行模型名前显示工具活动指示:运行中工具、流式生成、刚完成的工具;空闲时不显示
|
|
106
|
+
- 工具指示着色:运行中 accent、刚完成 success、thinking dim
|
|
107
|
+
|
|
108
|
+
### 修复
|
|
109
|
+
|
|
110
|
+
- thinking level 始终显示 off:改为从会话条目中获取最近一次的 `thinking_level_change`,正确跟随 Shift+Tab(数据源与内置页脚等价)
|
|
111
|
+
- MCP 扩展状态颜色:保留原始强调色值,`MCP:` 前缀置灰,主题切换不再丢色
|
|
112
|
+
- `/tf` 命令描述修正为 "Toggle pi-tidy-footer on / off";文案统一使用普通连字符
|
|
113
|
+
|
|
114
|
+
## [0.1.0] — 2026-07-18
|
|
115
|
+
|
|
116
|
+
### 新增
|
|
117
|
+
|
|
118
|
+
- 首个正式版本
|
|
119
|
+
- `/tf` 命令切换页脚,去除费用 ($) 显示
|
|
120
|
+
- 切换状态跨会话持久化
|
|
121
|
+
- 保留内置页脚全部其他信息
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 eriiic7z
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# pi-tidy-footer
|
|
2
|
+
|
|
3
|
+
[中文](#中文)
|
|
4
|
+
|
|
5
|
+
Tidy footer for Pi coding agent — native layout, layered with new features: extension sort order, token balance display (color-coded warnings with configurable thresholds), git status tokens, and tool activity indicators; minor display tweaks for selected third-party extensions, keeping it clean.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi install npm:pi-tidy-footer
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Account balance** — Shows DeepSeek or Kimi account balance after the model name (`¤¥` format), yellow below warn threshold, red below alert threshold, both configurable
|
|
16
|
+
- **Extension sort order** — Custom sort order for extension statuses, persisted across sessions; run empty command to view the current order prompt
|
|
17
|
+
- **Git status tokens** — Repo state next to the branch name: `⇡` ahead, `⇣` behind, `+` staged, `~` modified, `?` untracked, `!` conflicts; hidden when clean
|
|
18
|
+
- **Tool activity indicator** — Shows the running tool (highlighted) before the model name with brief hold for fast tools; hidden when idle
|
|
19
|
+
- **Hide per-session cost** — Removes `$0.604` from the left stats area, keeping it token-focused
|
|
20
|
+
- **Toggle & persistence** — `/tf` toggles back to the original footer anytime; all settings survive restarts
|
|
21
|
+
- **Mostly native** — Everything else (model, token stats, context %, git branch) kept intact
|
|
22
|
+
|
|
23
|
+
## Display tweaks for other extensions
|
|
24
|
+
|
|
25
|
+
- **pi-mcp-adapter** — Dimmed the `MCP:` label while preserving its original accent color, keeping the status line visually coherent across theme changes
|
|
26
|
+
- **ponytail** — Removed ⚡ emoji from status, level text lowercased, 🐴 emoji kept
|
|
27
|
+
- **caveman** — Label capitalised to `Caveman:`, level text lowercased
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
| Command | Description |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `/tf` | Toggle pi-tidy-footer (on/off) |
|
|
34
|
+
| `/bt <warn> <alert>` | Set balance yellow/red thresholds (warn > alert required) |
|
|
35
|
+
| `/se` | Show or set extension status sort order |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 中文
|
|
40
|
+
|
|
41
|
+
为 Pi coding agent 打造的整洁页脚 — 沿用原生布局,叠加了新的功能:插件自定义排序、token 余额显示(颜色警告、自定义警告阈值)、Git 状态标记、工具活动指示;对部分第三方插件进行名称显示微调,保持简洁。
|
|
42
|
+
|
|
43
|
+
## 安装
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pi install npm:pi-tidy-footer
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 功能
|
|
50
|
+
|
|
51
|
+
- **账户余额** — 模型名后显示 DeepSeek / Kimi 账户余额(`¤¥` 格式),低于预警值(warn)变黄,低于警报值(alert)变红,阈值可自定义
|
|
52
|
+
- **插件自定义排序** — 扩展状态按自定义顺序排列,持久化存储;可使用空命令查看排序字段提示
|
|
53
|
+
- **Git 状态标记** — 分支名旁显示仓库状态:`⇡` 领先、`⇣` 落后、`+` 已暂存、`~` 已修改、`?` 未跟踪、`!` 冲突;干净仓库不显示
|
|
54
|
+
- **工具活动指示** — 模型名前实时显示正在运行的工具(高亮),快工具至少有短暂停留;空闲时不显示
|
|
55
|
+
- **去除会话费用显示** — 从左侧统计模块移除 `$0.604`,让统计专注 token
|
|
56
|
+
- **开关与持久化** — `/tf` 随时切回原始页脚;所有设置重启后保持不变
|
|
57
|
+
- **基本保留原生样式** — 其余所有内置页脚信息(模型、Token 统计、上下文百分比、Git 分支)保持不变
|
|
58
|
+
|
|
59
|
+
## 其他扩展的显示微调
|
|
60
|
+
|
|
61
|
+
- **pi-mcp-adapter** — `MCP:` 标签置灰、保留原始强调色值,使状态行在不同主题下保持视觉协调
|
|
62
|
+
- **ponytail** — 状态中移除 ⚡ 符号,级别字母全小写,🐴 符号保留
|
|
63
|
+
- **caveman** — 标签首字母大写为 `Caveman:`,级别字母全小写
|
|
64
|
+
|
|
65
|
+
## 命令
|
|
66
|
+
|
|
67
|
+
| 命令 | 说明 |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `/tf` | 切换 pi-tidy-footer 页脚(开/关) |
|
|
70
|
+
| `/bt <warn> <alert>` | 设置余额黄/红阈值(warn > alert) |
|
|
71
|
+
| `/se` | 查看或设置扩展状态排列顺序 |
|
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-tidy-footer — removes cost ($) from the built-in Pi footer.
|
|
3
|
+
* Toggle with /tf. State persists across restarts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
9
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
10
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
11
|
+
|
|
12
|
+
const AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json");
|
|
13
|
+
|
|
14
|
+
/* ------------------------------------------------------------------ */
|
|
15
|
+
/* persistence */
|
|
16
|
+
/* ------------------------------------------------------------------ */
|
|
17
|
+
|
|
18
|
+
const STATE_DIR = join(homedir(), ".pi", "agent", "extensions");
|
|
19
|
+
const STATE_FILE = join(STATE_DIR, "pi-tidy-footer-state.json");
|
|
20
|
+
|
|
21
|
+
function readPersistedEnabled(): boolean {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(STATE_FILE, "utf-8")).enabled === true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readThresholds(): { warn: number; alert: number } {
|
|
30
|
+
try {
|
|
31
|
+
const raw = JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
|
32
|
+
let w = Number(raw?.balanceThresholdWarn);
|
|
33
|
+
let a = Number(raw?.balanceThresholdAlert);
|
|
34
|
+
if (!Number.isFinite(w) || !Number.isFinite(a) || w <= a) {
|
|
35
|
+
w = 30;
|
|
36
|
+
a = 10;
|
|
37
|
+
}
|
|
38
|
+
return { warn: w, alert: a };
|
|
39
|
+
} catch {
|
|
40
|
+
return { warn: 30, alert: 10 };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeThresholds(warn: number, alert: number): void {
|
|
45
|
+
try {
|
|
46
|
+
const prev: any = existsSync(STATE_FILE)
|
|
47
|
+
? JSON.parse(readFileSync(STATE_FILE, "utf-8"))
|
|
48
|
+
: {};
|
|
49
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
50
|
+
writeFileSync(
|
|
51
|
+
STATE_FILE,
|
|
52
|
+
JSON.stringify({
|
|
53
|
+
...prev,
|
|
54
|
+
balanceThresholdWarn: warn,
|
|
55
|
+
balanceThresholdAlert: alert,
|
|
56
|
+
}),
|
|
57
|
+
"utf-8",
|
|
58
|
+
);
|
|
59
|
+
} catch {
|
|
60
|
+
/* no-op */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writePersistedEnabled(enabled: boolean): void {
|
|
65
|
+
try {
|
|
66
|
+
const prev: any = existsSync(STATE_FILE)
|
|
67
|
+
? JSON.parse(readFileSync(STATE_FILE, "utf-8"))
|
|
68
|
+
: {};
|
|
69
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
70
|
+
writeFileSync(STATE_FILE, JSON.stringify({ ...prev, enabled }), "utf-8");
|
|
71
|
+
} catch {
|
|
72
|
+
/* no-op */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readExtensionOrder(): string[] {
|
|
77
|
+
const fallback = ["caveman", "ponytail", "mcp", "pi-lens-lsp"];
|
|
78
|
+
try {
|
|
79
|
+
const raw = JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
|
80
|
+
return Array.isArray(raw?.extensionOrder) ? raw.extensionOrder : fallback;
|
|
81
|
+
} catch {
|
|
82
|
+
return fallback;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeExtensionOrder(order: string[]): void {
|
|
87
|
+
try {
|
|
88
|
+
const prev: any = existsSync(STATE_FILE)
|
|
89
|
+
? JSON.parse(readFileSync(STATE_FILE, "utf-8"))
|
|
90
|
+
: {};
|
|
91
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
92
|
+
writeFileSync(
|
|
93
|
+
STATE_FILE,
|
|
94
|
+
JSON.stringify({ ...prev, extensionOrder: order }),
|
|
95
|
+
"utf-8",
|
|
96
|
+
);
|
|
97
|
+
} catch {
|
|
98
|
+
/* no-op */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getApiKey(provider: string): string | undefined {
|
|
103
|
+
try {
|
|
104
|
+
return (JSON.parse(readFileSync(AUTH_PATH, "utf-8")) as any)[provider]?.key;
|
|
105
|
+
} catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function fetchBalance(
|
|
111
|
+
provider: string,
|
|
112
|
+
key: string,
|
|
113
|
+
): Promise<string | undefined> {
|
|
114
|
+
const url =
|
|
115
|
+
provider === "deepseek"
|
|
116
|
+
? "https://api.deepseek.com/user/balance"
|
|
117
|
+
: provider === "moonshotai-cn"
|
|
118
|
+
? "https://api.moonshot.cn/v1/users/me/balance"
|
|
119
|
+
: null;
|
|
120
|
+
if (!url) return undefined;
|
|
121
|
+
const resp = await fetch(url, {
|
|
122
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
123
|
+
});
|
|
124
|
+
if (!resp.ok) return undefined;
|
|
125
|
+
const data = (await resp.json()) as any;
|
|
126
|
+
if (provider === "deepseek") {
|
|
127
|
+
const v = data?.balance_infos?.[0]?.total_balance;
|
|
128
|
+
return v != null ? parseFloat(v).toFixed(2) : undefined;
|
|
129
|
+
}
|
|
130
|
+
if (provider === "moonshotai-cn") {
|
|
131
|
+
const bal = data?.data;
|
|
132
|
+
const cash = Number.parseFloat(bal?.cash_balance ?? "");
|
|
133
|
+
const voucher = Number.parseFloat(bal?.voucher_balance ?? "");
|
|
134
|
+
const available = Number.parseFloat(bal?.available_balance ?? "");
|
|
135
|
+
const raw =
|
|
136
|
+
Number.isNaN(cash) || Number.isNaN(voucher)
|
|
137
|
+
? Number.isNaN(available)
|
|
138
|
+
? undefined
|
|
139
|
+
: available
|
|
140
|
+
: cash + voucher;
|
|
141
|
+
return raw === undefined ? undefined : raw.toFixed(2);
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* ------------------------------------------------------------------ */
|
|
147
|
+
/* helpers */
|
|
148
|
+
/* ------------------------------------------------------------------ */
|
|
149
|
+
|
|
150
|
+
function fmtTok(n: number): string {
|
|
151
|
+
if (n < 1000) return `${n}`;
|
|
152
|
+
if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
|
|
153
|
+
if (n < 1_000_000) return `${Math.round(n / 1000)}k`;
|
|
154
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function fmtCwd(cwd: string, home: string | undefined): string {
|
|
158
|
+
if (!home) return cwd;
|
|
159
|
+
const rc = resolve(cwd);
|
|
160
|
+
const rh = resolve(home);
|
|
161
|
+
const rel = relative(rh, rc);
|
|
162
|
+
if (
|
|
163
|
+
rel === "" ||
|
|
164
|
+
(rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
|
|
165
|
+
)
|
|
166
|
+
return rel === "" ? "~" : `~${sep}${rel}`;
|
|
167
|
+
return cwd;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* ------------------------------------------------------------------ */
|
|
171
|
+
/* git status tokens (feature ported from pi-statusline, plain style) */
|
|
172
|
+
/* ------------------------------------------------------------------ */
|
|
173
|
+
|
|
174
|
+
function parseGitStatusTokens(stdout: string): string {
|
|
175
|
+
let ahead = 0,
|
|
176
|
+
behind = 0,
|
|
177
|
+
staged = 0,
|
|
178
|
+
modified = 0,
|
|
179
|
+
untracked = 0,
|
|
180
|
+
conflicts = 0;
|
|
181
|
+
for (const line of stdout.split("\n")) {
|
|
182
|
+
if (!line) continue;
|
|
183
|
+
if (line.startsWith("## ")) {
|
|
184
|
+
const a = line.match(/\bahead (\d+)/);
|
|
185
|
+
const b = line.match(/\bbehind (\d+)/);
|
|
186
|
+
ahead = a ? Number(a[1]) : 0;
|
|
187
|
+
behind = b ? Number(b[1]) : 0;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const x = line[0] ?? " ";
|
|
191
|
+
const y = line[1] ?? " ";
|
|
192
|
+
if (x === "?" && y === "?") {
|
|
193
|
+
untracked++;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (
|
|
197
|
+
(x === "D" && y === "D") ||
|
|
198
|
+
(x === "A" && y === "A") ||
|
|
199
|
+
x === "U" ||
|
|
200
|
+
y === "U"
|
|
201
|
+
) {
|
|
202
|
+
conflicts++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (x !== " " && x !== "?" && x !== "!") staged++;
|
|
206
|
+
if (y !== " " && y !== "?" && y !== "!") modified++;
|
|
207
|
+
}
|
|
208
|
+
const parts: string[] = [];
|
|
209
|
+
if (ahead) parts.push(`⇡${ahead}`);
|
|
210
|
+
if (behind) parts.push(`⇣${behind}`);
|
|
211
|
+
if (staged) parts.push(`+${staged}`);
|
|
212
|
+
if (modified) parts.push(`~${modified}`);
|
|
213
|
+
if (untracked) parts.push(`?${untracked}`);
|
|
214
|
+
if (conflicts) parts.push(`!${conflicts}`);
|
|
215
|
+
return parts.join(" ");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ------------------------------------------------------------------ */
|
|
219
|
+
/* tool activity */
|
|
220
|
+
/* ------------------------------------------------------------------ */
|
|
221
|
+
|
|
222
|
+
interface ToolActivityState {
|
|
223
|
+
active: Map<string, number>;
|
|
224
|
+
minDisplayQueue: string[];
|
|
225
|
+
minDisplayTimer: ReturnType<typeof setTimeout> | undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
interface LiveHooks {
|
|
229
|
+
requestRender: (() => void) | undefined;
|
|
230
|
+
refreshGit: (() => void) | undefined;
|
|
231
|
+
refreshBalance: (() => void) | undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function formatToolActivity(state: ToolActivityState): string {
|
|
235
|
+
const active = [...state.active.entries()];
|
|
236
|
+
if (active.length > 0) {
|
|
237
|
+
const [name, count] = active[0];
|
|
238
|
+
const suffix =
|
|
239
|
+
count > 1
|
|
240
|
+
? `×${count}`
|
|
241
|
+
: active.length > 1
|
|
242
|
+
? `+${active.length - 1}`
|
|
243
|
+
: "";
|
|
244
|
+
return `⚙ ${name}${suffix}`;
|
|
245
|
+
}
|
|
246
|
+
return "";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* ------------------------------------------------------------------ */
|
|
250
|
+
/* footer factory */
|
|
251
|
+
/* ------------------------------------------------------------------ */
|
|
252
|
+
|
|
253
|
+
function makeFooter(
|
|
254
|
+
ctx: any,
|
|
255
|
+
tui: any,
|
|
256
|
+
theme: any,
|
|
257
|
+
fd: any,
|
|
258
|
+
toolState: ToolActivityState,
|
|
259
|
+
live: LiveHooks,
|
|
260
|
+
thresholds: { warn: number; alert: number },
|
|
261
|
+
extensionOrder: string[],
|
|
262
|
+
) {
|
|
263
|
+
let disposed = false;
|
|
264
|
+
let gitTokens = "";
|
|
265
|
+
let gitInFlight = false;
|
|
266
|
+
let gitQueued = false;
|
|
267
|
+
let gitDebounce: ReturnType<typeof setTimeout> | undefined;
|
|
268
|
+
let balanceText = "";
|
|
269
|
+
let balanceProvider = "";
|
|
270
|
+
let balanceStale = false;
|
|
271
|
+
let balanceLastFetch = 0;
|
|
272
|
+
let balanceInFlight = false;
|
|
273
|
+
|
|
274
|
+
const runGitStatus = () => {
|
|
275
|
+
if (disposed) return;
|
|
276
|
+
if (gitInFlight) {
|
|
277
|
+
gitQueued = true;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
gitInFlight = true;
|
|
281
|
+
void (async () => {
|
|
282
|
+
try {
|
|
283
|
+
const result = await ctx.exec(
|
|
284
|
+
"git",
|
|
285
|
+
[
|
|
286
|
+
"--no-optional-locks",
|
|
287
|
+
"status",
|
|
288
|
+
"--porcelain=v1",
|
|
289
|
+
"--branch",
|
|
290
|
+
"--untracked-files=normal",
|
|
291
|
+
],
|
|
292
|
+
{ cwd: ctx.sessionManager.getCwd(), timeout: 3000 },
|
|
293
|
+
);
|
|
294
|
+
gitTokens =
|
|
295
|
+
result.code === 0 && !result.killed
|
|
296
|
+
? parseGitStatusTokens(result.stdout)
|
|
297
|
+
: "";
|
|
298
|
+
} catch {
|
|
299
|
+
gitTokens = "";
|
|
300
|
+
} finally {
|
|
301
|
+
gitInFlight = false;
|
|
302
|
+
if (!disposed) tui.requestRender();
|
|
303
|
+
if (gitQueued) {
|
|
304
|
+
gitQueued = false;
|
|
305
|
+
runGitStatus();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
})();
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const refreshGit = () => {
|
|
312
|
+
if (disposed) return;
|
|
313
|
+
if (gitDebounce) clearTimeout(gitDebounce);
|
|
314
|
+
gitDebounce = setTimeout(() => {
|
|
315
|
+
gitDebounce = undefined;
|
|
316
|
+
runGitStatus();
|
|
317
|
+
}, 250);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const unsub = fd.onBranchChange(() => {
|
|
321
|
+
gitTokens = "";
|
|
322
|
+
refreshGit();
|
|
323
|
+
tui.requestRender();
|
|
324
|
+
});
|
|
325
|
+
const gitInterval = setInterval(runGitStatus, 30000);
|
|
326
|
+
live.requestRender = () => tui.requestRender();
|
|
327
|
+
live.refreshGit = refreshGit;
|
|
328
|
+
runGitStatus();
|
|
329
|
+
|
|
330
|
+
const runBalance = () => {
|
|
331
|
+
if (disposed) return;
|
|
332
|
+
const provider = ctx.model?.provider;
|
|
333
|
+
if (provider !== "deepseek" && provider !== "moonshotai-cn") {
|
|
334
|
+
balanceText = "";
|
|
335
|
+
balanceProvider = "";
|
|
336
|
+
balanceStale = false;
|
|
337
|
+
tui.requestRender();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const now = Date.now();
|
|
341
|
+
if (
|
|
342
|
+
balanceLastFetch > 0 &&
|
|
343
|
+
now - balanceLastFetch < 30_000 &&
|
|
344
|
+
balanceProvider === provider
|
|
345
|
+
) {
|
|
346
|
+
// still fresh
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
balanceProvider = provider;
|
|
350
|
+
balanceLastFetch = now;
|
|
351
|
+
if (balanceInFlight) return;
|
|
352
|
+
const key = getApiKey(provider);
|
|
353
|
+
if (!key) return;
|
|
354
|
+
balanceInFlight = true;
|
|
355
|
+
void (async () => {
|
|
356
|
+
try {
|
|
357
|
+
const value = await fetchBalance(provider, key);
|
|
358
|
+
if (disposed || ctx.model?.provider !== provider) {
|
|
359
|
+
balanceText = "";
|
|
360
|
+
balanceProvider = "";
|
|
361
|
+
} else if (value !== undefined) {
|
|
362
|
+
balanceText = `¤¥${value}`;
|
|
363
|
+
balanceStale = false;
|
|
364
|
+
} else {
|
|
365
|
+
balanceText = "";
|
|
366
|
+
balanceProvider = "";
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
if (!disposed && ctx.model?.provider === provider) {
|
|
370
|
+
if (balanceText) balanceStale = true;
|
|
371
|
+
else {
|
|
372
|
+
balanceText = "";
|
|
373
|
+
balanceProvider = "";
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} finally {
|
|
377
|
+
balanceInFlight = false;
|
|
378
|
+
if (!disposed) tui.requestRender();
|
|
379
|
+
}
|
|
380
|
+
})();
|
|
381
|
+
};
|
|
382
|
+
live.refreshBalance = runBalance;
|
|
383
|
+
if (!balanceText) runBalance();
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
dispose() {
|
|
387
|
+
disposed = true;
|
|
388
|
+
unsub();
|
|
389
|
+
clearInterval(gitInterval);
|
|
390
|
+
if (gitDebounce) clearTimeout(gitDebounce);
|
|
391
|
+
live.requestRender = undefined;
|
|
392
|
+
live.refreshGit = undefined;
|
|
393
|
+
live.refreshBalance = undefined;
|
|
394
|
+
},
|
|
395
|
+
render(width: number): string[] {
|
|
396
|
+
const session = ctx.sessionManager;
|
|
397
|
+
/* token stats — cost intentionally omitted */
|
|
398
|
+
let inp = 0,
|
|
399
|
+
out = 0,
|
|
400
|
+
cr = 0,
|
|
401
|
+
cw = 0;
|
|
402
|
+
let hitRate: number | undefined;
|
|
403
|
+
for (const e of session.getBranch()) {
|
|
404
|
+
if (e.type === "message" && e.message.role === "assistant") {
|
|
405
|
+
const msg = e.message as AssistantMessage;
|
|
406
|
+
inp += msg.usage.input;
|
|
407
|
+
out += msg.usage.output;
|
|
408
|
+
cr += msg.usage.cacheRead;
|
|
409
|
+
cw += msg.usage.cacheWrite;
|
|
410
|
+
const prompt =
|
|
411
|
+
msg.usage.input + msg.usage.cacheRead + msg.usage.cacheWrite;
|
|
412
|
+
hitRate =
|
|
413
|
+
prompt > 0 ? (msg.usage.cacheRead / prompt) * 100 : undefined;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* context usage */
|
|
418
|
+
const cu = ctx.getContextUsage();
|
|
419
|
+
const ctxWin = cu?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
420
|
+
const pct = cu?.percent ?? 0;
|
|
421
|
+
const pctStr = cu?.percent != null ? pct.toFixed(1) : "?";
|
|
422
|
+
|
|
423
|
+
/* pwd / git */
|
|
424
|
+
let pwd = fmtCwd(
|
|
425
|
+
session.getCwd(),
|
|
426
|
+
process.env.HOME || process.env.USERPROFILE,
|
|
427
|
+
);
|
|
428
|
+
const gitBranch = fd.getGitBranch();
|
|
429
|
+
if (gitBranch)
|
|
430
|
+
pwd = gitTokens
|
|
431
|
+
? `${pwd} (${gitBranch} ${gitTokens})`
|
|
432
|
+
: `${pwd} (${gitBranch})`;
|
|
433
|
+
const sname = session.getSessionName();
|
|
434
|
+
if (sname) pwd = `${pwd} • ${sname}`;
|
|
435
|
+
|
|
436
|
+
/* stats line */
|
|
437
|
+
const parts: string[] = [];
|
|
438
|
+
if (inp) parts.push(`↑${fmtTok(inp)}`);
|
|
439
|
+
if (out) parts.push(`↓${fmtTok(out)}`);
|
|
440
|
+
if (cr) parts.push(`R${fmtTok(cr)}`);
|
|
441
|
+
if (cw) parts.push(`W${fmtTok(cw)}`);
|
|
442
|
+
if ((cr || cw) && hitRate !== undefined)
|
|
443
|
+
parts.push(`CH${hitRate.toFixed(1)}%`);
|
|
444
|
+
const ctxPctDisp =
|
|
445
|
+
pctStr === "?" ? `?/${fmtTok(ctxWin)}` : `${pctStr}%/${fmtTok(ctxWin)}`;
|
|
446
|
+
let ctxPctStr: string;
|
|
447
|
+
if (pct > 90) ctxPctStr = theme.fg("error", ctxPctDisp);
|
|
448
|
+
else if (pct > 70) ctxPctStr = theme.fg("warning", ctxPctDisp);
|
|
449
|
+
else ctxPctStr = ctxPctDisp;
|
|
450
|
+
parts.push(ctxPctStr);
|
|
451
|
+
|
|
452
|
+
let left = parts.join(" ");
|
|
453
|
+
let lw = visibleWidth(left);
|
|
454
|
+
if (lw > width) left = truncateToWidth(left, width, "...");
|
|
455
|
+
lw = visibleWidth(left);
|
|
456
|
+
|
|
457
|
+
/* model / thinking — get thinking level from session entries (mirrors native footer) */
|
|
458
|
+
const mName = ctx.model?.id || "no-model";
|
|
459
|
+
let right = mName;
|
|
460
|
+
if (ctx.model?.reasoning) {
|
|
461
|
+
let tl = "off";
|
|
462
|
+
// Walk session branch backwards for most recent thinking_level_change
|
|
463
|
+
const branch = session.getBranch();
|
|
464
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
465
|
+
const e = branch[i];
|
|
466
|
+
if (e.type === "thinking_level_change") {
|
|
467
|
+
tl = (e as any).thinkingLevel || "off";
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
right = tl === "off" ? `${mName} • thinking off` : `${mName} • ${tl}`;
|
|
472
|
+
}
|
|
473
|
+
const toolText = formatToolActivity(toolState);
|
|
474
|
+
const toolColor = "accent";
|
|
475
|
+
const toolSeg = toolText ? `${theme.fg(toolColor, toolText)} ` : "";
|
|
476
|
+
const balanceTextVal = balanceText;
|
|
477
|
+
const balanceProviderVal = ctx.model?.provider;
|
|
478
|
+
let balanceSeg = "";
|
|
479
|
+
if (balanceTextVal && balanceProviderVal === balanceProvider) {
|
|
480
|
+
const num = Number.parseFloat(balanceTextVal.slice(2));
|
|
481
|
+
const color = Number.isNaN(num)
|
|
482
|
+
? "dim"
|
|
483
|
+
: num < thresholds.alert
|
|
484
|
+
? "error"
|
|
485
|
+
: num < thresholds.warn
|
|
486
|
+
? "warning"
|
|
487
|
+
: "dim";
|
|
488
|
+
balanceSeg = ` ${theme.fg(color, balanceTextVal + (balanceStale ? "?" : ""))}`;
|
|
489
|
+
}
|
|
490
|
+
const rw =
|
|
491
|
+
visibleWidth(right) +
|
|
492
|
+
(toolText ? visibleWidth(toolText) + 2 : 0) +
|
|
493
|
+
visibleWidth(balanceSeg);
|
|
494
|
+
|
|
495
|
+
const pad = width - lw - rw;
|
|
496
|
+
let line2: string;
|
|
497
|
+
if (pad >= 0) {
|
|
498
|
+
line2 =
|
|
499
|
+
theme.fg("dim", left) +
|
|
500
|
+
" ".repeat(pad) +
|
|
501
|
+
toolSeg +
|
|
502
|
+
theme.fg("dim", right) +
|
|
503
|
+
balanceSeg;
|
|
504
|
+
} else if (width - lw - 2 > 0) {
|
|
505
|
+
line2 =
|
|
506
|
+
theme.fg("dim", left) +
|
|
507
|
+
theme.fg(
|
|
508
|
+
"dim",
|
|
509
|
+
" " +
|
|
510
|
+
truncateToWidth(
|
|
511
|
+
(toolText ? `${toolText} ` : "") + right + balanceSeg,
|
|
512
|
+
width - lw - 2,
|
|
513
|
+
"",
|
|
514
|
+
),
|
|
515
|
+
);
|
|
516
|
+
} else {
|
|
517
|
+
line2 = theme.fg("dim", left);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/* extension statuses */
|
|
521
|
+
const extItems: string[] = [];
|
|
522
|
+
const raw = fd.getExtensionStatuses();
|
|
523
|
+
let mcpRgb = ""; // capture original MCP accent colour so theme changes work
|
|
524
|
+
if (raw.size > 0) {
|
|
525
|
+
const order = new Map<string, number>();
|
|
526
|
+
extensionOrder.forEach((key, i) => order.set(key, i));
|
|
527
|
+
const sorted = (Array.from(raw.entries()) as [string, string][])
|
|
528
|
+
.sort(([a], [b]) => {
|
|
529
|
+
const oa = order.get(a) ?? 99;
|
|
530
|
+
const ob = order.get(b) ?? 99;
|
|
531
|
+
return oa !== ob ? oa - ob : a.localeCompare(b);
|
|
532
|
+
})
|
|
533
|
+
.map(([k, v]) => {
|
|
534
|
+
if (k === "caveman") {
|
|
535
|
+
const s = (v as string)
|
|
536
|
+
.replace("caveman level:", "Caveman:")
|
|
537
|
+
.replace(/(FULL|ULTRA|LITE|OFF)/g, (w) => w.toLowerCase());
|
|
538
|
+
return s;
|
|
539
|
+
}
|
|
540
|
+
if (k === "ponytail") {
|
|
541
|
+
const s = (v as string)
|
|
542
|
+
.replace("⚡ ", "")
|
|
543
|
+
.replace(/(FULL|ULTRA|LITE|OFF)/g, (w) => w.toLowerCase())
|
|
544
|
+
.replace(
|
|
545
|
+
" 🐴 \x1b[38;2;128;128;128mponytail:",
|
|
546
|
+
" 🐴\x1b[38;2;128;128;128mponytail:",
|
|
547
|
+
);
|
|
548
|
+
return s;
|
|
549
|
+
}
|
|
550
|
+
return (v as string).trim();
|
|
551
|
+
})
|
|
552
|
+
.filter(Boolean);
|
|
553
|
+
extItems.push(...sorted);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Extract original MCP accent RGB from the raw text before we strip colours
|
|
557
|
+
const rawMcp = raw.get("mcp");
|
|
558
|
+
if (rawMcp) {
|
|
559
|
+
const m = (rawMcp as string).match(/\x1b\[38;2;(\d+);(\d+);(\d+)m/);
|
|
560
|
+
if (m) mcpRgb = `\x1b[38;2;${m[1]};${m[2]};${m[3]}m`;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const lines: string[] = [
|
|
564
|
+
truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "...")),
|
|
565
|
+
line2,
|
|
566
|
+
];
|
|
567
|
+
if (extItems.length > 0) {
|
|
568
|
+
let statusLine = extItems.join(" ");
|
|
569
|
+
// Rewrite MCP: prefix with dim colour, reset after the colon
|
|
570
|
+
statusLine = statusLine.replace(
|
|
571
|
+
/MCP:/,
|
|
572
|
+
`\x1b[38;2;128;128;128mMCP:${mcpRgb || "\x1b[38;2;138;190;183m"}`,
|
|
573
|
+
);
|
|
574
|
+
lines.push(truncateToWidth(statusLine, width, theme.fg("dim", "...")));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return lines;
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/* ------------------------------------------------------------------ */
|
|
583
|
+
/* extension entry-point */
|
|
584
|
+
/* ------------------------------------------------------------------ */
|
|
585
|
+
|
|
586
|
+
export default function (pi: any) {
|
|
587
|
+
const toolState: ToolActivityState = {
|
|
588
|
+
active: new Map(),
|
|
589
|
+
minDisplayQueue: [],
|
|
590
|
+
minDisplayTimer: undefined,
|
|
591
|
+
};
|
|
592
|
+
const live: LiveHooks = {
|
|
593
|
+
requestRender: undefined,
|
|
594
|
+
refreshGit: undefined,
|
|
595
|
+
refreshBalance: undefined,
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
function applyFooter(ctx: any) {
|
|
599
|
+
const thresholds = readThresholds();
|
|
600
|
+
const extensionOrder = readExtensionOrder();
|
|
601
|
+
ctx.ui.setFooter((tui: any, theme: any, fd: any) =>
|
|
602
|
+
makeFooter(
|
|
603
|
+
ctx,
|
|
604
|
+
tui,
|
|
605
|
+
theme,
|
|
606
|
+
fd,
|
|
607
|
+
toolState,
|
|
608
|
+
live,
|
|
609
|
+
thresholds,
|
|
610
|
+
extensionOrder,
|
|
611
|
+
),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
let enabled = readPersistedEnabled();
|
|
616
|
+
|
|
617
|
+
pi.on("session_start", async (_event: any, ctx: any) => {
|
|
618
|
+
enabled = readPersistedEnabled();
|
|
619
|
+
toolState.active.clear();
|
|
620
|
+
toolState.minDisplayQueue.length = 0;
|
|
621
|
+
if (toolState.minDisplayTimer) clearTimeout(toolState.minDisplayTimer);
|
|
622
|
+
toolState.minDisplayTimer = undefined;
|
|
623
|
+
if (enabled) applyFooter(ctx);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
pi.on("tool_execution_start", (event: any) => {
|
|
627
|
+
toolState.active.set(
|
|
628
|
+
event.toolName,
|
|
629
|
+
(toolState.active.get(event.toolName) ?? 0) + 1,
|
|
630
|
+
);
|
|
631
|
+
live.requestRender?.();
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
pi.on("tool_execution_end", (event: any) => {
|
|
635
|
+
const n = toolState.active.get(event.toolName) ?? 0;
|
|
636
|
+
if (n <= 1) toolState.active.delete(event.toolName);
|
|
637
|
+
else toolState.active.set(event.toolName, n - 1);
|
|
638
|
+
// minimum 150ms display so fast tools don't flicker
|
|
639
|
+
if (!toolState.active.has(event.toolName)) {
|
|
640
|
+
toolState.minDisplayQueue.push(event.toolName);
|
|
641
|
+
if (!toolState.minDisplayTimer) {
|
|
642
|
+
toolState.minDisplayTimer = setTimeout(() => {
|
|
643
|
+
toolState.minDisplayQueue = [];
|
|
644
|
+
toolState.minDisplayTimer = undefined;
|
|
645
|
+
live.requestRender?.();
|
|
646
|
+
}, 150);
|
|
647
|
+
}
|
|
648
|
+
} else {
|
|
649
|
+
toolState.minDisplayQueue = toolState.minDisplayQueue.filter(
|
|
650
|
+
(q) => q !== event.toolName,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
live.requestRender?.();
|
|
654
|
+
live.refreshGit?.();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
pi.on("agent_start", () => {
|
|
658
|
+
live.requestRender?.();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
pi.on("agent_end", () => {
|
|
662
|
+
live.requestRender?.();
|
|
663
|
+
live.refreshGit?.();
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
pi.on("model_select", () => {
|
|
667
|
+
live.refreshBalance?.();
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
pi.on("turn_end", () => {
|
|
671
|
+
live.refreshBalance?.();
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
pi.registerCommand("bt", {
|
|
675
|
+
description:
|
|
676
|
+
"Set balance thresholds: warn (yellow) / alert (red), warn > alert",
|
|
677
|
+
handler: async (args: string, ctx: any) => {
|
|
678
|
+
const parts = args.trim().split(/\s+/);
|
|
679
|
+
const warn = Number(parts[0]);
|
|
680
|
+
const alert = Number(parts[1]);
|
|
681
|
+
if (
|
|
682
|
+
parts.length !== 2 ||
|
|
683
|
+
!Number.isFinite(warn) ||
|
|
684
|
+
!Number.isFinite(alert) ||
|
|
685
|
+
warn <= alert
|
|
686
|
+
) {
|
|
687
|
+
ctx.ui.notify(
|
|
688
|
+
"Usage: /bt <warn> <alert> (warn > alert). Default: 30 10",
|
|
689
|
+
"error",
|
|
690
|
+
);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
writeThresholds(warn, alert);
|
|
694
|
+
// re-apply footer with fresh thresholds
|
|
695
|
+
if (enabled) applyFooter(ctx);
|
|
696
|
+
ctx.ui.notify(
|
|
697
|
+
`Balance thresholds: yellow < ${warn}, red < ${alert}`,
|
|
698
|
+
"info",
|
|
699
|
+
);
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
pi.registerCommand("tf", {
|
|
704
|
+
description: "Toggle pi-tidy-footer on / off",
|
|
705
|
+
handler: async (_args: string, ctx: any) => {
|
|
706
|
+
enabled = !enabled;
|
|
707
|
+
writePersistedEnabled(enabled);
|
|
708
|
+
|
|
709
|
+
if (enabled) {
|
|
710
|
+
applyFooter(ctx);
|
|
711
|
+
ctx.ui.notify("Cost-free footer ON", "info");
|
|
712
|
+
} else {
|
|
713
|
+
ctx.ui.setFooter(undefined);
|
|
714
|
+
ctx.ui.notify("Default footer restored", "info");
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
pi.registerCommand("se", {
|
|
720
|
+
description:
|
|
721
|
+
"Sort extension statuses (no args = show order, keys = set order)",
|
|
722
|
+
handler: async (args: string, ctx: any) => {
|
|
723
|
+
const trimmed = args.trim();
|
|
724
|
+
if (!trimmed) {
|
|
725
|
+
const current = readExtensionOrder();
|
|
726
|
+
ctx.ui.notify(`Extension order: ${current.join(" ")}`, "info");
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const keys = trimmed.split(/\s+/);
|
|
730
|
+
writeExtensionOrder(keys);
|
|
731
|
+
if (enabled) applyFooter(ctx);
|
|
732
|
+
ctx.ui.notify(`Extension order: ${keys.join(" ")}`, "info");
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-tidy-footer",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Tidy footer for Pi coding agent — native layout, layered with new features: extension sort order, token balance display (color-coded warnings with configurable thresholds), git status tokens, and tool activity indicators; minor display tweaks for selected third-party extensions, keeping it clean.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "extensions/pi-tidy-footer.ts",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/eriiic7z/pi-tidy-footer.git"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"extensions",
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"pi": {
|
|
19
|
+
"extensions": [
|
|
20
|
+
"./extensions/pi-tidy-footer.ts"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"pi-package",
|
|
25
|
+
"pi-extension",
|
|
26
|
+
"extension"
|
|
27
|
+
],
|
|
28
|
+
"author": "eriiic7z",
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^26.1.1"
|
|
31
|
+
}
|
|
32
|
+
}
|