pi-powerline-footer 0.2.2
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 +66 -0
- package/README.md +75 -0
- package/colors.ts +126 -0
- package/git-status.ts +242 -0
- package/icons.ts +156 -0
- package/index.ts +412 -0
- package/install.mjs +30 -0
- package/package.json +27 -0
- package/presets.ts +80 -0
- package/segments.ts +440 -0
- package/separators.ts +57 -0
- package/types.ts +129 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.2] - 2026-01-15
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **Path segment defaults to basename** — Shows just the directory name (e.g., `powerline-footer`) instead of full path to save space
|
|
7
|
+
- **New path modes** — `basename` (default), `abbreviated` (truncated full path), `full` (complete path)
|
|
8
|
+
- Simplified path options: replaced `abbreviate`, `stripWorkPrefix` with cleaner `mode` option
|
|
9
|
+
- Full/nerd presets use `abbreviated` mode, default/minimal/compact use `basename`
|
|
10
|
+
- Thinking segment now uses dedicated gradient colors (thinkingOff → thinkingMedium)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Path basename extraction now uses `path.basename()` for Windows compatibility
|
|
14
|
+
- Git branch cache now stores `null` results, preventing repeated git calls in non-git directories
|
|
15
|
+
- Git status cache now stores empty results for non-git directories (was also spawning repeatedly)
|
|
16
|
+
- Removed dead `footerDispose` variable (cleanup handled by pi internally)
|
|
17
|
+
|
|
18
|
+
## [0.2.1] - 2026-01-10
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Live git branch updates** — Branch now updates in real-time when switching via `git checkout`, `git switch`, etc.
|
|
22
|
+
- **Own branch fetching** — Extension fetches branch directly via `git branch --show-current` instead of relying solely on FooterDataProvider
|
|
23
|
+
- **Branch cache with 500ms TTL** — Faster refresh cycle for branch changes
|
|
24
|
+
- **Staggered re-renders for escape commands** — Multiple re-renders at 100/300/500ms to catch updates from `!` commands
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Git branch not updating after `git checkout` to existing branches
|
|
28
|
+
- Race condition where FooterDataProvider's branch cache wasn't updating in time
|
|
29
|
+
|
|
30
|
+
## [0.2.0] - 2026-01-10
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- **Extension statuses segment** — Displays status text from other extensions (e.g., rewind checkpoint count)
|
|
34
|
+
- **Thinking level segment** — Live-updating display of current thinking level (`thinking:off`, `thinking:med`, etc.)
|
|
35
|
+
- **Rainbow effect** — High and xhigh thinking levels display with rainbow gradient inspired by Claude Code's ultrathink
|
|
36
|
+
- **Color gradient** — Thinking levels use progressive colors: gray → purple-gray → blue → teal → rainbow
|
|
37
|
+
- **Streaming visibility** — Status bar now renders in footer during streaming so it's always visible
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- Extension statuses appear at end of status bar (last item in default/full/nerd presets)
|
|
41
|
+
- Default preset now includes `thinking` segment after model
|
|
42
|
+
- Thinking level reads from session branch entries for live updates
|
|
43
|
+
- Footer invalidate() now triggers re-render for settings changes
|
|
44
|
+
- Responsive truncation — progressively removes segments on narrow windows instead of hiding status
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
- ANSI color reset after status content to prevent color bleeding
|
|
48
|
+
- ANSI color reset after rainbow text
|
|
49
|
+
|
|
50
|
+
### Removed
|
|
51
|
+
- Unused brain icon definitions
|
|
52
|
+
|
|
53
|
+
## [0.1.0] - 2026-01-10
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
- Initial release
|
|
57
|
+
- Rounded box design rendering in editor top border
|
|
58
|
+
- 18 segment types: pi, model, thinking, path, git, subagents, token_in, token_out, token_total, cost, context_pct, context_total, time_spent, time, session, hostname, cache_read, cache_write
|
|
59
|
+
- 6 presets: default, minimal, compact, full, nerd, ascii
|
|
60
|
+
- 10 separator styles: powerline, powerline-thin, slash, pipe, dot, chevron, star, block, none, ascii
|
|
61
|
+
- Git integration with async status fetching and 1s cache TTL
|
|
62
|
+
- Nerd Font auto-detection for common terminals
|
|
63
|
+
- oh-my-pi dark theme color matching
|
|
64
|
+
- Context percentage warnings at 70%/90%
|
|
65
|
+
- Auto-compact indicator
|
|
66
|
+
- Subscription detection
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# pi-powerline-footer
|
|
2
|
+
|
|
3
|
+
A powerline-style status bar extension for [pi](https://github.com/badlogic/pi-mono), the coding agent. Inspired by [oh-my-pi](https://github.com/can1357/oh-my-pi).
|
|
4
|
+
|
|
5
|
+
<img width="1555" height="171" alt="image" src="https://github.com/user-attachments/assets/7bfc8a5a-29b5-478e-b9c8-c462c032a840" />
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
**Rounded box design** — Status renders directly in the editor's top border, not as a separate footer.
|
|
10
|
+
|
|
11
|
+
**Live thinking level indicator** — Shows current thinking level (`thinking:off`, `thinking:med`, etc.) with color-coded gradient. High and xhigh levels get a rainbow shimmer effect inspired by Claude Code's ultrathink.
|
|
12
|
+
|
|
13
|
+
**Smart defaults** — Nerd Font auto-detection for iTerm, WezTerm, Kitty, Ghostty, and Alacritty with ASCII fallbacks. Colors matched to oh-my-pi's dark theme.
|
|
14
|
+
|
|
15
|
+
**Git integration** — Async status fetching with 1s cache TTL. Automatically invalidates on file writes/edits. Shows branch, staged (+), unstaged (*), and untracked (?) counts.
|
|
16
|
+
|
|
17
|
+
**Context awareness** — Color-coded warnings at 70% (yellow) and 90% (red) context usage. Auto-compact indicator when enabled.
|
|
18
|
+
|
|
19
|
+
**Token intelligence** — Smart formatting (1.2k, 45M), subscription detection (shows "(sub)" vs dollar cost).
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx pi-powerline-footer
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This copies the extension to `~/.pi/agent/extensions/powerline-footer/`. Restart pi to activate.
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Activates automatically. Toggle with `/powerline`, switch presets with `/powerline <name>`.
|
|
32
|
+
|
|
33
|
+
| Preset | Description |
|
|
34
|
+
|--------|-------------|
|
|
35
|
+
| `default` | Model, thinking, path (basename), git, context, tokens, cost |
|
|
36
|
+
| `minimal` | Just path (basename), git, context |
|
|
37
|
+
| `compact` | Model, git, cost, context |
|
|
38
|
+
| `full` | Everything including hostname, time, abbreviated path |
|
|
39
|
+
| `nerd` | Maximum detail for Nerd Font users |
|
|
40
|
+
| `ascii` | Safe for any terminal |
|
|
41
|
+
|
|
42
|
+
**Environment:** `POWERLINE_NERD_FONTS=1` to force Nerd Fonts, `=0` for ASCII.
|
|
43
|
+
|
|
44
|
+
## Thinking Level Display
|
|
45
|
+
|
|
46
|
+
The thinking segment shows live updates when you change thinking level:
|
|
47
|
+
|
|
48
|
+
| Level | Display | Color |
|
|
49
|
+
|-------|---------|-------|
|
|
50
|
+
| off | `thinking:off` | gray |
|
|
51
|
+
| minimal | `thinking:min` | purple-gray |
|
|
52
|
+
| low | `thinking:low` | blue |
|
|
53
|
+
| medium | `thinking:med` | teal |
|
|
54
|
+
| high | `thinking:high` | 🌈 rainbow |
|
|
55
|
+
| xhigh | `thinking:xhigh` | 🌈 rainbow |
|
|
56
|
+
|
|
57
|
+
## Path Display
|
|
58
|
+
|
|
59
|
+
The path segment supports three modes:
|
|
60
|
+
|
|
61
|
+
| Mode | Example | Description |
|
|
62
|
+
|------|---------|-------------|
|
|
63
|
+
| `basename` | `powerline-footer` | Just the directory name (default) |
|
|
64
|
+
| `abbreviated` | `…/extensions/powerline-footer` | Full path with home abbreviated and length limit |
|
|
65
|
+
| `full` | `~/.pi/agent/extensions/powerline-footer` | Complete path with home abbreviated |
|
|
66
|
+
|
|
67
|
+
Configure via preset options: `path: { mode: "full" }`
|
|
68
|
+
|
|
69
|
+
## Segments
|
|
70
|
+
|
|
71
|
+
`pi` · `model` · `thinking` · `path` · `git` · `subagents` · `token_in` · `token_out` · `token_total` · `cost` · `context_pct` · `context_total` · `time_spent` · `time` · `session` · `hostname` · `cache_read` · `cache_write`
|
|
72
|
+
|
|
73
|
+
## Separators
|
|
74
|
+
|
|
75
|
+
`powerline` · `powerline-thin` · `slash` · `pipe` · `dot` · `chevron` · `star` · `block` · `none` · `ascii`
|
package/colors.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// ANSI escape codes for colors
|
|
2
|
+
// Matching oh-my-pi dark theme colors exactly
|
|
3
|
+
|
|
4
|
+
export interface AnsiColors {
|
|
5
|
+
getBgAnsi(r: number, g: number, b: number): string;
|
|
6
|
+
getFgAnsi(r: number, g: number, b: number): string;
|
|
7
|
+
getFgAnsi256(code: number): string;
|
|
8
|
+
reset: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ansi: AnsiColors = {
|
|
12
|
+
getBgAnsi: (r, g, b) => `\x1b[48;2;${r};${g};${b}m`,
|
|
13
|
+
getFgAnsi: (r, g, b) => `\x1b[38;2;${r};${g};${b}m`,
|
|
14
|
+
getFgAnsi256: (code) => `\x1b[38;5;${code}m`,
|
|
15
|
+
reset: "\x1b[0m",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Convert hex to RGB tuple
|
|
19
|
+
function hexToRgb(hex: string): [number, number, number] {
|
|
20
|
+
const h = hex.replace("#", "");
|
|
21
|
+
return [
|
|
22
|
+
parseInt(h.slice(0, 2), 16),
|
|
23
|
+
parseInt(h.slice(2, 4), 16),
|
|
24
|
+
parseInt(h.slice(4, 6), 16),
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// oh-my-pi dark theme colors (exact match)
|
|
29
|
+
const THEME = {
|
|
30
|
+
// Status line colors
|
|
31
|
+
sep: 244, // ANSI 256 gray
|
|
32
|
+
model: "#d787af", // Pink/mauve
|
|
33
|
+
path: "#00afaf", // Teal/cyan
|
|
34
|
+
gitClean: "#5faf5f", // Green
|
|
35
|
+
gitDirty: "#d7af5f", // Gold/orange
|
|
36
|
+
context: "#8787af", // Purple-gray
|
|
37
|
+
spend: "#5fafaf", // Teal
|
|
38
|
+
staged: 70, // ANSI 256 green
|
|
39
|
+
unstaged: 178, // ANSI 256 gold
|
|
40
|
+
untracked: 39, // ANSI 256 blue
|
|
41
|
+
output: 205, // ANSI 256 pink
|
|
42
|
+
cost: 205, // ANSI 256 pink
|
|
43
|
+
subagents: "#febc38", // Accent orange
|
|
44
|
+
|
|
45
|
+
// UI colors
|
|
46
|
+
accent: "#febc38", // Orange (for pi icon)
|
|
47
|
+
border: "#178fb9", // Blue (for box border)
|
|
48
|
+
warning: "#e4c00f", // Yellow
|
|
49
|
+
error: "#fc3a4b", // Red
|
|
50
|
+
text: "", // Default terminal color
|
|
51
|
+
|
|
52
|
+
// Thinking level colors (gradient from dim to bright)
|
|
53
|
+
thinkingOff: "#3d424a", // Dark gray
|
|
54
|
+
thinkingMinimal: "#5f6673", // Dim gray
|
|
55
|
+
thinkingLow: "#178fb9", // Blue
|
|
56
|
+
thinkingMedium: "#0088fa", // Bright blue
|
|
57
|
+
thinkingHigh: "#b281d6", // Purple
|
|
58
|
+
thinkingXhigh: "#e5c1ff", // Bright lavender
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Color name to ANSI code mapping
|
|
62
|
+
type ColorName =
|
|
63
|
+
| "sep" | "model" | "path" | "gitClean" | "gitDirty"
|
|
64
|
+
| "context" | "spend" | "staged" | "unstaged" | "untracked"
|
|
65
|
+
| "output" | "cost" | "subagents" | "accent" | "border"
|
|
66
|
+
| "warning" | "error" | "text"
|
|
67
|
+
| "thinkingOff" | "thinkingMinimal" | "thinkingLow"
|
|
68
|
+
| "thinkingMedium" | "thinkingHigh" | "thinkingXhigh";
|
|
69
|
+
|
|
70
|
+
function getAnsiCode(color: ColorName): string {
|
|
71
|
+
const value = THEME[color as keyof typeof THEME];
|
|
72
|
+
|
|
73
|
+
if (value === undefined || value === "") {
|
|
74
|
+
return ""; // No color, use terminal default
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof value === "number") {
|
|
78
|
+
return ansi.getFgAnsi256(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (typeof value === "string" && value.startsWith("#")) {
|
|
82
|
+
const [r, g, b] = hexToRgb(value);
|
|
83
|
+
return ansi.getFgAnsi(r, g, b);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Helper to apply foreground color only (no reset - caller manages reset)
|
|
90
|
+
export function fgOnly(color: ColorName, text: string): string {
|
|
91
|
+
const code = getAnsiCode(color);
|
|
92
|
+
return code ? `${code}${text}` : text;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Get raw ANSI code for a color
|
|
96
|
+
export function getFgAnsiCode(color: ColorName): string {
|
|
97
|
+
return getAnsiCode(color);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Rainbow colors for ultra/xhigh thinking (matches Claude Code ultrathink)
|
|
101
|
+
const RAINBOW_COLORS = [
|
|
102
|
+
"#b281d6", // purple
|
|
103
|
+
"#d787af", // pink
|
|
104
|
+
"#febc38", // orange
|
|
105
|
+
"#e4c00f", // yellow
|
|
106
|
+
"#89d281", // green
|
|
107
|
+
"#00afaf", // cyan
|
|
108
|
+
"#178fb9", // blue
|
|
109
|
+
"#b281d6", // purple (loop)
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
// Apply rainbow gradient to text (each character gets next color)
|
|
113
|
+
export function rainbow(text: string): string {
|
|
114
|
+
let result = "";
|
|
115
|
+
let colorIndex = 0;
|
|
116
|
+
for (const char of text) {
|
|
117
|
+
if (char === " " || char === ":") {
|
|
118
|
+
result += char;
|
|
119
|
+
} else {
|
|
120
|
+
const [r, g, b] = hexToRgb(RAINBOW_COLORS[colorIndex % RAINBOW_COLORS.length]);
|
|
121
|
+
result += `${ansi.getFgAnsi(r, g, b)}${char}`;
|
|
122
|
+
colorIndex++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return result + ansi.reset;
|
|
126
|
+
}
|
package/git-status.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import type { GitStatus } from "./types.js";
|
|
3
|
+
|
|
4
|
+
interface CachedGitStatus {
|
|
5
|
+
staged: number;
|
|
6
|
+
unstaged: number;
|
|
7
|
+
untracked: number;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface CachedBranch {
|
|
12
|
+
branch: string | null;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const CACHE_TTL_MS = 1000; // 1 second for file status
|
|
17
|
+
const BRANCH_TTL_MS = 500; // Shorter TTL so branch updates quickly after invalidation
|
|
18
|
+
let cachedStatus: CachedGitStatus | null = null;
|
|
19
|
+
let cachedBranch: CachedBranch | null = null;
|
|
20
|
+
let pendingFetch: Promise<void> | null = null;
|
|
21
|
+
let pendingBranchFetch: Promise<void> | null = null;
|
|
22
|
+
let invalidationCounter = 0; // Track invalidations to prevent stale updates
|
|
23
|
+
let branchInvalidationCounter = 0;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse git status --porcelain output
|
|
27
|
+
*
|
|
28
|
+
* Format: XY filename
|
|
29
|
+
* X = index status, Y = working tree status
|
|
30
|
+
* ?? = untracked
|
|
31
|
+
* Other X values = staged
|
|
32
|
+
* Other Y values = unstaged
|
|
33
|
+
*/
|
|
34
|
+
function parseGitStatusOutput(output: string): { staged: number; unstaged: number; untracked: number } {
|
|
35
|
+
let staged = 0;
|
|
36
|
+
let unstaged = 0;
|
|
37
|
+
let untracked = 0;
|
|
38
|
+
|
|
39
|
+
for (const line of output.split("\n")) {
|
|
40
|
+
if (!line) continue;
|
|
41
|
+
const x = line[0];
|
|
42
|
+
const y = line[1];
|
|
43
|
+
|
|
44
|
+
if (x === "?" && y === "?") {
|
|
45
|
+
untracked++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// X position (index/staged)
|
|
50
|
+
if (x && x !== " " && x !== "?") {
|
|
51
|
+
staged++;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Y position (working tree/unstaged)
|
|
55
|
+
if (y && y !== " ") {
|
|
56
|
+
unstaged++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { staged, unstaged, untracked };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Fetch current git branch asynchronously
|
|
65
|
+
*/
|
|
66
|
+
async function fetchGitBranch(): Promise<string | null> {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
const proc = spawn("git", ["branch", "--show-current"], {
|
|
69
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
let stdout = "";
|
|
73
|
+
let resolved = false;
|
|
74
|
+
|
|
75
|
+
const finish = (result: string | null) => {
|
|
76
|
+
if (resolved) return;
|
|
77
|
+
resolved = true;
|
|
78
|
+
clearTimeout(timeoutId);
|
|
79
|
+
resolve(result);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
proc.stdout.on("data", (data) => {
|
|
83
|
+
stdout += data.toString();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
proc.on("close", (code) => {
|
|
87
|
+
if (code !== 0) {
|
|
88
|
+
finish(null);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const branch = stdout.trim();
|
|
92
|
+
finish(branch || null); // Empty string means detached HEAD
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
proc.on("error", () => {
|
|
96
|
+
finish(null);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Timeout after 200ms
|
|
100
|
+
const timeoutId = setTimeout(() => {
|
|
101
|
+
proc.kill();
|
|
102
|
+
finish(null);
|
|
103
|
+
}, 200);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Fetch git status asynchronously
|
|
109
|
+
*/
|
|
110
|
+
async function fetchGitStatus(): Promise<{ staged: number; unstaged: number; untracked: number } | null> {
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
const proc = spawn("git", ["status", "--porcelain"], {
|
|
113
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
let stdout = "";
|
|
117
|
+
let resolved = false;
|
|
118
|
+
|
|
119
|
+
const finish = (result: { staged: number; unstaged: number; untracked: number } | null) => {
|
|
120
|
+
if (resolved) return;
|
|
121
|
+
resolved = true;
|
|
122
|
+
clearTimeout(timeoutId);
|
|
123
|
+
resolve(result);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
proc.stdout.on("data", (data) => {
|
|
127
|
+
stdout += data.toString();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
proc.on("close", (code) => {
|
|
131
|
+
if (code !== 0) {
|
|
132
|
+
finish(null);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
finish(parseGitStatusOutput(stdout));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
proc.on("error", () => {
|
|
139
|
+
finish(null);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Timeout after 500ms
|
|
143
|
+
const timeoutId = setTimeout(() => {
|
|
144
|
+
proc.kill();
|
|
145
|
+
finish(null);
|
|
146
|
+
}, 500);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get the current git branch with caching.
|
|
152
|
+
* Falls back to provider branch if our cache is empty.
|
|
153
|
+
*/
|
|
154
|
+
export function getCurrentBranch(providerBranch: string | null): string | null {
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
|
|
157
|
+
// Return cached if fresh
|
|
158
|
+
if (cachedBranch && now - cachedBranch.timestamp < BRANCH_TTL_MS) {
|
|
159
|
+
return cachedBranch.branch;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Trigger background fetch if not already pending
|
|
163
|
+
if (!pendingBranchFetch) {
|
|
164
|
+
const fetchId = branchInvalidationCounter;
|
|
165
|
+
pendingBranchFetch = fetchGitBranch().then((result) => {
|
|
166
|
+
// Cache result if no invalidation happened (including null for non-git dirs)
|
|
167
|
+
if (fetchId === branchInvalidationCounter) {
|
|
168
|
+
cachedBranch = {
|
|
169
|
+
branch: result,
|
|
170
|
+
timestamp: Date.now(),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
pendingBranchFetch = null;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Return cached branch, or fall back to provider
|
|
178
|
+
return cachedBranch?.branch ?? providerBranch;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get git status with caching.
|
|
183
|
+
* Returns cached value if within TTL, otherwise triggers async fetch.
|
|
184
|
+
* This is designed for synchronous render() calls - returns last known value
|
|
185
|
+
* while refreshing in background.
|
|
186
|
+
*/
|
|
187
|
+
export function getGitStatus(providerBranch: string | null): GitStatus {
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
const branch = getCurrentBranch(providerBranch);
|
|
190
|
+
|
|
191
|
+
// Return cached if fresh
|
|
192
|
+
if (cachedStatus && now - cachedStatus.timestamp < CACHE_TTL_MS) {
|
|
193
|
+
return {
|
|
194
|
+
branch,
|
|
195
|
+
staged: cachedStatus.staged,
|
|
196
|
+
unstaged: cachedStatus.unstaged,
|
|
197
|
+
untracked: cachedStatus.untracked,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Trigger background fetch if not already pending
|
|
202
|
+
if (!pendingFetch) {
|
|
203
|
+
const fetchId = invalidationCounter; // Capture current counter
|
|
204
|
+
pendingFetch = fetchGitStatus().then((result) => {
|
|
205
|
+
// Cache result if no invalidation happened (including null for non-git dirs)
|
|
206
|
+
if (fetchId === invalidationCounter) {
|
|
207
|
+
cachedStatus = result
|
|
208
|
+
? { staged: result.staged, unstaged: result.unstaged, untracked: result.untracked, timestamp: Date.now() }
|
|
209
|
+
: { staged: 0, unstaged: 0, untracked: 0, timestamp: Date.now() };
|
|
210
|
+
}
|
|
211
|
+
pendingFetch = null;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Return last cached or empty
|
|
216
|
+
if (cachedStatus) {
|
|
217
|
+
return {
|
|
218
|
+
branch,
|
|
219
|
+
staged: cachedStatus.staged,
|
|
220
|
+
unstaged: cachedStatus.unstaged,
|
|
221
|
+
untracked: cachedStatus.untracked,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { branch, staged: 0, unstaged: 0, untracked: 0 };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Force refresh git status (call when you know files changed)
|
|
230
|
+
*/
|
|
231
|
+
export function invalidateGitStatus(): void {
|
|
232
|
+
cachedStatus = null;
|
|
233
|
+
invalidationCounter++; // Increment to invalidate any pending fetches
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Force refresh git branch (call when you know branch might have changed)
|
|
238
|
+
*/
|
|
239
|
+
export function invalidateGitBranch(): void {
|
|
240
|
+
cachedBranch = null;
|
|
241
|
+
branchInvalidationCounter++;
|
|
242
|
+
}
|
package/icons.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export interface IconSet {
|
|
2
|
+
pi: string;
|
|
3
|
+
model: string;
|
|
4
|
+
folder: string;
|
|
5
|
+
branch: string;
|
|
6
|
+
git: string;
|
|
7
|
+
tokens: string;
|
|
8
|
+
context: string;
|
|
9
|
+
cost: string;
|
|
10
|
+
time: string;
|
|
11
|
+
agents: string;
|
|
12
|
+
cache: string;
|
|
13
|
+
input: string;
|
|
14
|
+
output: string;
|
|
15
|
+
host: string;
|
|
16
|
+
session: string;
|
|
17
|
+
auto: string;
|
|
18
|
+
warning: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Separator characters
|
|
22
|
+
export const SEP_DOT = " · ";
|
|
23
|
+
|
|
24
|
+
// Thinking level display text (Unicode/ASCII)
|
|
25
|
+
export const THINKING_TEXT_UNICODE: Record<string, string> = {
|
|
26
|
+
minimal: "[min]",
|
|
27
|
+
low: "[low]",
|
|
28
|
+
medium: "[med]",
|
|
29
|
+
high: "[high]",
|
|
30
|
+
xhigh: "[xhi]",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Thinking level display text (Nerd Fonts - with icons)
|
|
34
|
+
export const THINKING_TEXT_NERD: Record<string, string> = {
|
|
35
|
+
minimal: "\u{F0E7} min", // lightning bolt
|
|
36
|
+
low: "\u{F10C} low", // circle outline
|
|
37
|
+
medium: "\u{F192} med", // dot circle
|
|
38
|
+
high: "\u{F111} high", // circle
|
|
39
|
+
xhigh: "\u{F06D} xhi", // fire
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Get thinking text based on font support
|
|
43
|
+
export function getThinkingText(level: string): string | undefined {
|
|
44
|
+
if (hasNerdFonts()) {
|
|
45
|
+
return THINKING_TEXT_NERD[level];
|
|
46
|
+
}
|
|
47
|
+
return THINKING_TEXT_UNICODE[level];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Nerd Font icons (matching oh-my-pi exactly)
|
|
51
|
+
export const NERD_ICONS: IconSet = {
|
|
52
|
+
pi: "\uE22C", // nf-oct-pi (stylized pi icon)
|
|
53
|
+
model: "\uEC19", // nf-md-chip (model/AI chip)
|
|
54
|
+
folder: "\uF115", // nf-fa-folder_open
|
|
55
|
+
branch: "\uF126", // nf-fa-code_fork (git branch)
|
|
56
|
+
git: "\uF1D3", // nf-fa-git (git logo)
|
|
57
|
+
tokens: "\uE26B", // nf-seti-html (tokens symbol)
|
|
58
|
+
context: "\uE70F", // nf-dev-database (database)
|
|
59
|
+
cost: "\uF155", // nf-fa-dollar
|
|
60
|
+
time: "\uF017", // nf-fa-clock_o
|
|
61
|
+
agents: "\uF0C0", // nf-fa-users
|
|
62
|
+
cache: "\uF1C0", // nf-fa-database (cache)
|
|
63
|
+
input: "\uF090", // nf-fa-sign_in (input arrow)
|
|
64
|
+
output: "\uF08B", // nf-fa-sign_out (output arrow)
|
|
65
|
+
host: "\uF109", // nf-fa-laptop (host)
|
|
66
|
+
session: "\uF550", // nf-md-identifier (session id)
|
|
67
|
+
auto: "\u{F0068}", // nf-md-lightning_bolt (auto-compact)
|
|
68
|
+
warning: "\uF071", // nf-fa-warning
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ASCII/Unicode fallback icons (matching oh-my-pi)
|
|
72
|
+
export const ASCII_ICONS: IconSet = {
|
|
73
|
+
pi: "π",
|
|
74
|
+
model: "◈",
|
|
75
|
+
folder: "📁",
|
|
76
|
+
branch: "⎇",
|
|
77
|
+
git: "⎇",
|
|
78
|
+
tokens: "⊛",
|
|
79
|
+
context: "◫",
|
|
80
|
+
cost: "$",
|
|
81
|
+
time: "◷",
|
|
82
|
+
agents: "AG",
|
|
83
|
+
cache: "cache",
|
|
84
|
+
input: "in:",
|
|
85
|
+
output: "out:",
|
|
86
|
+
host: "host",
|
|
87
|
+
session: "id",
|
|
88
|
+
auto: "⚡",
|
|
89
|
+
warning: "⚠",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Separator characters
|
|
93
|
+
export interface SeparatorChars {
|
|
94
|
+
powerlineLeft: string;
|
|
95
|
+
powerlineRight: string;
|
|
96
|
+
powerlineThinLeft: string;
|
|
97
|
+
powerlineThinRight: string;
|
|
98
|
+
slash: string;
|
|
99
|
+
pipe: string;
|
|
100
|
+
block: string;
|
|
101
|
+
space: string;
|
|
102
|
+
asciiLeft: string;
|
|
103
|
+
asciiRight: string;
|
|
104
|
+
dot: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const NERD_SEPARATORS: SeparatorChars = {
|
|
108
|
+
powerlineLeft: "\uE0B0", //
|
|
109
|
+
powerlineRight: "\uE0B2", //
|
|
110
|
+
powerlineThinLeft: "\uE0B1", //
|
|
111
|
+
powerlineThinRight: "\uE0B3", //
|
|
112
|
+
slash: "/",
|
|
113
|
+
pipe: "|",
|
|
114
|
+
block: "█",
|
|
115
|
+
space: " ",
|
|
116
|
+
asciiLeft: ">",
|
|
117
|
+
asciiRight: "<",
|
|
118
|
+
dot: "·",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const ASCII_SEPARATORS: SeparatorChars = {
|
|
122
|
+
powerlineLeft: ">",
|
|
123
|
+
powerlineRight: "<",
|
|
124
|
+
powerlineThinLeft: "|",
|
|
125
|
+
powerlineThinRight: "|",
|
|
126
|
+
slash: "/",
|
|
127
|
+
pipe: "|",
|
|
128
|
+
block: "#",
|
|
129
|
+
space: " ",
|
|
130
|
+
asciiLeft: ">",
|
|
131
|
+
asciiRight: "<",
|
|
132
|
+
dot: ".",
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Detect Nerd Font support (check TERM or specific env var)
|
|
136
|
+
export function hasNerdFonts(): boolean {
|
|
137
|
+
// User can set this env var to force Nerd Fonts
|
|
138
|
+
if (process.env.POWERLINE_NERD_FONTS === "1") return true;
|
|
139
|
+
if (process.env.POWERLINE_NERD_FONTS === "0") return false;
|
|
140
|
+
|
|
141
|
+
// Check for Ghostty (survives into tmux via GHOSTTY_RESOURCES_DIR)
|
|
142
|
+
if (process.env.GHOSTTY_RESOURCES_DIR) return true;
|
|
143
|
+
|
|
144
|
+
// Check common terminals known to support Nerd Fonts (case-insensitive)
|
|
145
|
+
const term = (process.env.TERM_PROGRAM || "").toLowerCase();
|
|
146
|
+
const nerdTerms = ["iterm", "wezterm", "kitty", "ghostty", "alacritty"];
|
|
147
|
+
return nerdTerms.some(t => term.includes(t));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function getIcons(): IconSet {
|
|
151
|
+
return hasNerdFonts() ? NERD_ICONS : ASCII_ICONS;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function getSeparatorChars(): SeparatorChars {
|
|
155
|
+
return hasNerdFonts() ? NERD_SEPARATORS : ASCII_SEPARATORS;
|
|
156
|
+
}
|