pi-open-tui 0.2.10 → 0.2.12
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 +105 -103
- package/extensions/open-tui/config.ts +141 -139
- package/extensions/open-tui/editor.ts +100 -100
- package/extensions/open-tui/footer.ts +56 -11
- package/extensions/open-tui/git.ts +5 -2
- package/extensions/open-tui/icons.ts +4 -1
- package/extensions/open-tui/index.ts +296 -273
- package/extensions/open-tui/settings-command.ts +366 -358
- package/extensions/open-tui/state.ts +90 -90
- package/extensions/open-tui/utils.ts +38 -6
- package/package.json +66 -66
package/README.md
CHANGED
|
@@ -1,103 +1,105 @@
|
|
|
1
|
-
# pi-open-tui
|
|
2
|
-
|
|
3
|
-
A polished TUI for [Pi](https://pi.dev) coding agent. Combines the best of pi-haiku, pi-claude-code-tui, and pi-zentui into one cohesive package.
|
|
4
|
-
|
|
5
|
-

|
|
6
|
-
|
|
7
|
-
## What's in it
|
|
8
|
-
|
|
9
|
-
- **Animated Pi logo header** — 16-frame color-changing logo animation + "Let's build something great" tagline
|
|
10
|
-
- **Starship-style footer** — 2 lines showing cwd, git branch/status, runtime version, context bar, model, token counts, and cost
|
|
11
|
-
- **Rounded editor** — accent rail + borderMuted rounded corners, clean visual frame
|
|
12
|
-
- **60+ runtime detection** — Node, Rust, Go, Python, Ruby, Java, Swift, Kotlin, C/C++, Deno, Bun, and many more
|
|
13
|
-
- **Git status** — branch, ahead/behind, modified/untracked/staged/stashed, detached HEAD commit hash + tag
|
|
14
|
-
- **Working timer** — live elapsed time while the agent is working, done duration when finished
|
|
15
|
-
- **Turn telemetry** — generation speed, TTFT, stalls, tokens, and list-price rate after each complete agent run
|
|
16
|
-
- **Zero prototype patches** — uses public Pi APIs (setHeader/setFooter/setEditorComponent), safe across Pi updates
|
|
17
|
-
- **Interactive settings UI** — `/open-tui` opens a tabbed settings dialog (General / Icons / Footer / Telemetry)
|
|
18
|
-
|
|
19
|
-
## Install
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
pi install npm:pi-open-tui
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Or try it for one run:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
pi -e npm:pi-open-tui
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Configuration
|
|
32
|
-
|
|
33
|
-
Run `/open-tui` to open the interactive settings UI. Configuration is stored at `~/.pi/agent/open-tui.json`:
|
|
34
|
-
|
|
35
|
-
```json
|
|
36
|
-
{
|
|
37
|
-
"enabled": true,
|
|
38
|
-
"settingsLanguage": "en",
|
|
39
|
-
"icons": {
|
|
40
|
-
"mode": "auto"
|
|
41
|
-
},
|
|
42
|
-
"footerSegments": {
|
|
43
|
-
"cwd": true,
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- `
|
|
67
|
-
- `
|
|
68
|
-
- `footerSegments.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
- **[pi-
|
|
99
|
-
- **[pi-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
# pi-open-tui
|
|
2
|
+
|
|
3
|
+
A polished TUI for [Pi](https://pi.dev) coding agent. Combines the best of pi-haiku, pi-claude-code-tui, and pi-zentui into one cohesive package.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## What's in it
|
|
8
|
+
|
|
9
|
+
- **Animated Pi logo header** — 16-frame color-changing logo animation + "Let's build something great" tagline
|
|
10
|
+
- **Starship-style footer** — 2 lines showing cwd, git branch/status, runtime version, context bar, model, token counts, and cost
|
|
11
|
+
- **Rounded editor** — accent rail + borderMuted rounded corners, clean visual frame
|
|
12
|
+
- **60+ runtime detection** — Node, Rust, Go, Python, Ruby, Java, Swift, Kotlin, C/C++, Deno, Bun, and many more
|
|
13
|
+
- **Git status** — branch, ahead/behind, modified/untracked/staged/stashed, detached HEAD commit hash + tag
|
|
14
|
+
- **Working timer** — live elapsed time while the agent is working, done duration when finished
|
|
15
|
+
- **Turn telemetry** — generation speed, TTFT, stalls, tokens, and list-price rate after each complete agent run
|
|
16
|
+
- **Zero prototype patches** — uses public Pi APIs (setHeader/setFooter/setEditorComponent), safe across Pi updates
|
|
17
|
+
- **Interactive settings UI** — `/open-tui` opens a tabbed settings dialog (General / Icons / Footer / Telemetry)
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pi install npm:pi-open-tui
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or try it for one run:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pi -e npm:pi-open-tui
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
Run `/open-tui` to open the interactive settings UI. Configuration is stored at `~/.pi/agent/open-tui.json`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"enabled": true,
|
|
38
|
+
"settingsLanguage": "en",
|
|
39
|
+
"icons": {
|
|
40
|
+
"mode": "auto"
|
|
41
|
+
},
|
|
42
|
+
"footerSegments": {
|
|
43
|
+
"cwd": true,
|
|
44
|
+
"sessionName": false,
|
|
45
|
+
"gitBranch": true,
|
|
46
|
+
"gitStatus": true,
|
|
47
|
+
"gitCommit": false,
|
|
48
|
+
"runtime": true,
|
|
49
|
+
"context": true,
|
|
50
|
+
"tokens": true,
|
|
51
|
+
"cost": true,
|
|
52
|
+
"extensionStatuses": true
|
|
53
|
+
},
|
|
54
|
+
"telemetry": {
|
|
55
|
+
"enabled": true,
|
|
56
|
+
"tps": true,
|
|
57
|
+
"ttft": true,
|
|
58
|
+
"duration": true,
|
|
59
|
+
"tokens": true,
|
|
60
|
+
"stalls": true,
|
|
61
|
+
"cost": true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- `settingsLanguage`: language for the `/open-tui` settings UI only; `en` or `zh`
|
|
67
|
+
- `icons.mode`: `auto` (detect Nerd Font), `nerd` (force Nerd Font glyphs), or `ascii` (plain fallbacks)
|
|
68
|
+
- `footerSegments.sessionName`: shows the current session name next to cwd (off by default); hidden when the session has no name
|
|
69
|
+
- `footerSegments.gitCommit`: shows short hash + tag on detached HEAD (off by default)
|
|
70
|
+
- `footerSegments.extensionStatuses`: shows statuses published by extensions through Pi's `setStatus()` API (on by default); turn it off to hide the whole status line, including MCP
|
|
71
|
+
|
|
72
|
+
## Turn telemetry
|
|
73
|
+
|
|
74
|
+
After each complete agent run, open-tui shows one transient notification. Tool-call turns are aggregated into that single result:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
> TPS 42.5 tok/s | ~ TTFT 1.2s | + 29.7s | ↑ 567 | ↓ 1.2k | ! stall 1x / 4.3s | $ $3.60/M
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The notification uses the footer's icon mode and semantic theme colors. Configure its master switch and individual TPS, TTFT, duration, token, stall, and cost segments from the **Telemetry** tab in `/open-tui`.
|
|
81
|
+
|
|
82
|
+
TPS is the complete generation throughput for the agent run: all provider-reported assistant output tokens divided by the summed generation time of every LLM turn, measured from `turn_start` through the assistant `message_end`. This includes time-to-first-token, hidden reasoning, buffering, and stalls so the token count and timing cover the same interval. Tool execution between turns is excluded. A run with no output tokens or no measurable generation time is shown as `TPS —`. The `stall` segment shows occurrence count followed by accumulated duration. The optional `$ / M` value uses the model's list-price `usage.cost.total`; it is not the session's cumulative cost shown in the footer.
|
|
83
|
+
|
|
84
|
+
## Local development
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pi -e .
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
93
|
+
|
|
94
|
+
## Acknowledgements
|
|
95
|
+
|
|
96
|
+
This project builds on the work of several Pi community packages:
|
|
97
|
+
|
|
98
|
+
- **[pi-haiku](https://github.com/nnocte/pi-haiku)** — the 2-line footer structure (location+model · timer+context) and working-timer pattern
|
|
99
|
+
- **[pi-claude-code-tui](https://github.com/Phoobobo/pi-claude-code-tui)** — the 16-frame animated Pi logo and rounded editor border technique
|
|
100
|
+
- **[pi-zentui](https://github.com/lmilojevicc/pi-zentui)** — the Starship-style footer segments (git status icons, runtime detection, context gauge), generation-based session lifecycle, and interactive settings UI pattern
|
|
101
|
+
- **[pi-tps](https://github.com/monotykamary/pi-tps)** — the turn timing, stall detection, and conservative TPS measurement approach
|
|
102
|
+
|
|
103
|
+
The animated logo frames are derived from `pi-claude-code-tui`, which in turn derive from Pi's official install script (`pi.dev/install.sh`). The runtime detection list and git porcelain parsing borrow structure from `pi-zentui`.
|
|
104
|
+
|
|
105
|
+
Special thanks to the **[LINUX DO](https://linux.do)** community for their support.
|
|
@@ -1,139 +1,141 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import type { IconMode } from "./icons.ts";
|
|
5
|
-
|
|
6
|
-
export type SettingsLanguage = "en" | "zh";
|
|
7
|
-
|
|
8
|
-
export type { IconMode } from "./icons.ts";
|
|
9
|
-
|
|
10
|
-
export interface FooterSegments {
|
|
11
|
-
cwd: boolean;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
result[key] = overVal;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { IconMode } from "./icons.ts";
|
|
5
|
+
|
|
6
|
+
export type SettingsLanguage = "en" | "zh";
|
|
7
|
+
|
|
8
|
+
export type { IconMode } from "./icons.ts";
|
|
9
|
+
|
|
10
|
+
export interface FooterSegments {
|
|
11
|
+
cwd: boolean;
|
|
12
|
+
sessionName: boolean;
|
|
13
|
+
gitBranch: boolean;
|
|
14
|
+
gitStatus: boolean;
|
|
15
|
+
gitCommit: boolean;
|
|
16
|
+
runtime: boolean;
|
|
17
|
+
context: boolean;
|
|
18
|
+
tokens: boolean;
|
|
19
|
+
cost: boolean;
|
|
20
|
+
extensionStatuses: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TelemetryConfig {
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
tps: boolean;
|
|
26
|
+
ttft: boolean;
|
|
27
|
+
duration: boolean;
|
|
28
|
+
tokens: boolean;
|
|
29
|
+
stalls: boolean;
|
|
30
|
+
cost: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface OpenTuiConfig {
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
settingsLanguage: SettingsLanguage;
|
|
36
|
+
icons: {
|
|
37
|
+
mode: IconMode;
|
|
38
|
+
};
|
|
39
|
+
footerSegments: FooterSegments;
|
|
40
|
+
telemetry: TelemetryConfig;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const DEFAULT_CONFIG: OpenTuiConfig = {
|
|
44
|
+
enabled: true,
|
|
45
|
+
settingsLanguage: "en",
|
|
46
|
+
icons: {
|
|
47
|
+
mode: "auto",
|
|
48
|
+
},
|
|
49
|
+
footerSegments: {
|
|
50
|
+
cwd: true,
|
|
51
|
+
sessionName: false,
|
|
52
|
+
gitBranch: true,
|
|
53
|
+
gitStatus: true,
|
|
54
|
+
gitCommit: false,
|
|
55
|
+
runtime: true,
|
|
56
|
+
context: true,
|
|
57
|
+
tokens: true,
|
|
58
|
+
cost: true,
|
|
59
|
+
extensionStatuses: true,
|
|
60
|
+
},
|
|
61
|
+
telemetry: {
|
|
62
|
+
enabled: true,
|
|
63
|
+
tps: true,
|
|
64
|
+
ttft: true,
|
|
65
|
+
duration: true,
|
|
66
|
+
tokens: true,
|
|
67
|
+
stalls: true,
|
|
68
|
+
cost: true,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function getConfigPath(): string {
|
|
73
|
+
const agentDir = getAgentDir();
|
|
74
|
+
return join(agentDir, "open-tui.json");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function deepMerge<T>(base: T, override: unknown): T {
|
|
78
|
+
if (typeof base !== "object" || base === null || Array.isArray(base)) {
|
|
79
|
+
return (override as T) ?? base;
|
|
80
|
+
}
|
|
81
|
+
if (typeof override !== "object" || override === null || Array.isArray(override)) {
|
|
82
|
+
return base;
|
|
83
|
+
}
|
|
84
|
+
const result = { ...(base as Record<string, unknown>) };
|
|
85
|
+
const overrideRec = override as Record<string, unknown>;
|
|
86
|
+
for (const key of Object.keys(overrideRec)) {
|
|
87
|
+
const baseVal = (base as Record<string, unknown>)[key];
|
|
88
|
+
const overVal = overrideRec[key];
|
|
89
|
+
if (typeof baseVal === "object" && baseVal !== null && !Array.isArray(baseVal)
|
|
90
|
+
&& typeof overVal === "object" && overVal !== null && !Array.isArray(overVal)) {
|
|
91
|
+
result[key] = deepMerge(baseVal, overVal);
|
|
92
|
+
} else if (overVal !== undefined) {
|
|
93
|
+
result[key] = overVal;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result as T;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function ensureConfigExists(): void {
|
|
100
|
+
const path = getConfigPath();
|
|
101
|
+
if (existsSync(path)) return;
|
|
102
|
+
try {
|
|
103
|
+
const agentDir = getAgentDir();
|
|
104
|
+
if (!existsSync(agentDir)) mkdirSync(agentDir, { recursive: true });
|
|
105
|
+
writeFileSync(path, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n", "utf8");
|
|
106
|
+
} catch {
|
|
107
|
+
// ponytail: silent fallback — config creation is best-effort
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function loadConfig(notify?: (msg: string, level: "warning" | "info") => void): OpenTuiConfig {
|
|
112
|
+
const path = getConfigPath();
|
|
113
|
+
if (!existsSync(path)) {
|
|
114
|
+
ensureConfigExists();
|
|
115
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const raw = readFileSync(path, "utf8");
|
|
120
|
+
const parsed: unknown = JSON.parse(raw);
|
|
121
|
+
const config = deepMerge(DEFAULT_CONFIG, parsed);
|
|
122
|
+
if (config.settingsLanguage !== "en" && config.settingsLanguage !== "zh") {
|
|
123
|
+
config.settingsLanguage = DEFAULT_CONFIG.settingsLanguage;
|
|
124
|
+
}
|
|
125
|
+
return config;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
notify?.(`open-tui config parse error: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
128
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function saveConfig(config: OpenTuiConfig): void {
|
|
133
|
+
const path = getConfigPath();
|
|
134
|
+
try {
|
|
135
|
+
const agentDir = getAgentDir();
|
|
136
|
+
if (!existsSync(agentDir)) mkdirSync(agentDir, { recursive: true });
|
|
137
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
138
|
+
} catch {
|
|
139
|
+
// ponytail: silent fallback — config save is best-effort
|
|
140
|
+
}
|
|
141
|
+
}
|