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.
@@ -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, shrinking/dropping lowest-priority segments first.
100
- * Higher priority = survives longer. Returns the surviving segment texts in
101
- * original order, space-joined. Each segment is either kept whole, truncated
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) => ({ text: s.text, priority: s.priority, w: visibleWidth(s.text) }));
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
- items[target].text = truncateToWidth(items[target].text, avail, ellipsis);
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.9",
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
+ }