@quandev104/pi-style 0.1.1
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 +35 -0
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/dist/extensions/pi-style.js +7897 -0
- package/dist/extensions/pi-style.js.map +1 -0
- package/extension-src/pi-style/app/command-service.ts +244 -0
- package/extension-src/pi-style/app/commands.ts +12 -0
- package/extension-src/pi-style/app/config-storage.ts +98 -0
- package/extension-src/pi-style/app/doctor.ts +53 -0
- package/extension-src/pi-style/app/index.ts +322 -0
- package/extension-src/pi-style/app/providers.ts +268 -0
- package/extension-src/pi-style/app/render-scheduler.ts +48 -0
- package/extension-src/pi-style/app/runtime.ts +404 -0
- package/extension-src/pi-style/app/snapshot.ts +15 -0
- package/extension-src/pi-style/domain/capabilities.ts +34 -0
- package/extension-src/pi-style/domain/config-authorization.ts +20 -0
- package/extension-src/pi-style/domain/config-diagnostics.ts +20 -0
- package/extension-src/pi-style/domain/config-diff.ts +30 -0
- package/extension-src/pi-style/domain/config-migrations.ts +52 -0
- package/extension-src/pi-style/domain/config-normalization.ts +567 -0
- package/extension-src/pi-style/domain/config-presets.ts +51 -0
- package/extension-src/pi-style/domain/config-types.ts +115 -0
- package/extension-src/pi-style/domain/providers.ts +37 -0
- package/extension-src/pi-style/domain/status-presets.ts +60 -0
- package/extension-src/pi-style/domain/status-renderer.ts +142 -0
- package/extension-src/pi-style/domain/status.ts +430 -0
- package/extension-src/pi-style/domain/theme.ts +232 -0
- package/extension-src/pi-style/features/.gitkeep +0 -0
- package/extension-src/pi-style/features/editor/index.ts +304 -0
- package/extension-src/pi-style/features/messages/boxed-block.ts +74 -0
- package/extension-src/pi-style/features/messages/index.ts +145 -0
- package/extension-src/pi-style/features/messages/special-blocks.ts +281 -0
- package/extension-src/pi-style/features/startup/index.ts +564 -0
- package/extension-src/pi-style/features/startup/logo.ts +151 -0
- package/extension-src/pi-style/features/status-line/index.ts +319 -0
- package/extension-src/pi-style/features/tools/boxed/bash.ts +347 -0
- package/extension-src/pi-style/features/tools/boxed/edit.ts +113 -0
- package/extension-src/pi-style/features/tools/boxed/fallback.ts +67 -0
- package/extension-src/pi-style/features/tools/boxed/find.ts +65 -0
- package/extension-src/pi-style/features/tools/boxed/grep.ts +108 -0
- package/extension-src/pi-style/features/tools/boxed/index.ts +64 -0
- package/extension-src/pi-style/features/tools/boxed/ls.ts +65 -0
- package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +190 -0
- package/extension-src/pi-style/features/tools/boxed/read.ts +204 -0
- package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -0
- package/extension-src/pi-style/features/tools/boxed/shared.ts +157 -0
- package/extension-src/pi-style/features/tools/boxed/write.ts +89 -0
- package/extension-src/pi-style/features/tools/index.ts +480 -0
- package/extension-src/pi-style/pi/commands.ts +17 -0
- package/extension-src/pi-style/pi/compatibility-coordinator.ts +195 -0
- package/extension-src/pi-style/pi/compatibility-probe.ts +645 -0
- package/extension-src/pi-style/pi/compatibility-registry.ts +329 -0
- package/extension-src/pi-style/pi/config-host.ts +52 -0
- package/extension-src/pi-style/pi/config-session.ts +105 -0
- package/extension-src/pi-style/pi/index.ts +77 -0
- package/extension-src/pi-style/pi/operational-state.ts +29 -0
- package/extension-src/pi-style/pi/session-coordinator.ts +187 -0
- package/extension-src/pi-style/pi/session-usage.ts +62 -0
- package/extension-src/pi-style/pi/startup-resources.ts +70 -0
- package/extension-src/pi-style/shared/.gitkeep +0 -0
- package/extension-src/pi-style/shared/ansi.ts +386 -0
- package/extension-src/pi-style/shared/box.ts +805 -0
- package/extension-src/pi-style/shared/disposable-store.ts +28 -0
- package/extension-src/pi-style/shared/elapsed.ts +88 -0
- package/extension-src/pi-style/shared/render-budget.ts +367 -0
- package/extension-src/pi-style/shared/split-diff.ts +692 -0
- package/extension-src/pi-style/shared/theme-extras.ts +292 -0
- package/package.json +68 -0
- package/themes/.gitkeep +0 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import type { ResolvedTheme, SemanticToken } from "./theme.js";
|
|
2
|
+
|
|
3
|
+
export const STATUS_SEGMENT_IDS = [
|
|
4
|
+
"pi",
|
|
5
|
+
"model",
|
|
6
|
+
"thinking",
|
|
7
|
+
"model_effort",
|
|
8
|
+
"path",
|
|
9
|
+
"git",
|
|
10
|
+
"context_pct",
|
|
11
|
+
"context_bar",
|
|
12
|
+
"context_total",
|
|
13
|
+
"auto_compact",
|
|
14
|
+
"token_in",
|
|
15
|
+
"token_out",
|
|
16
|
+
"cache_read",
|
|
17
|
+
"cache_write",
|
|
18
|
+
"cost",
|
|
19
|
+
"time_spent",
|
|
20
|
+
"time",
|
|
21
|
+
"hostname",
|
|
22
|
+
"session",
|
|
23
|
+
"extension_statuses",
|
|
24
|
+
] as const;
|
|
25
|
+
export type StatusSegmentId = (typeof STATUS_SEGMENT_IDS)[number] | (string & {});
|
|
26
|
+
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
27
|
+
export type ContextState = "low" | "medium" | "high" | "critical";
|
|
28
|
+
|
|
29
|
+
export interface GitSnapshot {
|
|
30
|
+
readonly available: boolean;
|
|
31
|
+
readonly branch: string | null;
|
|
32
|
+
readonly staged: number;
|
|
33
|
+
readonly unstaged: number;
|
|
34
|
+
readonly untracked: number;
|
|
35
|
+
readonly ahead?: number;
|
|
36
|
+
readonly behind?: number;
|
|
37
|
+
readonly refreshing: boolean;
|
|
38
|
+
readonly error?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ContextSnapshot {
|
|
42
|
+
readonly currentTokens?: number;
|
|
43
|
+
readonly windowTokens?: number;
|
|
44
|
+
readonly percent?: number;
|
|
45
|
+
readonly state?: ContextState;
|
|
46
|
+
readonly autoCompacting?: boolean;
|
|
47
|
+
readonly customCompaction?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface UsageSnapshot {
|
|
51
|
+
readonly inputTokens: number;
|
|
52
|
+
readonly outputTokens: number;
|
|
53
|
+
readonly cacheReadTokens: number;
|
|
54
|
+
readonly cacheWriteTokens: number;
|
|
55
|
+
readonly cost?: number;
|
|
56
|
+
readonly currency?: string;
|
|
57
|
+
readonly subscriptionMode?: "api" | "subscription" | "unknown";
|
|
58
|
+
readonly streaming: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ExtensionStatus {
|
|
62
|
+
readonly key: string;
|
|
63
|
+
readonly value: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface StatusSnapshot {
|
|
67
|
+
readonly model?: string;
|
|
68
|
+
/** Provider name for the active model (e.g. `deepseek`), when known. */
|
|
69
|
+
readonly provider?: string;
|
|
70
|
+
/** Whether the active model supports reasoning/thinking levels, when known. */
|
|
71
|
+
readonly reasoning?: boolean;
|
|
72
|
+
readonly thinkingLevel?: ThinkingLevel;
|
|
73
|
+
readonly cwd?: string;
|
|
74
|
+
readonly git?: GitSnapshot;
|
|
75
|
+
readonly context?: ContextSnapshot;
|
|
76
|
+
readonly usage?: UsageSnapshot;
|
|
77
|
+
readonly hostname?: string;
|
|
78
|
+
readonly sessionName?: string | undefined;
|
|
79
|
+
readonly sessionId?: string | undefined;
|
|
80
|
+
readonly sessionStartedAt?: number;
|
|
81
|
+
readonly extensionStatuses?: readonly ExtensionStatus[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface StatusSegmentOptions {
|
|
85
|
+
readonly disabled?: boolean;
|
|
86
|
+
readonly label?: string;
|
|
87
|
+
readonly [key: string]: unknown;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface StatusCustomItem {
|
|
91
|
+
readonly id: string;
|
|
92
|
+
readonly statusKey: string;
|
|
93
|
+
readonly label?: string;
|
|
94
|
+
readonly priority?: number;
|
|
95
|
+
readonly placement?: "left" | "right" | "secondary";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface SegmentContext {
|
|
99
|
+
readonly snapshot: StatusSnapshot;
|
|
100
|
+
readonly theme: ResolvedTheme;
|
|
101
|
+
readonly options: Readonly<Record<string, StatusSegmentOptions | undefined>>;
|
|
102
|
+
readonly width: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface SegmentRenderResult {
|
|
106
|
+
readonly visible: boolean;
|
|
107
|
+
readonly content: string;
|
|
108
|
+
readonly compactContent?: string;
|
|
109
|
+
readonly minWidth?: number;
|
|
110
|
+
readonly truncatable?: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface StatusSegment {
|
|
114
|
+
readonly id: StatusSegmentId;
|
|
115
|
+
readonly defaultPriority: number;
|
|
116
|
+
readonly essential?: boolean;
|
|
117
|
+
readonly overflow?: "primary" | "secondary" | "drop";
|
|
118
|
+
render(context: SegmentContext): SegmentRenderResult;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface StatusLayout {
|
|
122
|
+
readonly left: readonly StatusSegmentId[];
|
|
123
|
+
readonly right: readonly StatusSegmentId[];
|
|
124
|
+
readonly secondary: readonly StatusSegmentId[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface StatusRenderResult {
|
|
128
|
+
readonly primary: string;
|
|
129
|
+
readonly secondary?: string;
|
|
130
|
+
readonly lines: readonly string[];
|
|
131
|
+
readonly visibleSegments: readonly StatusSegmentId[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function segment(
|
|
135
|
+
id: StatusSegmentId,
|
|
136
|
+
priority: number,
|
|
137
|
+
render: StatusSegment["render"],
|
|
138
|
+
essential = false,
|
|
139
|
+
): StatusSegment {
|
|
140
|
+
return { id, defaultPriority: priority, essential, overflow: essential ? "primary" : "secondary", render };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createBuiltinSegments(): ReadonlyMap<StatusSegmentId, StatusSegment> {
|
|
144
|
+
const segments: StatusSegment[] = [
|
|
145
|
+
segment("pi", 10, ({ theme }) => ({
|
|
146
|
+
visible: true,
|
|
147
|
+
content: theme.apply("accent", `${theme.glyph("pi")} pi`),
|
|
148
|
+
compactContent: theme.apply("accent", theme.glyph("pi")),
|
|
149
|
+
})),
|
|
150
|
+
segment(
|
|
151
|
+
"model",
|
|
152
|
+
100,
|
|
153
|
+
({ snapshot, theme }) => ({
|
|
154
|
+
visible: Boolean(snapshot.model),
|
|
155
|
+
content: theme.apply("model", snapshot.model ?? ""),
|
|
156
|
+
compactContent: theme.apply("model", snapshot.model ?? ""),
|
|
157
|
+
truncatable: true,
|
|
158
|
+
}),
|
|
159
|
+
true,
|
|
160
|
+
),
|
|
161
|
+
segment(
|
|
162
|
+
"model_effort",
|
|
163
|
+
40,
|
|
164
|
+
({ snapshot, theme }) => {
|
|
165
|
+
const model = snapshot.model;
|
|
166
|
+
if (!model) return { visible: false, content: "" };
|
|
167
|
+
const label = snapshot.provider ? `(${snapshot.provider}) ${model}` : model;
|
|
168
|
+
const styledLabel = theme.apply("model", label);
|
|
169
|
+
const effort = effortLevel(snapshot);
|
|
170
|
+
if (!effort) {
|
|
171
|
+
return { visible: true, content: styledLabel, compactContent: theme.apply("model", model) };
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
visible: true,
|
|
175
|
+
content: `${styledLabel} ${theme.apply("separator", "•")} ${styleEffort(theme, effort)}`,
|
|
176
|
+
compactContent: theme.apply("model", model),
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
false,
|
|
180
|
+
),
|
|
181
|
+
segment(
|
|
182
|
+
"thinking",
|
|
183
|
+
95,
|
|
184
|
+
({ snapshot, theme }) => {
|
|
185
|
+
const level = snapshot.thinkingLevel;
|
|
186
|
+
if (!level) return { visible: false, content: "" };
|
|
187
|
+
const label = thinkingLabel(level);
|
|
188
|
+
const text = `think:${label}`;
|
|
189
|
+
const compactText = `t:${label}`;
|
|
190
|
+
if (level === "high" || level === "xhigh" || level === "max") {
|
|
191
|
+
return { visible: true, content: theme.rainbow(text), compactContent: theme.rainbow(compactText) };
|
|
192
|
+
}
|
|
193
|
+
const token =
|
|
194
|
+
level === "minimal"
|
|
195
|
+
? "thinkingMinimal"
|
|
196
|
+
: level === "low"
|
|
197
|
+
? "thinkingLow"
|
|
198
|
+
: level === "medium"
|
|
199
|
+
? "thinkingMedium"
|
|
200
|
+
: "thinking";
|
|
201
|
+
return {
|
|
202
|
+
visible: true,
|
|
203
|
+
content: theme.apply(token, text),
|
|
204
|
+
compactContent: theme.apply(token, compactText),
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
true,
|
|
208
|
+
),
|
|
209
|
+
segment("path", 80, ({ snapshot, theme }) => {
|
|
210
|
+
const name = snapshot.cwd?.split(/[\\/]/).filter(Boolean).at(-1) ?? snapshot.cwd;
|
|
211
|
+
return {
|
|
212
|
+
visible: Boolean(snapshot.cwd),
|
|
213
|
+
content: theme.apply("path", name ?? ""),
|
|
214
|
+
compactContent: theme.apply("path", name ?? ""),
|
|
215
|
+
truncatable: true,
|
|
216
|
+
};
|
|
217
|
+
}),
|
|
218
|
+
segment("git", 75, ({ snapshot, theme }) => {
|
|
219
|
+
const git = snapshot.git;
|
|
220
|
+
if (!git?.available || !git.branch) return { visible: false, content: "" };
|
|
221
|
+
const counts = `${git.staged ? ` +${git.staged}` : ""}${git.unstaged ? ` *${git.unstaged}` : ""}${git.untracked ? ` ?${git.untracked}` : ""}`;
|
|
222
|
+
const token = git.staged || git.unstaged || git.untracked ? "gitDirty" : "gitClean";
|
|
223
|
+
return {
|
|
224
|
+
visible: true,
|
|
225
|
+
content: theme.apply(token, `${theme.glyph("git")} ${git.branch}${counts}`),
|
|
226
|
+
compactContent: theme.apply(token, `${theme.glyph("git")} ${git.branch}`),
|
|
227
|
+
};
|
|
228
|
+
}),
|
|
229
|
+
segment(
|
|
230
|
+
"context_pct",
|
|
231
|
+
90,
|
|
232
|
+
({ snapshot, theme }) => {
|
|
233
|
+
const percent = contextPercent(snapshot.context ?? {});
|
|
234
|
+
const state = contextState(percent);
|
|
235
|
+
const token =
|
|
236
|
+
state === "critical"
|
|
237
|
+
? "contextCritical"
|
|
238
|
+
: state === "high"
|
|
239
|
+
? "contextHigh"
|
|
240
|
+
: state === "medium"
|
|
241
|
+
? "contextMedium"
|
|
242
|
+
: "contextLow";
|
|
243
|
+
return {
|
|
244
|
+
visible: percent !== undefined,
|
|
245
|
+
content: percent === undefined ? "" : theme.apply(token, `ctx:${Math.round(percent)}%`),
|
|
246
|
+
compactContent: percent === undefined ? "" : theme.apply(token, `${Math.round(percent)}%`),
|
|
247
|
+
};
|
|
248
|
+
},
|
|
249
|
+
true,
|
|
250
|
+
),
|
|
251
|
+
segment("context_bar", 70, ({ snapshot, theme, options }) => {
|
|
252
|
+
const percent = contextPercent(snapshot.context ?? {});
|
|
253
|
+
if (percent === undefined) return { visible: false, content: "" };
|
|
254
|
+
const token = contextBarToken(percent);
|
|
255
|
+
const window = snapshot.context?.windowTokens;
|
|
256
|
+
const label = `ctx${window !== undefined ? ` (${formatTokens(window)})` : ""}:`;
|
|
257
|
+
const width = (options["context_bar"]?.width as number | undefined) ?? CONTEXT_BAR_WIDTH;
|
|
258
|
+
return {
|
|
259
|
+
visible: true,
|
|
260
|
+
content: `${theme.apply("muted", label)} ${theme.apply(token, contextBar(percent, width))} ${theme.apply(token, `${Math.round(percent)}%`)}`,
|
|
261
|
+
compactContent: theme.apply(token, `${Math.round(percent)}%`),
|
|
262
|
+
};
|
|
263
|
+
}),
|
|
264
|
+
segment("context_total", 60, ({ snapshot, theme }) => ({
|
|
265
|
+
visible: snapshot.context?.currentTokens !== undefined && snapshot.context.windowTokens !== undefined,
|
|
266
|
+
content:
|
|
267
|
+
snapshot.context?.currentTokens !== undefined && snapshot.context.windowTokens !== undefined
|
|
268
|
+
? theme.apply("contextLow", `${snapshot.context.currentTokens}/${snapshot.context.windowTokens}`)
|
|
269
|
+
: "",
|
|
270
|
+
})),
|
|
271
|
+
segment("auto_compact", 55, ({ snapshot, theme }) => ({
|
|
272
|
+
visible: Boolean(snapshot.context?.autoCompacting || snapshot.context?.customCompaction),
|
|
273
|
+
content: theme.apply("warning", snapshot.context?.customCompaction ?? "compacting"),
|
|
274
|
+
compactContent: theme.apply("warning", "compact"),
|
|
275
|
+
})),
|
|
276
|
+
segment("token_in", 50, ({ snapshot, theme }) => ({
|
|
277
|
+
visible: snapshot.usage?.inputTokens !== undefined,
|
|
278
|
+
content: theme.apply("tokens", `in:${snapshot.usage?.inputTokens ?? 0}`),
|
|
279
|
+
compactContent: theme.apply("tokens", `i:${snapshot.usage?.inputTokens ?? 0}`),
|
|
280
|
+
})),
|
|
281
|
+
segment("token_out", 50, ({ snapshot, theme }) => ({
|
|
282
|
+
visible: snapshot.usage?.outputTokens !== undefined,
|
|
283
|
+
content: theme.apply("tokens", `out:${snapshot.usage?.outputTokens ?? 0}`),
|
|
284
|
+
compactContent: theme.apply("tokens", `o:${snapshot.usage?.outputTokens ?? 0}`),
|
|
285
|
+
})),
|
|
286
|
+
segment("cache_read", 40, ({ snapshot, theme }) => ({
|
|
287
|
+
visible: Boolean(snapshot.usage?.cacheReadTokens),
|
|
288
|
+
content: theme.apply("cache", `cache:${snapshot.usage?.cacheReadTokens ?? 0}`),
|
|
289
|
+
compactContent: theme.apply("cache", `cr:${snapshot.usage?.cacheReadTokens ?? 0}`),
|
|
290
|
+
})),
|
|
291
|
+
segment("cache_write", 35, ({ snapshot, theme }) => ({
|
|
292
|
+
visible: Boolean(snapshot.usage?.cacheWriteTokens),
|
|
293
|
+
content: theme.apply("cache", `cw:${snapshot.usage?.cacheWriteTokens ?? 0}`),
|
|
294
|
+
})),
|
|
295
|
+
segment("cost", 65, ({ snapshot, theme }) => {
|
|
296
|
+
const cost = snapshot.usage?.cost;
|
|
297
|
+
const content = cost === undefined || cost <= 0 ? "" : theme.apply("cost", `$${cost.toFixed(3)}`);
|
|
298
|
+
return { visible: Boolean(content), content, compactContent: content };
|
|
299
|
+
}),
|
|
300
|
+
segment("time_spent", 25, ({ snapshot, theme }) => ({
|
|
301
|
+
visible: snapshot.sessionStartedAt !== undefined,
|
|
302
|
+
content:
|
|
303
|
+
snapshot.sessionStartedAt === undefined
|
|
304
|
+
? ""
|
|
305
|
+
: theme.apply("time", formatElapsed(Date.now() - snapshot.sessionStartedAt)),
|
|
306
|
+
compactContent:
|
|
307
|
+
snapshot.sessionStartedAt === undefined
|
|
308
|
+
? ""
|
|
309
|
+
: theme.apply("time", formatElapsed(Date.now() - snapshot.sessionStartedAt)),
|
|
310
|
+
})),
|
|
311
|
+
segment("time", 20, ({ theme }) => ({
|
|
312
|
+
visible: true,
|
|
313
|
+
content: theme.apply("time", new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })),
|
|
314
|
+
compactContent: theme.apply("time", new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })),
|
|
315
|
+
})),
|
|
316
|
+
segment("hostname", 20, ({ snapshot, theme }) => ({
|
|
317
|
+
visible: Boolean(snapshot.hostname),
|
|
318
|
+
content: theme.apply("muted", snapshot.hostname ?? ""),
|
|
319
|
+
})),
|
|
320
|
+
segment("session", 20, ({ snapshot, theme }) => ({
|
|
321
|
+
visible: Boolean(snapshot.sessionName || snapshot.sessionId),
|
|
322
|
+
content: theme.apply("muted", snapshot.sessionName ?? snapshot.sessionId ?? ""),
|
|
323
|
+
})),
|
|
324
|
+
segment("extension_statuses", 30, ({ snapshot, theme }) => {
|
|
325
|
+
const statuses = snapshot.extensionStatuses;
|
|
326
|
+
if (!statuses || statuses.length === 0) return { visible: false, content: "" };
|
|
327
|
+
// Values already carry their display label (extensions publish via
|
|
328
|
+
// `ctx.ui.setStatus(key, text)` where text is the visible string).
|
|
329
|
+
// Mirror Pi's native footer: sort by key, join the values only.
|
|
330
|
+
const text = [...statuses]
|
|
331
|
+
.sort((a, b) => a.key.localeCompare(b.key))
|
|
332
|
+
.map((item) => item.value)
|
|
333
|
+
.join(" ");
|
|
334
|
+
return { visible: true, content: theme.apply("muted", text) };
|
|
335
|
+
}),
|
|
336
|
+
];
|
|
337
|
+
return new Map(segments.map((item) => [item.id, item]));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const CONTEXT_BAR_WIDTH = 10;
|
|
341
|
+
|
|
342
|
+
function contextBar(percent: number, width = CONTEXT_BAR_WIDTH): string {
|
|
343
|
+
const filled = Math.max(0, Math.min(width, Math.round((percent / 100) * width)));
|
|
344
|
+
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Bar colors: green under 50%, yellow from 50% to 70%, red above 70%. */
|
|
348
|
+
function contextBarToken(percent: number): SemanticToken {
|
|
349
|
+
if (percent > 70) return "error";
|
|
350
|
+
if (percent >= 50) return "warning";
|
|
351
|
+
return "success";
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function formatTokens(value: number): string {
|
|
355
|
+
if (value >= 1_000_000) {
|
|
356
|
+
const millions = value / 1_000_000;
|
|
357
|
+
return `${Number.isInteger(millions) ? millions : millions.toFixed(1)}M`;
|
|
358
|
+
}
|
|
359
|
+
if (value >= 1_000) {
|
|
360
|
+
const thousands = value / 1_000;
|
|
361
|
+
return `${Number.isInteger(thousands) ? thousands : thousands.toFixed(1)}K`;
|
|
362
|
+
}
|
|
363
|
+
return String(value);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function effortLevel(snapshot: StatusSnapshot): ThinkingLevel | undefined {
|
|
367
|
+
const level = snapshot.thinkingLevel;
|
|
368
|
+
if (!level) return undefined;
|
|
369
|
+
// Reasoning models always surface an effort level (defaulting to off); other
|
|
370
|
+
// models only show a level when one is actively set.
|
|
371
|
+
if (snapshot.reasoning === true) return level;
|
|
372
|
+
return level === "off" ? undefined : level;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function styleEffort(theme: ResolvedTheme, level: ThinkingLevel): string {
|
|
376
|
+
if (level === "high" || level === "xhigh" || level === "max") return theme.rainbow(level);
|
|
377
|
+
const token =
|
|
378
|
+
level === "minimal"
|
|
379
|
+
? "thinkingMinimal"
|
|
380
|
+
: level === "low"
|
|
381
|
+
? "thinkingLow"
|
|
382
|
+
: level === "medium"
|
|
383
|
+
? "thinkingMedium"
|
|
384
|
+
: "thinking";
|
|
385
|
+
return theme.apply(token, level);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function formatElapsed(ms: number): string {
|
|
389
|
+
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
390
|
+
return `${Math.floor(seconds / 3600)}:${String(Math.floor((seconds % 3600) / 60)).padStart(2, "0")}:${String(seconds % 60).padStart(2, "0")}`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function thinkingLabel(level: ThinkingLevel): string {
|
|
394
|
+
return level === "minimal" ? "min" : level === "medium" ? "med" : level;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function contextState(percent: number | undefined): ContextState | undefined {
|
|
398
|
+
if (percent === undefined || !Number.isFinite(percent)) return undefined;
|
|
399
|
+
if (percent >= 90) return "critical";
|
|
400
|
+
if (percent >= 75) return "high";
|
|
401
|
+
if (percent >= 50) return "medium";
|
|
402
|
+
return "low";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function normalizeThinkingLevel(value: unknown): ThinkingLevel {
|
|
406
|
+
return ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value as string)
|
|
407
|
+
? (value as ThinkingLevel)
|
|
408
|
+
: "off";
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function contextPercent(
|
|
412
|
+
value: Pick<ContextSnapshot, "currentTokens" | "windowTokens" | "percent">,
|
|
413
|
+
): number | undefined {
|
|
414
|
+
const percent =
|
|
415
|
+
value.percent ??
|
|
416
|
+
(value.currentTokens !== undefined && value.windowTokens
|
|
417
|
+
? (value.currentTokens / value.windowTokens) * 100
|
|
418
|
+
: undefined);
|
|
419
|
+
return percent === undefined ? undefined : Math.max(0, Math.min(100, percent));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function contextSemanticToken(state: ContextState): SemanticToken {
|
|
423
|
+
return state === "critical"
|
|
424
|
+
? "contextCritical"
|
|
425
|
+
: state === "high"
|
|
426
|
+
? "contextHigh"
|
|
427
|
+
: state === "medium"
|
|
428
|
+
? "contextMedium"
|
|
429
|
+
: "contextLow";
|
|
430
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { NormalizedPiStyleConfig } from "./config-types.js";
|
|
2
|
+
|
|
3
|
+
export type GlyphMode = "nerd" | "unicode" | "ascii";
|
|
4
|
+
export type SemanticToken =
|
|
5
|
+
| "surface"
|
|
6
|
+
| "surfaceRaised"
|
|
7
|
+
| "surfaceMuted"
|
|
8
|
+
| "text"
|
|
9
|
+
| "muted"
|
|
10
|
+
| "dim"
|
|
11
|
+
| "accent"
|
|
12
|
+
| "accentStrong"
|
|
13
|
+
| "border"
|
|
14
|
+
| "borderMuted"
|
|
15
|
+
| "borderActive"
|
|
16
|
+
| "success"
|
|
17
|
+
| "warning"
|
|
18
|
+
| "error"
|
|
19
|
+
| "model"
|
|
20
|
+
| "thinking"
|
|
21
|
+
| "thinkingMinimal"
|
|
22
|
+
| "thinkingLow"
|
|
23
|
+
| "thinkingMedium"
|
|
24
|
+
| "thinkingHigh"
|
|
25
|
+
| "thinkingXhigh"
|
|
26
|
+
| "thinkingMax"
|
|
27
|
+
| "path"
|
|
28
|
+
| "gitClean"
|
|
29
|
+
| "gitDirty"
|
|
30
|
+
| "contextLow"
|
|
31
|
+
| "contextMedium"
|
|
32
|
+
| "contextHigh"
|
|
33
|
+
| "contextCritical"
|
|
34
|
+
| "tokens"
|
|
35
|
+
| "cache"
|
|
36
|
+
| "cost"
|
|
37
|
+
| "time"
|
|
38
|
+
| "separator";
|
|
39
|
+
|
|
40
|
+
/** Structural view of Pi's theme. fg/bg follow Pi's `(color, text) => string` shape. */
|
|
41
|
+
export interface ActiveTheme {
|
|
42
|
+
fg?: (color: string, text: string) => string;
|
|
43
|
+
bg?: (color: string, text: string) => string;
|
|
44
|
+
colors?: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ResolvedTheme {
|
|
48
|
+
/** ANSI color prefix for a semantic token, or "" when uncolored. */
|
|
49
|
+
readonly color: (token: SemanticToken) => string;
|
|
50
|
+
/** Prefix + text + reset; returns plain text when the token is uncolored. */
|
|
51
|
+
readonly apply: (token: SemanticToken, text: string) => string;
|
|
52
|
+
/** Rainbow gradient text (used for high thinking levels); plain when uncolored. */
|
|
53
|
+
readonly rainbow: (text: string) => string;
|
|
54
|
+
readonly glyph: (name: keyof typeof GLYPHS.unicode) => string;
|
|
55
|
+
readonly mode: GlyphMode;
|
|
56
|
+
readonly noColor: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Default semantic colors — hex values render without any Pi theme; theme names resolve through Pi's theme.
|
|
61
|
+
*/
|
|
62
|
+
const SEMANTIC_COLORS: Record<SemanticToken, string> = {
|
|
63
|
+
surface: "",
|
|
64
|
+
surfaceRaised: "",
|
|
65
|
+
surfaceMuted: "",
|
|
66
|
+
text: "text",
|
|
67
|
+
muted: "muted",
|
|
68
|
+
dim: "dim",
|
|
69
|
+
accent: "accent",
|
|
70
|
+
accentStrong: "accent",
|
|
71
|
+
border: "border",
|
|
72
|
+
borderMuted: "borderMuted",
|
|
73
|
+
borderActive: "borderAccent",
|
|
74
|
+
success: "success",
|
|
75
|
+
warning: "warning",
|
|
76
|
+
error: "error",
|
|
77
|
+
model: "#d787af",
|
|
78
|
+
thinking: "thinkingOff",
|
|
79
|
+
thinkingMinimal: "thinkingMinimal",
|
|
80
|
+
thinkingLow: "thinkingLow",
|
|
81
|
+
thinkingMedium: "thinkingMedium",
|
|
82
|
+
thinkingHigh: "thinkingHigh",
|
|
83
|
+
thinkingXhigh: "thinkingXhigh",
|
|
84
|
+
thinkingMax: "thinkingMax",
|
|
85
|
+
path: "#00afaf",
|
|
86
|
+
gitClean: "success",
|
|
87
|
+
gitDirty: "warning",
|
|
88
|
+
contextLow: "dim",
|
|
89
|
+
contextMedium: "warning",
|
|
90
|
+
contextHigh: "error",
|
|
91
|
+
contextCritical: "error",
|
|
92
|
+
tokens: "muted",
|
|
93
|
+
cache: "muted",
|
|
94
|
+
cost: "text",
|
|
95
|
+
time: "muted",
|
|
96
|
+
separator: "dim",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const GLYPHS = {
|
|
100
|
+
nerd: {
|
|
101
|
+
pi: "π",
|
|
102
|
+
git: "\uE0A0",
|
|
103
|
+
path: "\uF07B",
|
|
104
|
+
context: "\u{F035B}",
|
|
105
|
+
separator: "\uE0B0",
|
|
106
|
+
powerlineLeft: "\uE0B0",
|
|
107
|
+
powerlineRight: "\uE0B2",
|
|
108
|
+
powerlineThinLeft: "\uE0B1",
|
|
109
|
+
powerlineThinRight: "\uE0B3",
|
|
110
|
+
},
|
|
111
|
+
unicode: {
|
|
112
|
+
pi: "π",
|
|
113
|
+
git: "⎇",
|
|
114
|
+
path: "⌂",
|
|
115
|
+
context: "◫",
|
|
116
|
+
separator: "│",
|
|
117
|
+
powerlineLeft: "›",
|
|
118
|
+
powerlineRight: "‹",
|
|
119
|
+
powerlineThinLeft: "│",
|
|
120
|
+
powerlineThinRight: "│",
|
|
121
|
+
},
|
|
122
|
+
ascii: {
|
|
123
|
+
pi: "pi",
|
|
124
|
+
git: "git",
|
|
125
|
+
path: "path",
|
|
126
|
+
context: "ctx",
|
|
127
|
+
separator: "|",
|
|
128
|
+
powerlineLeft: ">",
|
|
129
|
+
powerlineRight: "<",
|
|
130
|
+
powerlineThinLeft: "|",
|
|
131
|
+
powerlineThinRight: "|",
|
|
132
|
+
},
|
|
133
|
+
} as const;
|
|
134
|
+
|
|
135
|
+
function isHex(color: string): color is `#${string}` {
|
|
136
|
+
return /^#[0-9a-fA-F]{6}$/.test(color);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function hexToAnsiPrefix(hex: string): string {
|
|
140
|
+
const value = hex.replace("#", "");
|
|
141
|
+
const r = Number.parseInt(value.slice(0, 2), 16);
|
|
142
|
+
const g = Number.parseInt(value.slice(2, 4), 16);
|
|
143
|
+
const b = Number.parseInt(value.slice(4, 6), 16);
|
|
144
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Rainbow gradient for high thinking levels. */
|
|
148
|
+
const RAINBOW_COLORS = [
|
|
149
|
+
"#b281d6",
|
|
150
|
+
"#d787af",
|
|
151
|
+
"#febc38",
|
|
152
|
+
"#e4c00f",
|
|
153
|
+
"#89d281",
|
|
154
|
+
"#00afaf",
|
|
155
|
+
"#178fb9",
|
|
156
|
+
"#b281d6",
|
|
157
|
+
] as const;
|
|
158
|
+
|
|
159
|
+
function rainbowAnsi(text: string): string {
|
|
160
|
+
let result = "";
|
|
161
|
+
let colorIndex = 0;
|
|
162
|
+
for (const char of text) {
|
|
163
|
+
if (char === " " || char === ":") {
|
|
164
|
+
result += char;
|
|
165
|
+
} else {
|
|
166
|
+
result += hexToAnsiPrefix(RAINBOW_COLORS[colorIndex % RAINBOW_COLORS.length] ?? "#b281d6") + char;
|
|
167
|
+
colorIndex++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return `${result}\x1b[0m`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function colorPrefixFor(
|
|
174
|
+
active: ActiveTheme | undefined,
|
|
175
|
+
config: NormalizedPiStyleConfig,
|
|
176
|
+
noColor: boolean,
|
|
177
|
+
token: SemanticToken,
|
|
178
|
+
): string {
|
|
179
|
+
if (noColor) return "";
|
|
180
|
+
const raw = config.theme.colors[token] ?? SEMANTIC_COLORS[token];
|
|
181
|
+
if (!raw) return "";
|
|
182
|
+
if (isHex(raw)) return hexToAnsiPrefix(raw);
|
|
183
|
+
if (active?.fg) {
|
|
184
|
+
try {
|
|
185
|
+
// Pi's fg returns `\x1b[38;5;Nm` + text + `\x1b[39m`; keep only the prefix.
|
|
186
|
+
const styled = active.fg(raw, "");
|
|
187
|
+
return styled.endsWith("\x1b[39m") ? styled.slice(0, -5) : styled;
|
|
188
|
+
} catch {
|
|
189
|
+
return "";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return "";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function detectGlyphMode(
|
|
196
|
+
config: NormalizedPiStyleConfig,
|
|
197
|
+
env: Record<string, string | undefined> = {},
|
|
198
|
+
): GlyphMode {
|
|
199
|
+
if (env.PI_STYLE_NERD_FONTS === "1") return "nerd";
|
|
200
|
+
if (env.PI_STYLE_NERD_FONTS === "0") return "unicode";
|
|
201
|
+
if (config.theme.nerdFonts === "on") return "nerd";
|
|
202
|
+
if (config.theme.nerdFonts === "off") return "unicode";
|
|
203
|
+
if (env.GHOSTTY_RESOURCES_DIR) return "nerd";
|
|
204
|
+
// Conservative heuristic: known terminals commonly ship with a Nerd Font. An explicit override stays authoritative.
|
|
205
|
+
const term = (env.TERM_PROGRAM ?? "").toLowerCase();
|
|
206
|
+
if (["iterm", "wezterm", "kitty", "ghostty", "alacritty", "warp"].some((name) => term.includes(name))) {
|
|
207
|
+
return "nerd";
|
|
208
|
+
}
|
|
209
|
+
return config.preset === "ascii" ? "ascii" : "unicode";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function resolveTheme(
|
|
213
|
+
active: ActiveTheme | undefined,
|
|
214
|
+
config: NormalizedPiStyleConfig,
|
|
215
|
+
env: Record<string, string | undefined> = {},
|
|
216
|
+
): ResolvedTheme {
|
|
217
|
+
const noColor = Object.hasOwn(env, "NO_COLOR") && env.NO_COLOR !== "" && config.theme.colors.colorOverride !== "on";
|
|
218
|
+
const mode = config.preset === "ascii" ? "ascii" : detectGlyphMode(config, env);
|
|
219
|
+
const color = (token: SemanticToken) => colorPrefixFor(active, config, noColor, token);
|
|
220
|
+
return {
|
|
221
|
+
mode,
|
|
222
|
+
noColor,
|
|
223
|
+
color,
|
|
224
|
+
apply: (token, text) => {
|
|
225
|
+
const prefix = color(token);
|
|
226
|
+
return prefix ? `${prefix}${text}\x1b[0m` : text;
|
|
227
|
+
},
|
|
228
|
+
rainbow: (text) => (noColor ? text : rainbowAnsi(text)),
|
|
229
|
+
glyph: (name) => config.theme.glyphs[name] ?? GLYPHS[mode][name],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
export { GLYPHS };
|
|
File without changes
|