@narumitw/pi-statusline 0.20.0 → 0.23.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 +143 -95
- 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 +104 -64
- 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 -38
- /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,85 +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);
|
|
99
|
+
case "provider":
|
|
100
|
+
return segment(name, ctx.model?.provider ?? "no-provider", config, "accent", "header");
|
|
103
101
|
case "model":
|
|
104
|
-
return segment(name,
|
|
102
|
+
return segment(name, shortenModel(ctx.model?.id ?? "no-model"), config, "accent", "header");
|
|
105
103
|
case "thinking":
|
|
106
104
|
return segment(
|
|
107
105
|
name,
|
|
108
|
-
|
|
106
|
+
runtime.thinkingLevel,
|
|
107
|
+
config,
|
|
109
108
|
thinkingColor(runtime.thinkingLevel),
|
|
110
109
|
"header",
|
|
111
110
|
);
|
|
112
111
|
case "branch": {
|
|
113
112
|
const branch = footerData.getGitBranch();
|
|
114
|
-
const pr = branch ?
|
|
115
|
-
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
|
+
);
|
|
116
121
|
}
|
|
117
122
|
case "cwd":
|
|
118
|
-
return segment(name,
|
|
123
|
+
return segment(name, basename(ctx.cwd) || ctx.cwd, config, "accent", "directory");
|
|
119
124
|
case "tools":
|
|
120
|
-
return segment(name, formatToolActivity(runtime),
|
|
125
|
+
return segment(name, formatToolActivity(runtime), config, "accent", "runtime");
|
|
121
126
|
case "context": {
|
|
122
127
|
const usage = ctx.getContextUsage();
|
|
123
128
|
const value =
|
|
124
129
|
usage?.percent === null || usage?.percent === undefined
|
|
125
|
-
? "
|
|
126
|
-
:
|
|
127
|
-
return segment(name, value, contextColor(usage?.percent), "runtime");
|
|
130
|
+
? "?"
|
|
131
|
+
: `${usage.percent.toFixed(0)}%`;
|
|
132
|
+
return segment(name, value, config, contextColor(usage?.percent), "runtime");
|
|
128
133
|
}
|
|
129
134
|
case "tokens": {
|
|
130
135
|
const totals = getTokenTotals(ctx);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
color,
|
|
137
|
-
"runtime",
|
|
138
|
-
);
|
|
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");
|
|
139
141
|
}
|
|
140
142
|
case "cost": {
|
|
141
143
|
const totals = getTokenTotals(ctx);
|
|
142
|
-
return segment(
|
|
144
|
+
return segment(
|
|
145
|
+
name,
|
|
146
|
+
totals.cost.toFixed(totals.cost >= 1 ? 2 : 3),
|
|
147
|
+
config,
|
|
148
|
+
"accent",
|
|
149
|
+
"meter",
|
|
150
|
+
);
|
|
143
151
|
}
|
|
144
152
|
case "time":
|
|
145
|
-
return segment(name,
|
|
153
|
+
return segment(name, formatTime(), config, "accent", "meter");
|
|
146
154
|
case "turn":
|
|
147
|
-
return segment(name,
|
|
155
|
+
return segment(name, `${runtime.turnCount}`, config, "accent", "meter");
|
|
148
156
|
}
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
function segment(
|
|
152
160
|
name: SegmentName,
|
|
153
|
-
|
|
154
|
-
|
|
161
|
+
value: string,
|
|
162
|
+
config: StatuslineConfig,
|
|
163
|
+
color: RenderSegment["color"],
|
|
155
164
|
block: TokyoNightBlockName,
|
|
156
165
|
emphasis = false,
|
|
157
166
|
): RenderSegment {
|
|
158
|
-
return { name, text, color, block, emphasis };
|
|
167
|
+
return { name, text: formatConfiguredSegment(name, value, config), color, block, emphasis };
|
|
159
168
|
}
|
|
160
169
|
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
|
|
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}`;
|
|
164
177
|
}
|
|
165
178
|
|
|
166
179
|
function thinkingColor(level: ThinkingLevel): ThemeColor {
|
|
@@ -217,6 +230,33 @@ export function prLinkFromStatuses(statuses: ReadonlyMap<string, string>): strin
|
|
|
217
230
|
return close === -1 ? undefined : value.slice(open, close + closeMarker.length);
|
|
218
231
|
}
|
|
219
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
|
+
|
|
220
260
|
function getTokenTotals(ctx: ExtensionContext): TokenTotals {
|
|
221
261
|
const totals: TokenTotals = { input: 0, output: 0, cost: 0 };
|
|
222
262
|
|