@narumitw/pi-statusline 0.21.0 → 0.25.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 +139 -91
- package/package.json +3 -4
- package/src/commands.ts +118 -0
- package/src/extension-status.ts +29 -32
- package/src/git-status.ts +11 -3
- package/src/render.ts +103 -65
- package/src/settings.ts +492 -109
- package/src/statusline.ts +51 -19
- package/src/tokyo-night.ts +180 -0
- package/src/types.ts +67 -0
- package/presets/classic.ts +0 -55
- package/presets/tokyo-night.ts +0 -84
- package/presets/types.ts +0 -39
- /package/{presets → src}/ansi.ts +0 -0
package/src/render.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
-
import process from "node:process";
|
|
3
2
|
import type {
|
|
4
3
|
ExtensionAPI,
|
|
5
4
|
ExtensionContext,
|
|
@@ -7,22 +6,21 @@ import type {
|
|
|
7
6
|
Theme,
|
|
8
7
|
ThemeColor,
|
|
9
8
|
} from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
11
|
-
import { renderClassicStatusline } from "../presets/classic.js";
|
|
12
|
-
import { renderTokyoNightStatusline } from "../presets/tokyo-night.js";
|
|
13
|
-
import type {
|
|
14
|
-
PaletteName,
|
|
15
|
-
RenderSegment,
|
|
16
|
-
SegmentName,
|
|
17
|
-
StatuslineConfig,
|
|
18
|
-
TokyoNightBlockName,
|
|
19
|
-
} from "../presets/types.js";
|
|
20
9
|
import {
|
|
21
|
-
formatExtensionStatuses,
|
|
22
10
|
type ExtensionStatusRuntime,
|
|
11
|
+
formatExtensionStatuses,
|
|
23
12
|
wrapExtensionStatusline,
|
|
24
13
|
} from "./extension-status.js";
|
|
25
|
-
import {
|
|
14
|
+
import { formatGitBranchValue, type GitStatusSummary } from "./git-status.js";
|
|
15
|
+
import { renderTokyoNightStatusline } from "./tokyo-night.js";
|
|
16
|
+
import {
|
|
17
|
+
LINE_BREAK_SEGMENT_NAME,
|
|
18
|
+
type RenderItem,
|
|
19
|
+
type RenderSegment,
|
|
20
|
+
type SegmentName,
|
|
21
|
+
type StatuslineConfig,
|
|
22
|
+
type TokyoNightBlockName,
|
|
23
|
+
} from "./types.js";
|
|
26
24
|
|
|
27
25
|
type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
28
26
|
export interface RuntimeState extends ExtensionStatusRuntime {
|
|
@@ -41,39 +39,30 @@ interface TokenTotals {
|
|
|
41
39
|
cost: number;
|
|
42
40
|
}
|
|
43
41
|
const GITHUB_PR_KEY = "github-pr";
|
|
44
|
-
const
|
|
45
|
-
ocean: ["accent", "muted", "success", "warning"],
|
|
46
|
-
sunset: ["warning", "accent", "success", "muted"],
|
|
47
|
-
forest: ["success", "accent", "muted", "warning"],
|
|
48
|
-
candy: ["accent", "warning", "success", "muted"],
|
|
49
|
-
neon: ["accent", "success", "warning", "error"],
|
|
50
|
-
mono: ["muted", "dim"],
|
|
51
|
-
};
|
|
52
|
-
|
|
42
|
+
const GITHUB_PR_STATUS_KEYS = new Set([GITHUB_PR_KEY]);
|
|
53
43
|
export function renderStatusline(
|
|
54
44
|
width: number,
|
|
55
45
|
ctx: ExtensionContext,
|
|
56
46
|
footerData: ReadonlyFooterDataProvider,
|
|
57
|
-
|
|
47
|
+
_theme: Theme,
|
|
58
48
|
config: StatuslineConfig,
|
|
59
49
|
runtime: RuntimeState,
|
|
60
50
|
): string {
|
|
61
51
|
if (width <= 0) return "";
|
|
62
52
|
|
|
63
53
|
const segments = config.segments
|
|
64
|
-
.map((
|
|
54
|
+
.map((name): RenderItem | undefined =>
|
|
55
|
+
name === LINE_BREAK_SEGMENT_NAME
|
|
56
|
+
? { name: LINE_BREAK_SEGMENT_NAME }
|
|
57
|
+
: buildSegment(name, ctx, footerData, config, runtime),
|
|
58
|
+
)
|
|
65
59
|
.filter(
|
|
66
|
-
(segment): segment is
|
|
60
|
+
(segment): segment is RenderItem =>
|
|
61
|
+
segment !== undefined &&
|
|
62
|
+
(segment.name === LINE_BREAK_SEGMENT_NAME || segment.text.length > 0),
|
|
67
63
|
);
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
switch (config.preset) {
|
|
72
|
-
case "classic":
|
|
73
|
-
return renderClassicStatusline(width, segments, theme, config);
|
|
74
|
-
case "tokyo-night":
|
|
75
|
-
return renderTokyoNightStatusline(width, segments);
|
|
76
|
-
}
|
|
65
|
+
return renderTokyoNightStatusline(width, segments, config);
|
|
77
66
|
}
|
|
78
67
|
|
|
79
68
|
export function renderExtensionStatusline(
|
|
@@ -82,87 +71,109 @@ export function renderExtensionStatusline(
|
|
|
82
71
|
theme: Theme,
|
|
83
72
|
config: StatuslineConfig,
|
|
84
73
|
runtime: RuntimeState,
|
|
74
|
+
mainLine: string,
|
|
85
75
|
): string[] {
|
|
86
|
-
const
|
|
76
|
+
const statuses = footerData.getExtensionStatuses();
|
|
77
|
+
const prContext = prContextFromStatuses(statuses);
|
|
78
|
+
const rendersPrInline = prContext !== undefined && mainLine.includes(prContext);
|
|
79
|
+
const status = formatExtensionStatuses(
|
|
80
|
+
statuses,
|
|
81
|
+
theme,
|
|
82
|
+
config,
|
|
83
|
+
runtime,
|
|
84
|
+
rendersPrInline ? GITHUB_PR_STATUS_KEYS : undefined,
|
|
85
|
+
);
|
|
87
86
|
return wrapExtensionStatusline(status, width);
|
|
88
87
|
}
|
|
89
88
|
|
|
90
89
|
function buildSegment(
|
|
91
90
|
name: SegmentName,
|
|
92
|
-
index: number,
|
|
93
91
|
ctx: ExtensionContext,
|
|
94
92
|
footerData: ReadonlyFooterDataProvider,
|
|
95
93
|
config: StatuslineConfig,
|
|
96
94
|
runtime: RuntimeState,
|
|
97
95
|
): RenderSegment | undefined {
|
|
98
|
-
const color = pickColor(config, index);
|
|
99
|
-
|
|
100
96
|
switch (name) {
|
|
101
97
|
case "brand":
|
|
102
|
-
return segment(name, "π", "accent", "header", true);
|
|
98
|
+
return segment(name, "π", config, "accent", "header", true);
|
|
103
99
|
case "provider":
|
|
104
|
-
return segment(name,
|
|
100
|
+
return segment(name, ctx.model?.provider ?? "no-provider", config, "accent", "header");
|
|
105
101
|
case "model":
|
|
106
|
-
return segment(name,
|
|
102
|
+
return segment(name, shortenModel(ctx.model?.id ?? "no-model"), config, "accent", "header");
|
|
107
103
|
case "thinking":
|
|
108
104
|
return segment(
|
|
109
105
|
name,
|
|
110
|
-
|
|
106
|
+
runtime.thinkingLevel,
|
|
107
|
+
config,
|
|
111
108
|
thinkingColor(runtime.thinkingLevel),
|
|
112
109
|
"header",
|
|
113
110
|
);
|
|
114
111
|
case "branch": {
|
|
115
112
|
const branch = footerData.getGitBranch();
|
|
116
|
-
const pr = branch ?
|
|
117
|
-
return segment(
|
|
113
|
+
const pr = branch ? prContextFromStatuses(footerData.getExtensionStatuses()) : undefined;
|
|
114
|
+
return segment(
|
|
115
|
+
name,
|
|
116
|
+
formatGitBranchValue(branch, runtime.gitStatus, pr),
|
|
117
|
+
config,
|
|
118
|
+
"accent",
|
|
119
|
+
"git",
|
|
120
|
+
);
|
|
118
121
|
}
|
|
119
122
|
case "cwd":
|
|
120
|
-
return segment(name,
|
|
123
|
+
return segment(name, basename(ctx.cwd) || ctx.cwd, config, "accent", "directory");
|
|
121
124
|
case "tools":
|
|
122
|
-
return segment(name, formatToolActivity(runtime),
|
|
125
|
+
return segment(name, formatToolActivity(runtime), config, "accent", "runtime");
|
|
123
126
|
case "context": {
|
|
124
127
|
const usage = ctx.getContextUsage();
|
|
125
128
|
const value =
|
|
126
129
|
usage?.percent === null || usage?.percent === undefined
|
|
127
|
-
? "
|
|
128
|
-
:
|
|
129
|
-
return segment(name, value, contextColor(usage?.percent), "runtime");
|
|
130
|
+
? "?"
|
|
131
|
+
: `${usage.percent.toFixed(0)}%`;
|
|
132
|
+
return segment(name, value, config, contextColor(usage?.percent), "runtime");
|
|
130
133
|
}
|
|
131
134
|
case "tokens": {
|
|
132
135
|
const totals = getTokenTotals(ctx);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
color,
|
|
139
|
-
"runtime",
|
|
140
|
-
);
|
|
136
|
+
const value =
|
|
137
|
+
totals.input === 0 && totals.output === 0
|
|
138
|
+
? "tok 0"
|
|
139
|
+
: `↑${formatCount(totals.input)} ↓${formatCount(totals.output)}`;
|
|
140
|
+
return segment(name, value, config, "accent", "runtime");
|
|
141
141
|
}
|
|
142
142
|
case "cost": {
|
|
143
143
|
const totals = getTokenTotals(ctx);
|
|
144
|
-
return segment(
|
|
144
|
+
return segment(
|
|
145
|
+
name,
|
|
146
|
+
totals.cost.toFixed(totals.cost >= 1 ? 2 : 3),
|
|
147
|
+
config,
|
|
148
|
+
"accent",
|
|
149
|
+
"meter",
|
|
150
|
+
);
|
|
145
151
|
}
|
|
146
152
|
case "time":
|
|
147
|
-
return segment(name,
|
|
153
|
+
return segment(name, formatTime(), config, "accent", "meter");
|
|
148
154
|
case "turn":
|
|
149
|
-
return segment(name,
|
|
155
|
+
return segment(name, `${runtime.turnCount}`, config, "accent", "meter");
|
|
150
156
|
}
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
function segment(
|
|
154
160
|
name: SegmentName,
|
|
155
|
-
|
|
156
|
-
|
|
161
|
+
value: string,
|
|
162
|
+
config: StatuslineConfig,
|
|
163
|
+
color: RenderSegment["color"],
|
|
157
164
|
block: TokyoNightBlockName,
|
|
158
165
|
emphasis = false,
|
|
159
166
|
): RenderSegment {
|
|
160
|
-
return { name, text, color, block, emphasis };
|
|
167
|
+
return { name, text: formatConfiguredSegment(name, value, config), color, block, emphasis };
|
|
161
168
|
}
|
|
162
169
|
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
170
|
+
export function formatConfiguredSegment(
|
|
171
|
+
name: SegmentName,
|
|
172
|
+
value: string,
|
|
173
|
+
config: Pick<StatuslineConfig, "segmentText">,
|
|
174
|
+
): string {
|
|
175
|
+
const presentation = config.segmentText[name];
|
|
176
|
+
return `${presentation.prefix}${value}${presentation.suffix}`;
|
|
166
177
|
}
|
|
167
178
|
|
|
168
179
|
function thinkingColor(level: ThinkingLevel): ThemeColor {
|
|
@@ -219,6 +230,33 @@ export function prLinkFromStatuses(statuses: ReadonlyMap<string, string>): strin
|
|
|
219
230
|
return close === -1 ? undefined : value.slice(open, close + closeMarker.length);
|
|
220
231
|
}
|
|
221
232
|
|
|
233
|
+
export function prContextFromStatuses(statuses: ReadonlyMap<string, string>): string | undefined {
|
|
234
|
+
const value = statuses.get(GITHUB_PR_KEY);
|
|
235
|
+
const link = prLinkFromStatuses(statuses);
|
|
236
|
+
if (!value || !link) return undefined;
|
|
237
|
+
|
|
238
|
+
const state = compactPrState(value.replace(link, ""));
|
|
239
|
+
return state ? `${link} · ${state}` : undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function compactPrState(value: string): string | undefined {
|
|
243
|
+
if (/:\s*merged\s*$/.test(value)) return "merged";
|
|
244
|
+
if (/:\s*closed\s*$/.test(value)) return "closed";
|
|
245
|
+
if (/\bdraft\b/.test(value)) return "draft";
|
|
246
|
+
|
|
247
|
+
const failing = /\bchecks failing \((\d+)\)/.exec(value);
|
|
248
|
+
if (failing) return `${failing[1]} failing`;
|
|
249
|
+
if (/\bchanges requested\b/.test(value)) return "changes requested";
|
|
250
|
+
|
|
251
|
+
const pending = /\bchecks pending \((\d+)\)/.exec(value);
|
|
252
|
+
if (pending) return `${pending[1]} pending`;
|
|
253
|
+
if (/\bapproved\b/.test(value)) return "approved";
|
|
254
|
+
if (/\breview required\b/.test(value)) return "review required";
|
|
255
|
+
if (/\bchecks passing\b/.test(value)) return "checks passing";
|
|
256
|
+
if (/\bno checks\b/.test(value)) return "no checks";
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
222
260
|
function getTokenTotals(ctx: ExtensionContext): TokenTotals {
|
|
223
261
|
const totals: TokenTotals = { input: 0, output: 0, cost: 0 };
|
|
224
262
|
|