pi-open-tui 0.2.9 → 0.2.11
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 -99
- package/extensions/open-tui/config.ts +141 -129
- package/extensions/open-tui/editor.ts +100 -100
- package/extensions/open-tui/footer.ts +67 -20
- package/extensions/open-tui/git.ts +5 -2
- package/extensions/open-tui/icons.ts +4 -1
- package/extensions/open-tui/index.ts +286 -273
- package/extensions/open-tui/settings-command.ts +361 -289
- package/extensions/open-tui/state.ts +90 -90
- package/extensions/open-tui/utils.ts +38 -6
- package/package.json +66 -66
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
3
|
-
import type { GitStatus } from "./git.ts";
|
|
4
|
-
import { emptyGitStatus } from "./git.ts";
|
|
5
|
-
import type { RuntimeInfo } from "./runtime.ts";
|
|
6
|
-
import { fmtTokens, formatProviderLabel } from "./utils.ts";
|
|
7
|
-
|
|
8
|
-
export interface FooterState {
|
|
9
|
-
git: GitStatus;
|
|
10
|
-
runtime: RuntimeInfo | null;
|
|
11
|
-
sessionStartEpoch: number;
|
|
12
|
-
workingSince: number | undefined;
|
|
13
|
-
lastDoneIn: number | undefined;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface UsageTotals {
|
|
17
|
-
input: number;
|
|
18
|
-
output: number;
|
|
19
|
-
cacheRead: number;
|
|
20
|
-
cacheWrite: number;
|
|
21
|
-
cost: number;
|
|
22
|
-
latestCacheHitRate: number | undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let usageCache: { key: string; totals: UsageTotals } | undefined;
|
|
26
|
-
|
|
27
|
-
function entriesKey(ctx: ExtensionContext): string {
|
|
28
|
-
const entries = ctx.sessionManager.getEntries();
|
|
29
|
-
const last = entries.at(-1);
|
|
30
|
-
return `${entries.length}:${last?.id ?? ""}:${last?.timestamp ?? ""}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function getUsageTotals(ctx: ExtensionContext): UsageTotals {
|
|
34
|
-
const key = entriesKey(ctx);
|
|
35
|
-
if (usageCache && usageCache.key === key) return usageCache.totals;
|
|
36
|
-
|
|
37
|
-
const totals: UsageTotals = {
|
|
38
|
-
input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0,
|
|
39
|
-
latestCacheHitRate: undefined,
|
|
40
|
-
};
|
|
41
|
-
for (const entry of ctx.sessionManager.getEntries()) {
|
|
42
|
-
if (entry.type === "message" && entry.message?.role === "assistant") {
|
|
43
|
-
const m = entry.message as AssistantMessage;
|
|
44
|
-
const u = m.usage;
|
|
45
|
-
if (!u) continue;
|
|
46
|
-
totals.input += u.input ?? 0;
|
|
47
|
-
totals.output += u.output ?? 0;
|
|
48
|
-
totals.cacheRead += u.cacheRead ?? 0;
|
|
49
|
-
totals.cacheWrite += u.cacheWrite ?? 0;
|
|
50
|
-
totals.cost += u.cost?.total ?? 0;
|
|
51
|
-
const promptTokens = (u.input ?? 0) + (u.cacheRead ?? 0) + (u.cacheWrite ?? 0);
|
|
52
|
-
if (promptTokens > 0) {
|
|
53
|
-
totals.latestCacheHitRate = ((u.cacheRead ?? 0) / promptTokens) * 100;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
usageCache = { key, totals };
|
|
58
|
-
return totals;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function invalidateUsageCache(): void {
|
|
62
|
-
usageCache = undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function createInitialState(): FooterState {
|
|
66
|
-
return {
|
|
67
|
-
git: emptyGitStatus(),
|
|
68
|
-
runtime: null,
|
|
69
|
-
sessionStartEpoch: Date.now(),
|
|
70
|
-
workingSince: undefined,
|
|
71
|
-
lastDoneIn: undefined,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface ModelMeta {
|
|
76
|
-
provider: string;
|
|
77
|
-
model: string;
|
|
78
|
-
effort: string | undefined;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function getModelMeta(
|
|
82
|
-
ctx: ExtensionContext,
|
|
83
|
-
getThinkingLevel: () => string,
|
|
84
|
-
): ModelMeta {
|
|
85
|
-
const provider = formatProviderLabel(ctx.model?.provider);
|
|
86
|
-
const model = ctx.model?.id ?? "no-model";
|
|
87
|
-
const reasoning = ctx.model?.reasoning ?? false;
|
|
88
|
-
const effort = reasoning ? getThinkingLevel() : undefined;
|
|
89
|
-
return { provider, model, effort };
|
|
90
|
-
}
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { GitStatus } from "./git.ts";
|
|
4
|
+
import { emptyGitStatus } from "./git.ts";
|
|
5
|
+
import type { RuntimeInfo } from "./runtime.ts";
|
|
6
|
+
import { fmtTokens, formatProviderLabel } from "./utils.ts";
|
|
7
|
+
|
|
8
|
+
export interface FooterState {
|
|
9
|
+
git: GitStatus;
|
|
10
|
+
runtime: RuntimeInfo | null;
|
|
11
|
+
sessionStartEpoch: number;
|
|
12
|
+
workingSince: number | undefined;
|
|
13
|
+
lastDoneIn: number | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UsageTotals {
|
|
17
|
+
input: number;
|
|
18
|
+
output: number;
|
|
19
|
+
cacheRead: number;
|
|
20
|
+
cacheWrite: number;
|
|
21
|
+
cost: number;
|
|
22
|
+
latestCacheHitRate: number | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let usageCache: { key: string; totals: UsageTotals } | undefined;
|
|
26
|
+
|
|
27
|
+
function entriesKey(ctx: ExtensionContext): string {
|
|
28
|
+
const entries = ctx.sessionManager.getEntries();
|
|
29
|
+
const last = entries.at(-1);
|
|
30
|
+
return `${entries.length}:${last?.id ?? ""}:${last?.timestamp ?? ""}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getUsageTotals(ctx: ExtensionContext): UsageTotals {
|
|
34
|
+
const key = entriesKey(ctx);
|
|
35
|
+
if (usageCache && usageCache.key === key) return usageCache.totals;
|
|
36
|
+
|
|
37
|
+
const totals: UsageTotals = {
|
|
38
|
+
input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0,
|
|
39
|
+
latestCacheHitRate: undefined,
|
|
40
|
+
};
|
|
41
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
42
|
+
if (entry.type === "message" && entry.message?.role === "assistant") {
|
|
43
|
+
const m = entry.message as AssistantMessage;
|
|
44
|
+
const u = m.usage;
|
|
45
|
+
if (!u) continue;
|
|
46
|
+
totals.input += u.input ?? 0;
|
|
47
|
+
totals.output += u.output ?? 0;
|
|
48
|
+
totals.cacheRead += u.cacheRead ?? 0;
|
|
49
|
+
totals.cacheWrite += u.cacheWrite ?? 0;
|
|
50
|
+
totals.cost += u.cost?.total ?? 0;
|
|
51
|
+
const promptTokens = (u.input ?? 0) + (u.cacheRead ?? 0) + (u.cacheWrite ?? 0);
|
|
52
|
+
if (promptTokens > 0) {
|
|
53
|
+
totals.latestCacheHitRate = ((u.cacheRead ?? 0) / promptTokens) * 100;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
usageCache = { key, totals };
|
|
58
|
+
return totals;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function invalidateUsageCache(): void {
|
|
62
|
+
usageCache = undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createInitialState(): FooterState {
|
|
66
|
+
return {
|
|
67
|
+
git: emptyGitStatus(),
|
|
68
|
+
runtime: null,
|
|
69
|
+
sessionStartEpoch: Date.now(),
|
|
70
|
+
workingSince: undefined,
|
|
71
|
+
lastDoneIn: undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ModelMeta {
|
|
76
|
+
provider: string;
|
|
77
|
+
model: string;
|
|
78
|
+
effort: string | undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getModelMeta(
|
|
82
|
+
ctx: ExtensionContext,
|
|
83
|
+
getThinkingLevel: () => string,
|
|
84
|
+
): ModelMeta {
|
|
85
|
+
const provider = formatProviderLabel(ctx.model?.provider);
|
|
86
|
+
const model = ctx.model?.name ?? ctx.model?.id ?? "no-model";
|
|
87
|
+
const reasoning = ctx.model?.reasoning ?? false;
|
|
88
|
+
const effort = reasoning ? getThinkingLevel() : undefined;
|
|
89
|
+
return { provider, model, effort };
|
|
90
|
+
}
|
|
@@ -24,6 +24,17 @@ export function formatCwd(cwd: string): string {
|
|
|
24
24
|
return rel === "" ? "~" : `~${sep}${rel}`;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export function basenamePath(path: string): string {
|
|
28
|
+
return path.split(/[\\/]/).filter(Boolean).at(-1) ?? path;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Truncate branch names from the end so prefixes like `fix/` stay visible. */
|
|
32
|
+
export function truncateBranch(branch: string, maxLen: number): string {
|
|
33
|
+
if (branch.length <= maxLen) return branch;
|
|
34
|
+
if (maxLen <= 3) return "...".slice(0, maxLen);
|
|
35
|
+
return `${branch.slice(0, maxLen - 3)}...`;
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
export function truncatePath(path: string, maxLen: number): string {
|
|
28
39
|
if (path.length <= maxLen) return path;
|
|
29
40
|
if (maxLen <= 3) return "...".slice(0, maxLen);
|
|
@@ -93,24 +104,42 @@ export function alignRight(left: string, right: string, width: number, theme: Th
|
|
|
93
104
|
export type PrioritizedSegment = {
|
|
94
105
|
text: string;
|
|
95
106
|
priority: number;
|
|
107
|
+
/** Compact form swapped in before any segment is truncated or dropped. */
|
|
108
|
+
compactText?: string;
|
|
109
|
+
/** Segment-aware truncation replacing the generic truncateToWidth. */
|
|
110
|
+
truncate?: (text: string, maxWidth: number, ellipsis: string) => string;
|
|
96
111
|
};
|
|
97
112
|
|
|
98
113
|
/**
|
|
99
|
-
* Pack segments into maxWidth
|
|
100
|
-
*
|
|
101
|
-
* original order, space-joined.
|
|
102
|
-
* with ellipsis, or dropped entirely.
|
|
114
|
+
* Pack segments into maxWidth: compact segments first, then shrink/drop the
|
|
115
|
+
* lowest-priority segments (higher priority = survives longer). Returns the
|
|
116
|
+
* surviving segment texts in original order, space-joined.
|
|
103
117
|
*/
|
|
104
118
|
export function fitSegmentsByPriority(
|
|
105
119
|
segs: readonly PrioritizedSegment[],
|
|
106
120
|
maxW: number,
|
|
107
121
|
ellipsis = "...",
|
|
108
122
|
): string[] {
|
|
109
|
-
const items = segs.map((s) => ({
|
|
123
|
+
const items = segs.map((s) => ({
|
|
124
|
+
text: s.text,
|
|
125
|
+
compactText: s.compactText,
|
|
126
|
+
priority: s.priority,
|
|
127
|
+
truncate: s.truncate,
|
|
128
|
+
w: visibleWidth(s.text),
|
|
129
|
+
}));
|
|
110
130
|
const totalW = () => {
|
|
111
131
|
const active = items.filter((it) => it.text !== "");
|
|
112
132
|
return active.reduce((a, it) => a + it.w, 0) + Math.max(0, active.length - 1);
|
|
113
133
|
};
|
|
134
|
+
// Compact (e.g. cwd -> basename) before sacrificing any segment content.
|
|
135
|
+
if (totalW() > maxW) {
|
|
136
|
+
for (const item of items) {
|
|
137
|
+
if (!item.compactText || visibleWidth(item.compactText) >= item.w) continue;
|
|
138
|
+
item.text = item.compactText;
|
|
139
|
+
item.w = visibleWidth(item.text);
|
|
140
|
+
if (totalW() <= maxW) break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
114
143
|
while (totalW() > maxW) {
|
|
115
144
|
let target = -1;
|
|
116
145
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -126,7 +155,10 @@ export function fitSegmentsByPriority(
|
|
|
126
155
|
items[target].text = "";
|
|
127
156
|
items[target].w = 0;
|
|
128
157
|
} else if (avail < items[target].w) {
|
|
129
|
-
|
|
158
|
+
const truncate = items[target].truncate;
|
|
159
|
+
items[target].text = truncate
|
|
160
|
+
? truncate(items[target].text, avail, ellipsis)
|
|
161
|
+
: truncateToWidth(items[target].text, avail, ellipsis);
|
|
130
162
|
items[target].w = visibleWidth(items[target].text);
|
|
131
163
|
} else {
|
|
132
164
|
break;
|
package/package.json
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-open-tui",
|
|
3
|
-
"version": "0.2.
|
|
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. Combines the best of pi-haiku, pi-claude-code-tui, and pi-zentui.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/OldSuns/pi-open-tui.git"
|
|
9
|
-
},
|
|
10
|
-
"keywords": [
|
|
11
|
-
"pi-package",
|
|
12
|
-
"pi",
|
|
13
|
-
"pi-coding-agent",
|
|
14
|
-
"pi-extension",
|
|
15
|
-
"tui",
|
|
16
|
-
"starship",
|
|
17
|
-
"theme"
|
|
18
|
-
],
|
|
19
|
-
"license": "MIT",
|
|
20
|
-
"files": [
|
|
21
|
-
"extensions/",
|
|
22
|
-
"assets/",
|
|
23
|
-
"README.md",
|
|
24
|
-
"LICENSE"
|
|
25
|
-
],
|
|
26
|
-
"scripts": {
|
|
27
|
-
"test": "node --test tests/settings-command.test.ts tests/footer.test.ts tests/editor.test.ts tests/telemetry.test.ts",
|
|
28
|
-
"typecheck": "tsc --noEmit"
|
|
29
|
-
},
|
|
30
|
-
"pi": {
|
|
31
|
-
"extensions": [
|
|
32
|
-
"./extensions/open-tui/index.ts"
|
|
33
|
-
],
|
|
34
|
-
"image": "https://raw.githubusercontent.com/OldSuns/pi-open-tui/main/assets/preview_dashboard_1.png"
|
|
35
|
-
},
|
|
36
|
-
"peerDependencies": {
|
|
37
|
-
"@earendil-works/pi-coding-agent": ">=0.80",
|
|
38
|
-
"@earendil-works/pi-ai": ">=0.80",
|
|
39
|
-
"@earendil-works/pi-tui": ">=0.80"
|
|
40
|
-
},
|
|
41
|
-
"peerDependenciesMeta": {
|
|
42
|
-
"@earendil-works/pi-coding-agent": {
|
|
43
|
-
"optional": true
|
|
44
|
-
},
|
|
45
|
-
"@earendil-works/pi-ai": {
|
|
46
|
-
"optional": true
|
|
47
|
-
},
|
|
48
|
-
"@earendil-works/pi-tui": {
|
|
49
|
-
"optional": true
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-coding-agent": "*",
|
|
54
|
-
"@earendil-works/pi-ai": "*",
|
|
55
|
-
"@earendil-works/pi-tui": "*",
|
|
56
|
-
"@types/node": "^22.0.0",
|
|
57
|
-
"typescript": "^5.5.0"
|
|
58
|
-
},
|
|
59
|
-
"allowScripts": {
|
|
60
|
-
"protobufjs@7.6.5": true,
|
|
61
|
-
"protobufjs@7.6.4": true,
|
|
62
|
-
"@google/genai@1.52.0": true,
|
|
63
|
-
"koffi@2.16.2": true,
|
|
64
|
-
"tree-sitter-bash@0.25.1": true
|
|
65
|
-
}
|
|
66
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-open-tui",
|
|
3
|
+
"version": "0.2.11",
|
|
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. Combines the best of pi-haiku, pi-claude-code-tui, and pi-zentui.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/OldSuns/pi-open-tui.git"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pi-package",
|
|
12
|
+
"pi",
|
|
13
|
+
"pi-coding-agent",
|
|
14
|
+
"pi-extension",
|
|
15
|
+
"tui",
|
|
16
|
+
"starship",
|
|
17
|
+
"theme"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"files": [
|
|
21
|
+
"extensions/",
|
|
22
|
+
"assets/",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node --test tests/settings-command.test.ts tests/footer.test.ts tests/editor.test.ts tests/telemetry.test.ts",
|
|
28
|
+
"typecheck": "tsc --noEmit"
|
|
29
|
+
},
|
|
30
|
+
"pi": {
|
|
31
|
+
"extensions": [
|
|
32
|
+
"./extensions/open-tui/index.ts"
|
|
33
|
+
],
|
|
34
|
+
"image": "https://raw.githubusercontent.com/OldSuns/pi-open-tui/main/assets/preview_dashboard_1.png"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@earendil-works/pi-coding-agent": ">=0.80",
|
|
38
|
+
"@earendil-works/pi-ai": ">=0.80",
|
|
39
|
+
"@earendil-works/pi-tui": ">=0.80"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"@earendil-works/pi-coding-agent": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
45
|
+
"@earendil-works/pi-ai": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
48
|
+
"@earendil-works/pi-tui": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
54
|
+
"@earendil-works/pi-ai": "*",
|
|
55
|
+
"@earendil-works/pi-tui": "*",
|
|
56
|
+
"@types/node": "^22.0.0",
|
|
57
|
+
"typescript": "^5.5.0"
|
|
58
|
+
},
|
|
59
|
+
"allowScripts": {
|
|
60
|
+
"protobufjs@7.6.5": true,
|
|
61
|
+
"protobufjs@7.6.4": true,
|
|
62
|
+
"@google/genai@1.52.0": true,
|
|
63
|
+
"koffi@2.16.2": true,
|
|
64
|
+
"tree-sitter-bash@0.25.1": true
|
|
65
|
+
}
|
|
66
|
+
}
|