@narumitw/pi-statusline 0.25.0 → 0.31.0
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 +212 -115
- package/package.json +5 -5
- package/src/commands.ts +775 -9
- package/src/extension-status.ts +28 -2
- package/src/index.ts +1 -0
- package/src/information-profiles.ts +39 -0
- package/src/powerline.ts +169 -0
- package/src/presets/candy.ts +9 -0
- package/src/presets/create-ramp.ts +41 -0
- package/src/presets/custom.ts +11 -0
- package/src/presets/forest.ts +9 -0
- package/src/presets/index.ts +54 -0
- package/src/presets/mono.ts +9 -0
- package/src/presets/neon.ts +9 -0
- package/src/presets/ocean.ts +9 -0
- package/src/presets/sunset.ts +9 -0
- package/src/presets/tokyo-night.ts +13 -0
- package/src/presets/types.ts +12 -0
- package/src/render.ts +34 -23
- package/src/settings.ts +135 -23
- package/src/statusline.ts +42 -8
- package/src/types.ts +14 -3
- package/src/tokyo-night.ts +0 -180
package/src/extension-status.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
6
|
+
import { powerlineExtensionSeparator } from "./powerline.js";
|
|
6
7
|
import { DEFAULT_EXTENSION_STATUS_ICONS } from "./settings.js";
|
|
7
|
-
import { tokyoNightExtensionSeparator } from "./tokyo-night.js";
|
|
8
8
|
import type { StatuslineConfig } from "./types.js";
|
|
9
9
|
|
|
10
10
|
export type ExtensionStatusIconAliasMap = ReadonlyMap<string, readonly string[]>;
|
|
@@ -14,9 +14,15 @@ export interface ExtensionStatusRuntime {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const STATUSLINE_KEY = "statusline";
|
|
17
|
+
const COMPATIBLE_STATUS_ICON_KEYS: Readonly<Record<string, string>> = {
|
|
18
|
+
retry: "unknown-error-retry",
|
|
19
|
+
sync: "pisync",
|
|
20
|
+
"unknown-error-retry": "retry",
|
|
21
|
+
pisync: "sync",
|
|
22
|
+
};
|
|
17
23
|
const EMPTY_EXTENSION_STATUS_ICON_ALIASES: ExtensionStatusIconAliasMap = new Map();
|
|
18
24
|
function extensionStatusSeparator(config: StatuslineConfig, theme: Theme): string {
|
|
19
|
-
return
|
|
25
|
+
return powerlineExtensionSeparator(theme, config.palettePreset);
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export function formatExtensionStatuses(
|
|
@@ -69,12 +75,32 @@ function extensionStatusIcon(
|
|
|
69
75
|
extensionStatusIconAliases: ExtensionStatusIconAliasMap,
|
|
70
76
|
) {
|
|
71
77
|
if (Object.hasOwn(configuredIcons, key)) return configuredIcons[key];
|
|
78
|
+
const namespaceIcon = configuredNamespaceIcon(key, configuredIcons);
|
|
79
|
+
if (namespaceIcon !== undefined) return namespaceIcon;
|
|
80
|
+
const compatibleKey = COMPATIBLE_STATUS_ICON_KEYS[key];
|
|
81
|
+
if (compatibleKey && Object.hasOwn(configuredIcons, compatibleKey)) {
|
|
82
|
+
return configuredIcons[compatibleKey];
|
|
83
|
+
}
|
|
72
84
|
for (const alias of extensionStatusAliasesForKey(key, extensionStatusIconAliases)) {
|
|
73
85
|
if (Object.hasOwn(configuredIcons, alias)) return configuredIcons[alias];
|
|
74
86
|
}
|
|
75
87
|
return leadingIcon ?? DEFAULT_EXTENSION_STATUS_ICONS[key] ?? "🔌";
|
|
76
88
|
}
|
|
77
89
|
|
|
90
|
+
function configuredNamespaceIcon(
|
|
91
|
+
key: string,
|
|
92
|
+
configuredIcons: Readonly<Record<string, string>>,
|
|
93
|
+
): string | undefined {
|
|
94
|
+
let match: { baseLength: number; icon: string } | undefined;
|
|
95
|
+
for (const [selector, icon] of Object.entries(configuredIcons)) {
|
|
96
|
+
if (!selector.endsWith(":*")) continue;
|
|
97
|
+
const base = selector.slice(0, -2);
|
|
98
|
+
if (!base || !key.startsWith(`${base}:`)) continue;
|
|
99
|
+
if (!match || base.length > match.baseLength) match = { baseLength: base.length, icon };
|
|
100
|
+
}
|
|
101
|
+
return match?.icon;
|
|
102
|
+
}
|
|
103
|
+
|
|
78
104
|
function extensionStatusAliasesForKey(
|
|
79
105
|
key: string,
|
|
80
106
|
extensionStatusIconAliases: ExtensionStatusIconAliasMap,
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./statusline.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ConfigSegmentName, SegmentName } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const INFORMATION_PROFILE_NAMES = ["minimal", "balanced", "detailed"] as const;
|
|
4
|
+
export type InformationProfileName = (typeof INFORMATION_PROFILE_NAMES)[number];
|
|
5
|
+
export type InformationProfile = InformationProfileName | "custom";
|
|
6
|
+
|
|
7
|
+
export const INFORMATION_PROFILES: Readonly<
|
|
8
|
+
Record<InformationProfileName, readonly SegmentName[]>
|
|
9
|
+
> = {
|
|
10
|
+
minimal: ["model", "cwd", "branch", "context"],
|
|
11
|
+
balanced: ["model", "thinking", "cwd", "branch", "tools", "context", "cost"],
|
|
12
|
+
detailed: [
|
|
13
|
+
"provider",
|
|
14
|
+
"model",
|
|
15
|
+
"thinking",
|
|
16
|
+
"cwd",
|
|
17
|
+
"branch",
|
|
18
|
+
"tools",
|
|
19
|
+
"context",
|
|
20
|
+
"tokens",
|
|
21
|
+
"cost",
|
|
22
|
+
"time",
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function inferInformationProfile(
|
|
27
|
+
segments: readonly ConfigSegmentName[],
|
|
28
|
+
): InformationProfile {
|
|
29
|
+
for (const name of INFORMATION_PROFILE_NAMES) {
|
|
30
|
+
const profile = INFORMATION_PROFILES[name];
|
|
31
|
+
if (
|
|
32
|
+
segments.length === profile.length &&
|
|
33
|
+
segments.every((segment, index) => segment === profile[index])
|
|
34
|
+
) {
|
|
35
|
+
return name;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return "custom";
|
|
39
|
+
}
|
package/src/powerline.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { ansiStyle } from "./ansi.js";
|
|
4
|
+
import { resolvePreset } from "./presets/index.js";
|
|
5
|
+
import type { BlockColors, PowerlinePreset } from "./presets/types.js";
|
|
6
|
+
import {
|
|
7
|
+
LINE_BREAK_SEGMENT_NAME,
|
|
8
|
+
type PalettePreset,
|
|
9
|
+
type PowerlineBlockName,
|
|
10
|
+
type RenderItem,
|
|
11
|
+
type RenderSegment,
|
|
12
|
+
type SegmentPalette,
|
|
13
|
+
type SeparatorName,
|
|
14
|
+
type StatuslineConfig,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
|
|
17
|
+
interface PowerlineBlock {
|
|
18
|
+
baseBlock: PowerlineBlockName;
|
|
19
|
+
colors: BlockColors;
|
|
20
|
+
segments: RenderSegment[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderPowerlineStatusline(
|
|
24
|
+
width: number,
|
|
25
|
+
items: RenderItem[],
|
|
26
|
+
config: Pick<StatuslineConfig, "palettePreset" | "palette" | "density" | "separator">,
|
|
27
|
+
): string {
|
|
28
|
+
if (items.length === 0 || width <= 0) return "";
|
|
29
|
+
return splitLines(items)
|
|
30
|
+
.map((segments) => fitPowerlineSegments(segments, width, config))
|
|
31
|
+
.join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function splitLines(items: RenderItem[]): RenderSegment[][] {
|
|
35
|
+
const lines: RenderSegment[][] = [[]];
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
if (item.name === LINE_BREAK_SEGMENT_NAME) lines.push([]);
|
|
38
|
+
else lines.at(-1)?.push(item);
|
|
39
|
+
}
|
|
40
|
+
return lines;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SEGMENT_RETENTION_PRIORITY: Readonly<Record<RenderSegment["name"], number>> = {
|
|
44
|
+
context: 120,
|
|
45
|
+
model: 110,
|
|
46
|
+
branch: 100,
|
|
47
|
+
tools: 90,
|
|
48
|
+
cwd: 80,
|
|
49
|
+
thinking: 70,
|
|
50
|
+
cost: 60,
|
|
51
|
+
provider: 50,
|
|
52
|
+
tokens: 40,
|
|
53
|
+
time: 30,
|
|
54
|
+
turn: 20,
|
|
55
|
+
brand: 10,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function fitPowerlineSegments(
|
|
59
|
+
segments: readonly RenderSegment[],
|
|
60
|
+
width: number,
|
|
61
|
+
config: Pick<StatuslineConfig, "palettePreset" | "palette" | "density" | "separator">,
|
|
62
|
+
): string {
|
|
63
|
+
if (segments.length === 0) return "";
|
|
64
|
+
const fitted = [...segments];
|
|
65
|
+
while (fitted.length > 1) {
|
|
66
|
+
const rendered = joinPowerlineSegments(fitted, config);
|
|
67
|
+
if (visibleWidth(rendered) <= width) return rendered;
|
|
68
|
+
let removalIndex = 0;
|
|
69
|
+
for (let index = 1; index < fitted.length; index += 1) {
|
|
70
|
+
const candidate = fitted[index];
|
|
71
|
+
const current = fitted[removalIndex];
|
|
72
|
+
if (
|
|
73
|
+
candidate &&
|
|
74
|
+
current &&
|
|
75
|
+
SEGMENT_RETENTION_PRIORITY[candidate.name] < SEGMENT_RETENTION_PRIORITY[current.name]
|
|
76
|
+
) {
|
|
77
|
+
removalIndex = index;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
fitted.splice(removalIndex, 1);
|
|
81
|
+
}
|
|
82
|
+
const rendered = joinPowerlineSegments(fitted, config);
|
|
83
|
+
return visibleWidth(rendered) <= width ? rendered : "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function powerlineExtensionSeparator(
|
|
87
|
+
_theme: Theme,
|
|
88
|
+
palettePreset: PalettePreset = "tokyo-night",
|
|
89
|
+
): string {
|
|
90
|
+
return ansiStyle(" • ", { fg: resolvePreset(palettePreset).extensionSeparator });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function joinPowerlineSegments(
|
|
94
|
+
segments: RenderSegment[],
|
|
95
|
+
config: Pick<StatuslineConfig, "palettePreset" | "palette" | "density" | "separator">,
|
|
96
|
+
): string {
|
|
97
|
+
const preset = resolvePreset(config.palettePreset);
|
|
98
|
+
const blocks = contiguousBlocks(segments, preset, config.palettePreset, config.palette);
|
|
99
|
+
let line = ansiStyle("░▒▓", { fg: preset.lead });
|
|
100
|
+
|
|
101
|
+
for (const [index, block] of blocks.entries()) {
|
|
102
|
+
const previous = index === 0 ? undefined : blocks[index - 1]?.colors;
|
|
103
|
+
if (previous) line += ansiStyle("", { fg: previous.bg, bg: block.colors.bg });
|
|
104
|
+
line += ansiStyle(formatBlockText(block, config), block.colors);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const lastBlock = blocks.at(-1);
|
|
108
|
+
if (lastBlock) line += ansiStyle("", { fg: lastBlock.colors.bg });
|
|
109
|
+
return line;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function contiguousBlocks(
|
|
113
|
+
segments: RenderSegment[],
|
|
114
|
+
preset: PowerlinePreset,
|
|
115
|
+
palettePreset: PalettePreset,
|
|
116
|
+
configuredPalette: SegmentPalette,
|
|
117
|
+
): PowerlineBlock[] {
|
|
118
|
+
const blocks: PowerlineBlock[] = [];
|
|
119
|
+
const usesConfiguredColors = palettePreset === "custom";
|
|
120
|
+
for (const segment of segments) {
|
|
121
|
+
const colors = usesConfiguredColors
|
|
122
|
+
? (configuredPalette[segment.name] ?? {})
|
|
123
|
+
: preset.blocks[segment.block];
|
|
124
|
+
const previous = blocks.at(-1);
|
|
125
|
+
const matchesPrevious =
|
|
126
|
+
previous !== undefined &&
|
|
127
|
+
(usesConfiguredColors
|
|
128
|
+
? colorsEqual(previous.colors, colors)
|
|
129
|
+
: previous.baseBlock === segment.block);
|
|
130
|
+
if (matchesPrevious) previous.segments.push(segment);
|
|
131
|
+
else blocks.push({ baseBlock: segment.block, colors, segments: [segment] });
|
|
132
|
+
}
|
|
133
|
+
return blocks;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function colorsEqual(left: BlockColors, right: BlockColors): boolean {
|
|
137
|
+
return left.fg === right.fg && left.bg === right.bg;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatBlockText(
|
|
141
|
+
block: PowerlineBlock,
|
|
142
|
+
config: Pick<StatuslineConfig, "density" | "separator">,
|
|
143
|
+
): string {
|
|
144
|
+
const texts = block.segments.map(formatSegmentText);
|
|
145
|
+
const separator = separatorText(config.separator, config.density === "cozy");
|
|
146
|
+
const leading = config.density === "cozy" ? " " : " ";
|
|
147
|
+
const trailing = config.density === "cozy" ? " " : "";
|
|
148
|
+
return `${leading}${texts.join(separator)}${trailing}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function formatSegmentText(segment: RenderSegment): string {
|
|
152
|
+
return segment.emphasis ? `\u001b[1m${segment.text}\u001b[22m` : segment.text;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function separatorText(separator: SeparatorName, cozy: boolean): string {
|
|
156
|
+
const padding = cozy ? " " : " ";
|
|
157
|
+
switch (separator) {
|
|
158
|
+
case "dot":
|
|
159
|
+
return `${padding}•${padding}`;
|
|
160
|
+
case "bar":
|
|
161
|
+
return `${padding}│${padding}`;
|
|
162
|
+
case "powerline":
|
|
163
|
+
return `${padding}${padding}`;
|
|
164
|
+
case "round":
|
|
165
|
+
return `${padding}❯${padding}`;
|
|
166
|
+
case "none":
|
|
167
|
+
return padding;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PowerlineBlockName } from "../types.js";
|
|
2
|
+
import type { PowerlinePreset } from "./types.js";
|
|
3
|
+
|
|
4
|
+
const BLOCK_NAMES: PowerlineBlockName[] = ["header", "directory", "git", "runtime", "meter"];
|
|
5
|
+
|
|
6
|
+
type PaletteRamp = readonly [string, string, string, string, string];
|
|
7
|
+
|
|
8
|
+
export function createRampPreset(backgrounds: PaletteRamp): PowerlinePreset {
|
|
9
|
+
return {
|
|
10
|
+
lead: backgrounds[0],
|
|
11
|
+
blocks: Object.fromEntries(
|
|
12
|
+
BLOCK_NAMES.map((block, index) => {
|
|
13
|
+
const background = backgrounds[index];
|
|
14
|
+
return [block, { fg: contrastColor(background), bg: background }];
|
|
15
|
+
}),
|
|
16
|
+
) as Record<PowerlineBlockName, { fg: string; bg: string }>,
|
|
17
|
+
extensionSeparator: backgrounds[2],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function contrastColor(hex: string): string {
|
|
22
|
+
const background = relativeLuminance(hex);
|
|
23
|
+
const dark = relativeLuminance("#090c0c");
|
|
24
|
+
const light = relativeLuminance("#f0f0f0");
|
|
25
|
+
return contrastRatio(background, dark) >= contrastRatio(background, light)
|
|
26
|
+
? "#090c0c"
|
|
27
|
+
: "#f0f0f0";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function contrastRatio(left: number, right: number): number {
|
|
31
|
+
return (Math.max(left, right) + 0.05) / (Math.min(left, right) + 0.05);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function relativeLuminance(hex: string): number {
|
|
35
|
+
const normalized = hex.slice(1);
|
|
36
|
+
const channels = [0, 2, 4].map((offset) => {
|
|
37
|
+
const value = Number.parseInt(normalized.slice(offset, offset + 2), 16) / 255;
|
|
38
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
39
|
+
});
|
|
40
|
+
return 0.2126 * (channels[0] ?? 0) + 0.7152 * (channels[1] ?? 0) + 0.0722 * (channels[2] ?? 0);
|
|
41
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type PaletteName,
|
|
3
|
+
type PalettePreset,
|
|
4
|
+
type PowerlineBlockName,
|
|
5
|
+
SEGMENT_NAMES,
|
|
6
|
+
type SegmentName,
|
|
7
|
+
type SegmentPalette,
|
|
8
|
+
} from "../types.js";
|
|
9
|
+
import { CANDY_PRESET } from "./candy.js";
|
|
10
|
+
import { CUSTOM_PRESET } from "./custom.js";
|
|
11
|
+
import { FOREST_PRESET } from "./forest.js";
|
|
12
|
+
import { MONO_PRESET } from "./mono.js";
|
|
13
|
+
import { NEON_PRESET } from "./neon.js";
|
|
14
|
+
import { OCEAN_PRESET } from "./ocean.js";
|
|
15
|
+
import { SUNSET_PRESET } from "./sunset.js";
|
|
16
|
+
import { TOKYO_NIGHT_PRESET } from "./tokyo-night.js";
|
|
17
|
+
import type { PowerlinePreset } from "./types.js";
|
|
18
|
+
|
|
19
|
+
const PRESETS = {
|
|
20
|
+
"tokyo-night": TOKYO_NIGHT_PRESET,
|
|
21
|
+
ocean: OCEAN_PRESET,
|
|
22
|
+
sunset: SUNSET_PRESET,
|
|
23
|
+
forest: FOREST_PRESET,
|
|
24
|
+
candy: CANDY_PRESET,
|
|
25
|
+
neon: NEON_PRESET,
|
|
26
|
+
mono: MONO_PRESET,
|
|
27
|
+
custom: CUSTOM_PRESET,
|
|
28
|
+
} satisfies Record<PalettePreset, PowerlinePreset>;
|
|
29
|
+
|
|
30
|
+
const SEGMENT_BLOCKS: Record<SegmentName, PowerlineBlockName> = {
|
|
31
|
+
brand: "header",
|
|
32
|
+
provider: "header",
|
|
33
|
+
model: "header",
|
|
34
|
+
thinking: "header",
|
|
35
|
+
cwd: "directory",
|
|
36
|
+
branch: "git",
|
|
37
|
+
tools: "runtime",
|
|
38
|
+
context: "runtime",
|
|
39
|
+
tokens: "runtime",
|
|
40
|
+
cost: "meter",
|
|
41
|
+
time: "meter",
|
|
42
|
+
turn: "meter",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function resolvePreset(preset: PalettePreset): PowerlinePreset {
|
|
46
|
+
return PRESETS[preset];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function segmentPaletteForPreset(preset: PaletteName): SegmentPalette {
|
|
50
|
+
const blocks = PRESETS[preset].blocks;
|
|
51
|
+
return Object.fromEntries(
|
|
52
|
+
SEGMENT_NAMES.map((name) => [name, { ...blocks[SEGMENT_BLOCKS[name]] }]),
|
|
53
|
+
) as SegmentPalette;
|
|
54
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PowerlinePreset } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const TOKYO_NIGHT_PRESET: PowerlinePreset = {
|
|
4
|
+
lead: "#a3aed2",
|
|
5
|
+
blocks: {
|
|
6
|
+
header: { fg: "#090c0c", bg: "#a3aed2" },
|
|
7
|
+
directory: { fg: "#e3e5e5", bg: "#769ff0" },
|
|
8
|
+
git: { fg: "#769ff0", bg: "#394260" },
|
|
9
|
+
runtime: { fg: "#769ff0", bg: "#212736" },
|
|
10
|
+
meter: { fg: "#a0a9cb", bg: "#1d2230" },
|
|
11
|
+
},
|
|
12
|
+
extensionSeparator: "#394260",
|
|
13
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PowerlineBlockName } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export interface BlockColors {
|
|
4
|
+
fg?: string;
|
|
5
|
+
bg?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PowerlinePreset {
|
|
9
|
+
lead?: string;
|
|
10
|
+
blocks: Record<PowerlineBlockName, BlockColors>;
|
|
11
|
+
extensionSeparator?: string;
|
|
12
|
+
}
|
package/src/render.ts
CHANGED
|
@@ -12,22 +12,20 @@ import {
|
|
|
12
12
|
wrapExtensionStatusline,
|
|
13
13
|
} from "./extension-status.js";
|
|
14
14
|
import { formatGitBranchValue, type GitStatusSummary } from "./git-status.js";
|
|
15
|
-
import {
|
|
15
|
+
import { renderPowerlineStatusline } from "./powerline.js";
|
|
16
16
|
import {
|
|
17
17
|
LINE_BREAK_SEGMENT_NAME,
|
|
18
|
+
type PowerlineBlockName,
|
|
18
19
|
type RenderItem,
|
|
19
20
|
type RenderSegment,
|
|
20
21
|
type SegmentName,
|
|
21
22
|
type StatuslineConfig,
|
|
22
|
-
type TokyoNightBlockName,
|
|
23
23
|
} from "./types.js";
|
|
24
24
|
|
|
25
25
|
type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
26
26
|
export interface RuntimeState extends ExtensionStatusRuntime {
|
|
27
27
|
turnCount: number;
|
|
28
28
|
activeTools: Map<string, number>;
|
|
29
|
-
lastTool?: string;
|
|
30
|
-
lastCompletedTool?: string;
|
|
31
29
|
isStreaming: boolean;
|
|
32
30
|
thinkingLevel: ThinkingLevel;
|
|
33
31
|
gitStatus?: GitStatusSummary;
|
|
@@ -50,19 +48,32 @@ export function renderStatusline(
|
|
|
50
48
|
): string {
|
|
51
49
|
if (width <= 0) return "";
|
|
52
50
|
|
|
53
|
-
const segments =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
);
|
|
51
|
+
const rows: Array<{ configuredSegments: number; segments: RenderSegment[] }> = [
|
|
52
|
+
{ configuredSegments: 0, segments: [] },
|
|
53
|
+
];
|
|
54
|
+
for (const name of config.segments) {
|
|
55
|
+
if (name === LINE_BREAK_SEGMENT_NAME) {
|
|
56
|
+
rows.push({ configuredSegments: 0, segments: [] });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const row = rows.at(-1);
|
|
61
|
+
if (!row) continue;
|
|
62
|
+
row.configuredSegments += 1;
|
|
63
|
+
const rendered = buildSegment(name, ctx, footerData, config, runtime);
|
|
64
|
+
if (rendered && rendered.text.length > 0) row.segments.push(rendered);
|
|
65
|
+
}
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
const segments: RenderItem[] = [];
|
|
68
|
+
const renderedRows = rows.filter(
|
|
69
|
+
(row) => row.configuredSegments === 0 || row.segments.length > 0,
|
|
70
|
+
);
|
|
71
|
+
for (const [index, row] of renderedRows.entries()) {
|
|
72
|
+
if (index > 0) segments.push({ name: LINE_BREAK_SEGMENT_NAME });
|
|
73
|
+
segments.push(...row.segments);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return renderPowerlineStatusline(width, segments, config);
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
export function renderExtensionStatusline(
|
|
@@ -121,8 +132,10 @@ function buildSegment(
|
|
|
121
132
|
}
|
|
122
133
|
case "cwd":
|
|
123
134
|
return segment(name, basename(ctx.cwd) || ctx.cwd, config, "accent", "directory");
|
|
124
|
-
case "tools":
|
|
125
|
-
|
|
135
|
+
case "tools": {
|
|
136
|
+
const activity = formatToolActivity(runtime);
|
|
137
|
+
return activity ? segment(name, activity, config, "accent", "runtime") : undefined;
|
|
138
|
+
}
|
|
126
139
|
case "context": {
|
|
127
140
|
const usage = ctx.getContextUsage();
|
|
128
141
|
const value =
|
|
@@ -161,7 +174,7 @@ function segment(
|
|
|
161
174
|
value: string,
|
|
162
175
|
config: StatuslineConfig,
|
|
163
176
|
color: RenderSegment["color"],
|
|
164
|
-
block:
|
|
177
|
+
block: PowerlineBlockName,
|
|
165
178
|
emphasis = false,
|
|
166
179
|
): RenderSegment {
|
|
167
180
|
return { name, text: formatConfiguredSegment(name, value, config), color, block, emphasis };
|
|
@@ -204,7 +217,7 @@ export function contextColor(percent: number | null | undefined): ThemeColor {
|
|
|
204
217
|
return "success";
|
|
205
218
|
}
|
|
206
219
|
|
|
207
|
-
export function formatToolActivity(runtime: RuntimeState): string {
|
|
220
|
+
export function formatToolActivity(runtime: RuntimeState): string | undefined {
|
|
208
221
|
const active = [...runtime.activeTools.entries()];
|
|
209
222
|
if (active.length > 0) {
|
|
210
223
|
const [name, count] = active[0] ?? ["tool", 1];
|
|
@@ -212,9 +225,7 @@ export function formatToolActivity(runtime: RuntimeState): string {
|
|
|
212
225
|
return `⚙ ${name}${suffix}`;
|
|
213
226
|
}
|
|
214
227
|
|
|
215
|
-
|
|
216
|
-
if (runtime.lastCompletedTool) return `✅ ${runtime.lastCompletedTool}`;
|
|
217
|
-
return "💤 idle";
|
|
228
|
+
return runtime.isStreaming ? "💭 thinking" : undefined;
|
|
218
229
|
}
|
|
219
230
|
|
|
220
231
|
export function prLinkFromStatuses(statuses: ReadonlyMap<string, string>): string | undefined {
|