@narumitw/pi-statusline 0.1.27 → 0.1.29
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 +30 -9
- package/package.json +2 -1
- package/presets/ansi.ts +26 -0
- package/presets/classic.ts +55 -0
- package/presets/tokyo-night.ts +84 -0
- package/presets/types.ts +37 -0
- package/src/statusline.ts +71 -119
package/README.md
CHANGED
|
@@ -8,14 +8,14 @@ Use it to monitor model selection, thinking level, git branch, working directory
|
|
|
8
8
|
|
|
9
9
|
## ✨ Features
|
|
10
10
|
|
|
11
|
-
- Replaces the default Pi footer with a compact
|
|
11
|
+
- Replaces the default Pi footer with a compact preset-based statusline.
|
|
12
12
|
- Shows model, thinking level, git branch, project directory, active tool, context usage, tokens, cost, and clock.
|
|
13
13
|
- Displays compact statuses published through Pi's generic extension status API.
|
|
14
14
|
- Preserves extension-provided status icons when the status text starts with one.
|
|
15
15
|
- Warns when the same extension package is installed from multiple sources.
|
|
16
|
-
- Uses emoji-labeled segments for readability.
|
|
16
|
+
- Uses emoji-labeled segments for readability in both classic and Tokyo Night presets.
|
|
17
17
|
- Adapts to terminal width and truncates safely.
|
|
18
|
-
- Requires no configuration
|
|
18
|
+
- Requires no configuration, with optional preset selection through `PI_STATUSLINE_PRESET`.
|
|
19
19
|
|
|
20
20
|
## 📦 Install
|
|
21
21
|
|
|
@@ -35,22 +35,38 @@ Try this package locally from the repository root:
|
|
|
35
35
|
pi -e ./extensions/pi-statusline
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## 🎨 Presets
|
|
39
|
+
|
|
40
|
+
`pi-statusline` supports presets through the `PI_STATUSLINE_PRESET` environment variable:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
PI_STATUSLINE_PRESET=tokyo-night pi
|
|
44
|
+
PI_STATUSLINE_PRESET=classic pi
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Supported presets:
|
|
48
|
+
|
|
49
|
+
- `tokyo-night` — the default, inspired by the [Starship Tokyo Night preset](https://starship.rs/presets/tokyo-night), using `░▒▓` / `` powerline blocks and the Tokyo Night color ramp.
|
|
50
|
+
- `classic` — a compact Pi-themed statusline with left-aligned `•` separators.
|
|
51
|
+
|
|
52
|
+
Unset or invalid values fall back to `tokyo-night`. Both presets keep the same emoji-labeled information and preserve extension-provided status icons.
|
|
53
|
+
|
|
38
54
|
## 👀 What it shows
|
|
39
55
|
|
|
40
|
-
The default statusline includes:
|
|
56
|
+
The default `tokyo-night` statusline uses a Starship-inspired `░▒▓` / `` powerline layout and includes:
|
|
41
57
|
|
|
42
58
|
- `π` brand marker.
|
|
43
59
|
- 🤖 current model.
|
|
44
60
|
- 🧠 thinking level.
|
|
45
|
-
- 🌿 git branch.
|
|
46
61
|
- 📁 current project directory.
|
|
47
|
-
-
|
|
48
|
-
-
|
|
62
|
+
- 🌿 git branch.
|
|
63
|
+
- ⚙ active or last tool.
|
|
64
|
+
- 🪟 context usage percentage.
|
|
49
65
|
- 🔢 token totals.
|
|
50
|
-
-
|
|
66
|
+
- 💸 estimated cost.
|
|
51
67
|
- 🕒 clock.
|
|
52
68
|
|
|
53
|
-
Statuses from other extensions appear on their own compact line below the main statusline and
|
|
69
|
+
Statuses from other extensions appear on their own compact line below the main statusline and use each preset's separator.
|
|
54
70
|
|
|
55
71
|
`pi-statusline` is extension-agnostic: it consumes Pi's generic extension status API and does not import or depend on status-producing extensions. If an extension wants a custom icon, it should include that icon at the start of its status text, for example `ctx.ui.setStatus("goal", "🎯 active")`. Statuses without a leading icon use the generic `🔌` icon.
|
|
56
72
|
|
|
@@ -76,6 +92,11 @@ Examples:
|
|
|
76
92
|
extensions/pi-statusline/
|
|
77
93
|
├── src/
|
|
78
94
|
│ └── statusline.ts
|
|
95
|
+
├── presets/
|
|
96
|
+
│ ├── ansi.ts
|
|
97
|
+
│ ├── classic.ts
|
|
98
|
+
│ ├── tokyo-night.ts
|
|
99
|
+
│ └── types.ts
|
|
79
100
|
├── README.md
|
|
80
101
|
├── LICENSE
|
|
81
102
|
├── tsconfig.json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-statusline",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"description": "Pi extension that replaces the footer with an information-rich statusline.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
],
|
|
15
15
|
"files": [
|
|
16
16
|
"src",
|
|
17
|
+
"presets",
|
|
17
18
|
"README.md",
|
|
18
19
|
"LICENSE"
|
|
19
20
|
],
|
package/presets/ansi.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function ansiStyle(text: string, colors: { fg?: string; bg?: string }): string {
|
|
2
|
+
const codes = [
|
|
3
|
+
colors.fg ? truecolorCode("38", colors.fg) : undefined,
|
|
4
|
+
colors.bg ? truecolorCode("48", colors.bg) : undefined,
|
|
5
|
+
].filter((code): code is string => code !== undefined);
|
|
6
|
+
if (codes.length === 0) return text;
|
|
7
|
+
return `\u001b[${codes.join(";")}m${text}\u001b[0m`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ansiFg(hex: string, text: string): string {
|
|
11
|
+
return ansiStyle(text, { fg: hex });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function truecolorCode(prefix: "38" | "48", hex: string): string {
|
|
15
|
+
const { red, green, blue } = hexToRgb(hex);
|
|
16
|
+
return `${prefix};2;${red};${green};${blue}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hexToRgb(hex: string): { red: number; green: number; blue: number } {
|
|
20
|
+
const normalized = hex.replace(/^#/, "");
|
|
21
|
+
return {
|
|
22
|
+
red: Number.parseInt(normalized.slice(0, 2), 16),
|
|
23
|
+
green: Number.parseInt(normalized.slice(2, 4), 16),
|
|
24
|
+
blue: Number.parseInt(normalized.slice(4, 6), 16),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
3
|
+
import type { RenderSegment, SeparatorName, StatuslineConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export function renderClassicStatusline(
|
|
6
|
+
width: number,
|
|
7
|
+
segments: RenderSegment[],
|
|
8
|
+
theme: Theme,
|
|
9
|
+
config: StatuslineConfig,
|
|
10
|
+
): string {
|
|
11
|
+
return truncateToWidth(joinSegments(segments, theme, config), width, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function classicExtensionSeparator(theme: Theme): string {
|
|
15
|
+
return theme.fg("dim", " • ");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function joinSegments(segments: RenderSegment[], theme: Theme, config: StatuslineConfig): string {
|
|
19
|
+
const separator = separatorText(config.separator);
|
|
20
|
+
return segments
|
|
21
|
+
.map((segment, index) => styleSegment(segment, index, theme, config))
|
|
22
|
+
.join(theme.fg("dim", separator));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function styleSegment(
|
|
26
|
+
segment: RenderSegment,
|
|
27
|
+
index: number,
|
|
28
|
+
theme: Theme,
|
|
29
|
+
config: StatuslineConfig,
|
|
30
|
+
): string {
|
|
31
|
+
const padding = config.density === "cozy" ? " " : "";
|
|
32
|
+
const text = `${padding}${segment.text}${padding}`;
|
|
33
|
+
const styledText = segment.emphasis ? theme.bold(text) : text;
|
|
34
|
+
|
|
35
|
+
if (config.palette === "mono") {
|
|
36
|
+
return index === 0 ? theme.fg("muted", styledText) : theme.fg("dim", styledText);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return theme.fg(segment.color as ThemeColor, styledText);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function separatorText(separator: SeparatorName): string {
|
|
43
|
+
switch (separator) {
|
|
44
|
+
case "powerline":
|
|
45
|
+
return " ";
|
|
46
|
+
case "bar":
|
|
47
|
+
return " │ ";
|
|
48
|
+
case "round":
|
|
49
|
+
return " ❯ ";
|
|
50
|
+
case "none":
|
|
51
|
+
return " ";
|
|
52
|
+
case "dot":
|
|
53
|
+
return " • ";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
3
|
+
import { ansiFg, ansiStyle } from "./ansi.js";
|
|
4
|
+
import type { RenderSegment, TokyoNightBlockName } from "./types.js";
|
|
5
|
+
|
|
6
|
+
interface TokyoNightBlock {
|
|
7
|
+
name: TokyoNightBlockName;
|
|
8
|
+
segments: RenderSegment[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface BlockColors {
|
|
12
|
+
fg: string;
|
|
13
|
+
bg: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const TOKYO_NIGHT_COLORS = {
|
|
17
|
+
lead: "#a3aed2",
|
|
18
|
+
header: { fg: "#090c0c", bg: "#a3aed2" },
|
|
19
|
+
directory: { fg: "#e3e5e5", bg: "#769ff0" },
|
|
20
|
+
git: { fg: "#769ff0", bg: "#394260" },
|
|
21
|
+
runtime: { fg: "#769ff0", bg: "#212736" },
|
|
22
|
+
meter: { fg: "#a0a9cb", bg: "#1d2230" },
|
|
23
|
+
extensionSeparator: "#394260",
|
|
24
|
+
} as const satisfies Record<string, string | BlockColors>;
|
|
25
|
+
|
|
26
|
+
const TOKYO_NIGHT_BLOCK_ORDER: TokyoNightBlockName[] = [
|
|
27
|
+
"header",
|
|
28
|
+
"directory",
|
|
29
|
+
"git",
|
|
30
|
+
"runtime",
|
|
31
|
+
"meter",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export function renderTokyoNightStatusline(width: number, segments: RenderSegment[]): string {
|
|
35
|
+
return truncateToWidth(joinTokyoNightSegments(segments), width, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function tokyoNightExtensionSeparator(_theme: Theme): string {
|
|
39
|
+
return ansiFg(TOKYO_NIGHT_COLORS.extensionSeparator, " ");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function joinTokyoNightSegments(segments: RenderSegment[]): string {
|
|
43
|
+
const blocks = groupTokyoNightBlocks(segments);
|
|
44
|
+
let line = ansiFg(TOKYO_NIGHT_COLORS.lead, "░▒▓");
|
|
45
|
+
|
|
46
|
+
for (const [index, block] of blocks.entries()) {
|
|
47
|
+
const colors = getTokyoNightBlockColors(block.name);
|
|
48
|
+
const previous =
|
|
49
|
+
index === 0 ? undefined : getTokyoNightBlockColors(blocks[index - 1]?.name ?? "header");
|
|
50
|
+
if (previous) line += ansiStyle("", { fg: previous.bg, bg: colors.bg });
|
|
51
|
+
line += ansiStyle(formatTokyoNightBlockText(block), colors);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lastBlock = blocks.at(-1);
|
|
55
|
+
if (lastBlock) line += ansiFg(getTokyoNightBlockColors(lastBlock.name).bg, "");
|
|
56
|
+
|
|
57
|
+
return line;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function groupTokyoNightBlocks(segments: RenderSegment[]): TokyoNightBlock[] {
|
|
61
|
+
const blocksByName = new Map<TokyoNightBlockName, RenderSegment[]>();
|
|
62
|
+
for (const segment of segments) {
|
|
63
|
+
const blockSegments = blocksByName.get(segment.block) ?? [];
|
|
64
|
+
blockSegments.push(segment);
|
|
65
|
+
blocksByName.set(segment.block, blockSegments);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return TOKYO_NIGHT_BLOCK_ORDER.flatMap((name) => {
|
|
69
|
+
const blockSegments = blocksByName.get(name);
|
|
70
|
+
return blockSegments ? [{ name, segments: blockSegments }] : [];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatTokyoNightBlockText(block: TokyoNightBlock): string {
|
|
75
|
+
return ` ${block.segments.map(formatTokyoNightSegmentText).join(" ")}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatTokyoNightSegmentText(segment: RenderSegment): string {
|
|
79
|
+
return segment.emphasis ? `\u001b[1m${segment.text}\u001b[22m` : segment.text;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getTokyoNightBlockColors(block: TokyoNightBlockName): BlockColors {
|
|
83
|
+
return TOKYO_NIGHT_COLORS[block];
|
|
84
|
+
}
|
package/presets/types.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ThemeColor } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export type SegmentName =
|
|
4
|
+
| "brand"
|
|
5
|
+
| "model"
|
|
6
|
+
| "thinking"
|
|
7
|
+
| "cwd"
|
|
8
|
+
| "branch"
|
|
9
|
+
| "tools"
|
|
10
|
+
| "context"
|
|
11
|
+
| "tokens"
|
|
12
|
+
| "cost"
|
|
13
|
+
| "time"
|
|
14
|
+
| "turn";
|
|
15
|
+
|
|
16
|
+
export type StatuslinePresetName = "classic" | "tokyo-night";
|
|
17
|
+
export type TokyoNightBlockName = "header" | "directory" | "git" | "runtime" | "meter";
|
|
18
|
+
export type PaletteName = "ocean" | "sunset" | "forest" | "candy" | "neon" | "mono";
|
|
19
|
+
export type Density = "compact" | "cozy";
|
|
20
|
+
export type SeparatorName = "dot" | "bar" | "powerline" | "round" | "none";
|
|
21
|
+
|
|
22
|
+
export interface StatuslineConfig {
|
|
23
|
+
preset: StatuslinePresetName;
|
|
24
|
+
palette: PaletteName;
|
|
25
|
+
density: Density;
|
|
26
|
+
separator: SeparatorName;
|
|
27
|
+
showLabels: boolean;
|
|
28
|
+
segments: SegmentName[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RenderSegment {
|
|
32
|
+
name: SegmentName;
|
|
33
|
+
text: string;
|
|
34
|
+
color: ThemeColor;
|
|
35
|
+
block: TokyoNightBlockName;
|
|
36
|
+
emphasis?: boolean;
|
|
37
|
+
}
|
package/src/statusline.ts
CHANGED
|
@@ -8,35 +8,20 @@ import type {
|
|
|
8
8
|
Theme,
|
|
9
9
|
ThemeColor,
|
|
10
10
|
} from "@mariozechner/pi-coding-agent";
|
|
11
|
-
import { truncateToWidth
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
| "cost"
|
|
23
|
-
| "time"
|
|
24
|
-
| "turn";
|
|
25
|
-
|
|
26
|
-
type PaletteName = "ocean" | "sunset" | "forest" | "candy" | "neon" | "mono";
|
|
27
|
-
type Density = "compact" | "cozy";
|
|
28
|
-
type SeparatorName = "dot" | "bar" | "powerline" | "round" | "none";
|
|
11
|
+
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
12
|
+
import { classicExtensionSeparator, renderClassicStatusline } from "../presets/classic.js";
|
|
13
|
+
import { renderTokyoNightStatusline, tokyoNightExtensionSeparator } from "../presets/tokyo-night.js";
|
|
14
|
+
import type {
|
|
15
|
+
PaletteName,
|
|
16
|
+
RenderSegment,
|
|
17
|
+
SegmentName,
|
|
18
|
+
StatuslineConfig,
|
|
19
|
+
StatuslinePresetName,
|
|
20
|
+
TokyoNightBlockName,
|
|
21
|
+
} from "../presets/types.js";
|
|
29
22
|
|
|
30
23
|
type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
31
24
|
|
|
32
|
-
interface StatuslineConfig {
|
|
33
|
-
palette: PaletteName;
|
|
34
|
-
density: Density;
|
|
35
|
-
separator: SeparatorName;
|
|
36
|
-
showLabels: boolean;
|
|
37
|
-
segments: SegmentName[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
25
|
interface RuntimeState {
|
|
41
26
|
turnCount: number;
|
|
42
27
|
activeTools: Map<string, number>;
|
|
@@ -54,21 +39,15 @@ interface TokenTotals {
|
|
|
54
39
|
cost: number;
|
|
55
40
|
}
|
|
56
41
|
|
|
57
|
-
interface RenderSegment {
|
|
58
|
-
name: SegmentName;
|
|
59
|
-
text: string;
|
|
60
|
-
color: ThemeColor;
|
|
61
|
-
emphasis?: boolean;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
42
|
const STATUSLINE_KEY = "statusline";
|
|
43
|
+
const DEFAULT_PRESET: StatuslinePresetName = "tokyo-night";
|
|
65
44
|
|
|
66
45
|
const DEFAULT_SEGMENTS: SegmentName[] = [
|
|
67
46
|
"brand",
|
|
68
47
|
"model",
|
|
69
48
|
"thinking",
|
|
70
|
-
"branch",
|
|
71
49
|
"cwd",
|
|
50
|
+
"branch",
|
|
72
51
|
"tools",
|
|
73
52
|
"context",
|
|
74
53
|
"tokens",
|
|
@@ -76,8 +55,6 @@ const DEFAULT_SEGMENTS: SegmentName[] = [
|
|
|
76
55
|
"time",
|
|
77
56
|
];
|
|
78
57
|
|
|
79
|
-
const RIGHT_SEGMENTS = new Set<SegmentName>(["context", "tokens", "cost", "time", "turn"]);
|
|
80
|
-
|
|
81
58
|
const PALETTES: Record<PaletteName, ThemeColor[]> = {
|
|
82
59
|
ocean: ["accent", "muted", "success", "warning"],
|
|
83
60
|
sunset: ["warning", "accent", "success", "muted"],
|
|
@@ -116,7 +93,13 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
116
93
|
invalidate() {},
|
|
117
94
|
render(width: number): string[] {
|
|
118
95
|
const lines = [renderStatusline(width, ctx, footerData, theme, config, runtime)];
|
|
119
|
-
const extensionStatusLine = renderExtensionStatusline(
|
|
96
|
+
const extensionStatusLine = renderExtensionStatusline(
|
|
97
|
+
width,
|
|
98
|
+
footerData,
|
|
99
|
+
theme,
|
|
100
|
+
config,
|
|
101
|
+
runtime,
|
|
102
|
+
);
|
|
120
103
|
if (extensionStatusLine) lines.push(extensionStatusLine);
|
|
121
104
|
return lines;
|
|
122
105
|
},
|
|
@@ -184,14 +167,21 @@ export default function statusline(pi: ExtensionAPI) {
|
|
|
184
167
|
|
|
185
168
|
function createDefaultConfig(): StatuslineConfig {
|
|
186
169
|
return {
|
|
170
|
+
preset: readStatuslinePreset(),
|
|
187
171
|
palette: "candy",
|
|
188
172
|
density: "compact",
|
|
189
|
-
separator: "
|
|
173
|
+
separator: "dot",
|
|
190
174
|
showLabels: false,
|
|
191
175
|
segments: [...DEFAULT_SEGMENTS],
|
|
192
176
|
};
|
|
193
177
|
}
|
|
194
178
|
|
|
179
|
+
function readStatuslinePreset(): StatuslinePresetName {
|
|
180
|
+
const preset = process.env.PI_STATUSLINE_PRESET?.trim().toLowerCase();
|
|
181
|
+
if (preset === "classic" || preset === "tokyo-night") return preset;
|
|
182
|
+
return DEFAULT_PRESET;
|
|
183
|
+
}
|
|
184
|
+
|
|
195
185
|
function renderStatusline(
|
|
196
186
|
width: number,
|
|
197
187
|
ctx: ExtensionContext,
|
|
@@ -208,38 +198,24 @@ function renderStatusline(
|
|
|
208
198
|
(segment): segment is RenderSegment => segment !== undefined && segment.text.length > 0,
|
|
209
199
|
);
|
|
210
200
|
|
|
211
|
-
|
|
212
|
-
segments.filter((segment) => !RIGHT_SEGMENTS.has(segment.name)),
|
|
213
|
-
theme,
|
|
214
|
-
config,
|
|
215
|
-
);
|
|
216
|
-
const right = joinSegments(
|
|
217
|
-
segments.filter((segment) => RIGHT_SEGMENTS.has(segment.name)),
|
|
218
|
-
theme,
|
|
219
|
-
config,
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
if (!left && !right) return truncateToWidth(theme.fg("dim", "pi-statusline"), width);
|
|
223
|
-
if (!right) return truncateToWidth(left, width);
|
|
224
|
-
if (!left) return truncateToWidth(right, width);
|
|
225
|
-
|
|
226
|
-
const rightWidth = visibleWidth(right);
|
|
227
|
-
if (rightWidth + 1 >= width) return truncateToWidth(right, width);
|
|
201
|
+
if (segments.length === 0) return truncateToWidth(theme.fg("dim", "pi-statusline"), width);
|
|
228
202
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
203
|
+
switch (config.preset) {
|
|
204
|
+
case "classic":
|
|
205
|
+
return renderClassicStatusline(width, segments, theme, config);
|
|
206
|
+
case "tokyo-night":
|
|
207
|
+
return renderTokyoNightStatusline(width, segments);
|
|
208
|
+
}
|
|
234
209
|
}
|
|
235
210
|
|
|
236
211
|
function renderExtensionStatusline(
|
|
237
212
|
width: number,
|
|
238
213
|
footerData: ReadonlyFooterDataProvider,
|
|
239
214
|
theme: Theme,
|
|
215
|
+
config: StatuslineConfig,
|
|
240
216
|
runtime: RuntimeState,
|
|
241
217
|
): string | undefined {
|
|
242
|
-
const status = formatExtensionStatuses(footerData.getExtensionStatuses(), theme, runtime);
|
|
218
|
+
const status = formatExtensionStatuses(footerData.getExtensionStatuses(), theme, config, runtime);
|
|
243
219
|
if (!status) return undefined;
|
|
244
220
|
|
|
245
221
|
return truncateToWidth(status, width, "");
|
|
@@ -257,98 +233,68 @@ function buildSegment(
|
|
|
257
233
|
|
|
258
234
|
switch (name) {
|
|
259
235
|
case "brand":
|
|
260
|
-
return
|
|
236
|
+
return segment(name, "π", "accent", "header", true);
|
|
261
237
|
case "model":
|
|
262
|
-
return
|
|
238
|
+
return segment(name, `🤖 ${shortenModel(ctx.model?.id ?? "no-model")}`, color, "header");
|
|
263
239
|
case "thinking":
|
|
264
|
-
return
|
|
240
|
+
return segment(
|
|
265
241
|
name,
|
|
266
242
|
`🧠 ${runtime.thinkingLevel}`,
|
|
267
243
|
thinkingColor(runtime.thinkingLevel),
|
|
268
|
-
|
|
244
|
+
"header",
|
|
269
245
|
);
|
|
270
246
|
case "branch":
|
|
271
|
-
return
|
|
247
|
+
return segment(name, `🌿 ${footerData.getGitBranch() ?? "no-git"}`, color, "git");
|
|
272
248
|
case "cwd":
|
|
273
|
-
return
|
|
249
|
+
return segment(name, `📁 ${basename(ctx.cwd) || ctx.cwd}`, color, "directory");
|
|
274
250
|
case "tools":
|
|
275
|
-
return
|
|
251
|
+
return segment(name, formatToolActivity(runtime), color, "runtime");
|
|
276
252
|
case "context": {
|
|
277
253
|
const usage = ctx.getContextUsage();
|
|
278
254
|
const value =
|
|
279
255
|
usage?.percent === null || usage?.percent === undefined
|
|
280
256
|
? "🪟 ctx ?"
|
|
281
257
|
: `🪟 ctx ${usage.percent.toFixed(0)}%`;
|
|
282
|
-
return
|
|
258
|
+
return segment(name, value, contextColor(usage?.percent), "runtime");
|
|
283
259
|
}
|
|
284
260
|
case "tokens": {
|
|
285
261
|
const totals = getTokenTotals(ctx);
|
|
286
262
|
if (totals.input === 0 && totals.output === 0)
|
|
287
|
-
return
|
|
288
|
-
return
|
|
263
|
+
return segment(name, "🔢 tok 0", color, "runtime");
|
|
264
|
+
return segment(
|
|
289
265
|
name,
|
|
290
266
|
`🔢 ↑${formatCount(totals.input)} ↓${formatCount(totals.output)}`,
|
|
291
267
|
color,
|
|
292
|
-
|
|
268
|
+
"runtime",
|
|
293
269
|
);
|
|
294
270
|
}
|
|
295
271
|
case "cost": {
|
|
296
272
|
const totals = getTokenTotals(ctx);
|
|
297
|
-
return
|
|
273
|
+
return segment(name, `💸 $${totals.cost.toFixed(totals.cost >= 1 ? 2 : 3)}`, color, "meter");
|
|
298
274
|
}
|
|
299
275
|
case "time":
|
|
300
|
-
return
|
|
276
|
+
return segment(name, `🕒 ${formatTime()}`, color, "meter");
|
|
301
277
|
case "turn":
|
|
302
|
-
return
|
|
278
|
+
return segment(name, `🔁 #${runtime.turnCount}`, color, "meter");
|
|
303
279
|
}
|
|
304
280
|
}
|
|
305
281
|
|
|
306
|
-
function
|
|
282
|
+
function segment(
|
|
307
283
|
name: SegmentName,
|
|
308
|
-
|
|
284
|
+
text: string,
|
|
309
285
|
color: ThemeColor,
|
|
310
|
-
|
|
286
|
+
block: TokyoNightBlockName,
|
|
287
|
+
emphasis = false,
|
|
311
288
|
): RenderSegment {
|
|
312
|
-
|
|
313
|
-
return { name, text: `${name} ${value}`, color };
|
|
289
|
+
return { name, text, color, block, emphasis };
|
|
314
290
|
}
|
|
315
291
|
|
|
316
|
-
function
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
function styleSegment(
|
|
324
|
-
segment: RenderSegment,
|
|
325
|
-
index: number,
|
|
326
|
-
theme: Theme,
|
|
327
|
-
config: StatuslineConfig,
|
|
328
|
-
): string {
|
|
329
|
-
const padding = config.density === "cozy" ? " " : "";
|
|
330
|
-
const text = `${padding}${segment.text}${padding}`;
|
|
331
|
-
const styledText = segment.emphasis ? theme.bold(text) : text;
|
|
332
|
-
|
|
333
|
-
if (config.palette === "mono") {
|
|
334
|
-
return index === 0 ? theme.fg("muted", styledText) : theme.fg("dim", styledText);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return theme.fg(segment.color, styledText);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function separatorText(separator: SeparatorName): string {
|
|
341
|
-
switch (separator) {
|
|
342
|
-
case "powerline":
|
|
343
|
-
return " ";
|
|
344
|
-
case "bar":
|
|
345
|
-
return " │ ";
|
|
346
|
-
case "round":
|
|
347
|
-
return " ❯ ";
|
|
348
|
-
case "none":
|
|
349
|
-
return " ";
|
|
350
|
-
case "dot":
|
|
351
|
-
return " • ";
|
|
292
|
+
function extensionStatusSeparator(presetName: StatuslinePresetName, theme: Theme): string {
|
|
293
|
+
switch (presetName) {
|
|
294
|
+
case "classic":
|
|
295
|
+
return classicExtensionSeparator(theme);
|
|
296
|
+
case "tokyo-night":
|
|
297
|
+
return tokyoNightExtensionSeparator(theme);
|
|
352
298
|
}
|
|
353
299
|
}
|
|
354
300
|
|
|
@@ -397,9 +343,10 @@ function formatToolActivity(runtime: RuntimeState): string {
|
|
|
397
343
|
function formatExtensionStatuses(
|
|
398
344
|
statuses: ReadonlyMap<string, string>,
|
|
399
345
|
theme: Theme,
|
|
346
|
+
config: StatuslineConfig,
|
|
400
347
|
runtime: RuntimeState,
|
|
401
348
|
): string {
|
|
402
|
-
const separator =
|
|
349
|
+
const separator = extensionStatusSeparator(config.preset, theme);
|
|
403
350
|
const visibleStatuses = [
|
|
404
351
|
...formatDuplicateExtensionStatus(runtime, theme),
|
|
405
352
|
...[...statuses.entries()]
|
|
@@ -421,7 +368,8 @@ function formatExtensionStatus(key: string, value: string, theme: Theme): string
|
|
|
421
368
|
function formatDuplicateExtensionStatus(runtime: RuntimeState, theme: Theme): string[] {
|
|
422
369
|
if (runtime.duplicateExtensions.length === 0) return [];
|
|
423
370
|
const names = runtime.duplicateExtensions.slice(0, 2).join(", ");
|
|
424
|
-
const suffix =
|
|
371
|
+
const suffix =
|
|
372
|
+
runtime.duplicateExtensions.length > 2 ? ` +${runtime.duplicateExtensions.length - 2}` : "";
|
|
425
373
|
return [`${theme.fg("warning", "⚠️")} ${theme.fg("warning", `dup ${names}${suffix}`)}`];
|
|
426
374
|
}
|
|
427
375
|
|
|
@@ -494,7 +442,11 @@ function readPackageSources(settingsFile: string): string[] {
|
|
|
494
442
|
return (settings.packages ?? [])
|
|
495
443
|
.map((entry) => {
|
|
496
444
|
if (typeof entry === "string") return entry;
|
|
497
|
-
if (
|
|
445
|
+
if (
|
|
446
|
+
entry &&
|
|
447
|
+
typeof entry === "object" &&
|
|
448
|
+
typeof (entry as { source?: unknown }).source === "string"
|
|
449
|
+
) {
|
|
498
450
|
return (entry as { source: string }).source;
|
|
499
451
|
}
|
|
500
452
|
return undefined;
|