pi-zentui 0.3.2 → 0.3.3
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/LICENSE +1 -1
- package/extensions/zentui/config.ts +12 -1
- package/extensions/zentui/footer.ts +41 -11
- package/extensions/zentui/format.ts +10 -0
- package/extensions/zentui/git.ts +7 -6
- package/extensions/zentui/index.ts +26 -0
- package/extensions/zentui/settings-command.ts +7 -0
- package/extensions/zentui/state.ts +2 -0
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -22,10 +22,12 @@ export type FooterSegmentsConfig = {
|
|
|
22
22
|
cwd: boolean;
|
|
23
23
|
gitBranch: boolean;
|
|
24
24
|
gitStatus: boolean;
|
|
25
|
+
gitCounts: boolean;
|
|
25
26
|
runtime: boolean;
|
|
26
27
|
context: boolean;
|
|
27
28
|
tokens: boolean;
|
|
28
29
|
cost: boolean;
|
|
30
|
+
sessionDuration: boolean;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export type ExtensionStatusPlacement = "off" | "left" | "middle" | "right";
|
|
@@ -74,6 +76,7 @@ export type PolishedTuiConfig = {
|
|
|
74
76
|
separator: ColorSpec;
|
|
75
77
|
runtimePrefix: ColorSpec;
|
|
76
78
|
extensionStatus: ColorSpec;
|
|
79
|
+
sessionDuration: ColorSpec;
|
|
77
80
|
editorAccent?: ColorSpec;
|
|
78
81
|
editorPrompt?: ColorSpec;
|
|
79
82
|
editorBorder?: ColorSpec;
|
|
@@ -126,6 +129,7 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
126
129
|
separator: "bright-black",
|
|
127
130
|
runtimePrefix: "",
|
|
128
131
|
extensionStatus: "bright-black",
|
|
132
|
+
sessionDuration: "yellow",
|
|
129
133
|
},
|
|
130
134
|
colorSources: {
|
|
131
135
|
starship: "theme",
|
|
@@ -141,10 +145,12 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
141
145
|
cwd: true,
|
|
142
146
|
gitBranch: true,
|
|
143
147
|
gitStatus: true,
|
|
148
|
+
gitCounts: false,
|
|
144
149
|
runtime: true,
|
|
145
150
|
context: true,
|
|
146
151
|
tokens: true,
|
|
147
152
|
cost: true,
|
|
153
|
+
sessionDuration: false,
|
|
148
154
|
},
|
|
149
155
|
extensionStatuses: {
|
|
150
156
|
defaultPlacement: "right",
|
|
@@ -256,6 +262,7 @@ function normalizeColors(record: Record<string, unknown>): Partial<PolishedTuiCo
|
|
|
256
262
|
separator: colorValue(record, "separator"),
|
|
257
263
|
runtimePrefix: colorValue(record, "runtimePrefix"),
|
|
258
264
|
extensionStatus: colorValue(record, "extensionStatus"),
|
|
265
|
+
sessionDuration: colorValue(record, "sessionDuration"),
|
|
259
266
|
editorAccent: colorValue(record, "editorAccent"),
|
|
260
267
|
editorPrompt: colorValue(record, "editorPrompt"),
|
|
261
268
|
editorBorder: colorValue(record, "editorBorder"),
|
|
@@ -291,10 +298,12 @@ function normalizeFooterSegments(record: Record<string, unknown>): FooterSegment
|
|
|
291
298
|
cwd: footerSegmentValue(record, "cwd"),
|
|
292
299
|
gitBranch: footerSegmentValue(record, "gitBranch"),
|
|
293
300
|
gitStatus: footerSegmentValue(record, "gitStatus"),
|
|
301
|
+
gitCounts: footerSegmentValue(record, "gitCounts"),
|
|
294
302
|
runtime: footerSegmentValue(record, "runtime"),
|
|
295
303
|
context: footerSegmentValue(record, "context"),
|
|
296
304
|
tokens: footerSegmentValue(record, "tokens"),
|
|
297
305
|
cost: footerSegmentValue(record, "cost"),
|
|
306
|
+
sessionDuration: footerSegmentValue(record, "sessionDuration"),
|
|
298
307
|
};
|
|
299
308
|
}
|
|
300
309
|
|
|
@@ -347,10 +356,12 @@ function isFooterSegmentKey(value: string): value is keyof FooterSegmentsConfig
|
|
|
347
356
|
value === "cwd" ||
|
|
348
357
|
value === "gitBranch" ||
|
|
349
358
|
value === "gitStatus" ||
|
|
359
|
+
value === "gitCounts" ||
|
|
350
360
|
value === "runtime" ||
|
|
351
361
|
value === "context" ||
|
|
352
362
|
value === "tokens" ||
|
|
353
|
-
value === "cost"
|
|
363
|
+
value === "cost" ||
|
|
364
|
+
value === "sessionDuration"
|
|
354
365
|
);
|
|
355
366
|
}
|
|
356
367
|
|
|
@@ -2,7 +2,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { PolishedTuiConfig } from "./config";
|
|
4
4
|
import { collectExtensionStatusSegments, type ExtensionStatusSegment } from "./extension-status";
|
|
5
|
-
import { formatCwdLabel, formatRuntimeSegment } from "./format";
|
|
5
|
+
import { buildSessionDurationLabel, formatCwdLabel, formatRuntimeSegment } from "./format";
|
|
6
6
|
import type { FooterState } from "./state";
|
|
7
7
|
import { renderStyleForSource } from "./style";
|
|
8
8
|
|
|
@@ -176,9 +176,16 @@ export function installFooter(
|
|
|
176
176
|
const gitStatusColor = (text: string) =>
|
|
177
177
|
renderStyleForSource(theme, colorSource, config.colors.gitStatus, text);
|
|
178
178
|
const gitIcon = config.icons.git ? gitColor(config.icons.git) : "";
|
|
179
|
+
const gitCounts = config.footerSegments.gitCounts;
|
|
180
|
+
const stashLabel =
|
|
181
|
+
state.stashed > 0
|
|
182
|
+
? gitCounts
|
|
183
|
+
? `${config.icons.stashed}${state.stashed}`
|
|
184
|
+
: config.icons.stashed
|
|
185
|
+
: "";
|
|
179
186
|
const allStatus = [
|
|
180
187
|
state.conflicted > 0 ? config.icons.conflicted : "",
|
|
181
|
-
|
|
188
|
+
stashLabel,
|
|
182
189
|
state.deleted > 0 ? config.icons.deleted : "",
|
|
183
190
|
state.renamed > 0 ? config.icons.renamed : "",
|
|
184
191
|
state.modified > 0 ? config.icons.modified : "",
|
|
@@ -186,14 +193,18 @@ export function installFooter(
|
|
|
186
193
|
state.staged > 0 ? config.icons.staged : "",
|
|
187
194
|
state.untracked > 0 ? config.icons.untracked : "",
|
|
188
195
|
].join("");
|
|
189
|
-
const aheadBehind =
|
|
190
|
-
state.ahead > 0 && state.behind > 0
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
const aheadBehind = (() => {
|
|
197
|
+
if (state.ahead > 0 && state.behind > 0) {
|
|
198
|
+
return gitCounts
|
|
199
|
+
? `${config.icons.ahead}${state.ahead}${config.icons.behind}${state.behind}`
|
|
200
|
+
: config.icons.diverged;
|
|
201
|
+
}
|
|
202
|
+
if (state.ahead > 0)
|
|
203
|
+
return gitCounts ? `${config.icons.ahead}${state.ahead}` : config.icons.ahead;
|
|
204
|
+
if (state.behind > 0)
|
|
205
|
+
return gitCounts ? `${config.icons.behind}${state.behind}` : config.icons.behind;
|
|
206
|
+
return "";
|
|
207
|
+
})();
|
|
197
208
|
const statusBlock =
|
|
198
209
|
allStatus || aheadBehind ? gitStatusColor(`[${allStatus}${aheadBehind}]`) : "";
|
|
199
210
|
const branchParts =
|
|
@@ -206,9 +217,27 @@ export function installFooter(
|
|
|
206
217
|
? formatRuntimeSegment(theme, state.runtime, config.colors.runtimePrefix, colorSource)
|
|
207
218
|
: "";
|
|
208
219
|
|
|
209
|
-
const
|
|
220
|
+
const sessionDurationSegment = (() => {
|
|
221
|
+
if (!config.footerSegments.sessionDuration || !state.sessionStartEpoch) return "";
|
|
222
|
+
const timeLabel = buildSessionDurationLabel(state.sessionStartEpoch);
|
|
223
|
+
const prefix = renderStyleForSource(theme, colorSource, "", "up for");
|
|
224
|
+
const time = renderStyleForSource(
|
|
225
|
+
theme,
|
|
226
|
+
colorSource,
|
|
227
|
+
config.colors.sessionDuration,
|
|
228
|
+
timeLabel,
|
|
229
|
+
);
|
|
230
|
+
return `${prefix} ${time}`;
|
|
231
|
+
})();
|
|
232
|
+
const left = [
|
|
233
|
+
config.footerSegments.cwd ? cwdLabel : "",
|
|
234
|
+
branchLabel,
|
|
235
|
+
runtimeLabel,
|
|
236
|
+
sessionDurationSegment,
|
|
237
|
+
]
|
|
210
238
|
.filter(Boolean)
|
|
211
239
|
.join(" ");
|
|
240
|
+
|
|
212
241
|
const right = [
|
|
213
242
|
config.footerSegments.context
|
|
214
243
|
? renderStyleForSource(theme, colorSource, contextColor, state.contextLabel)
|
|
@@ -222,6 +251,7 @@ export function installFooter(
|
|
|
222
251
|
]
|
|
223
252
|
.filter(Boolean)
|
|
224
253
|
.join(separator);
|
|
254
|
+
|
|
225
255
|
const extensionStatuses = collectExtensionStatusSegments(
|
|
226
256
|
footerData.getExtensionStatuses(),
|
|
227
257
|
config,
|
|
@@ -91,6 +91,16 @@ export function buildCostLabel(totals: UsageTotals): string {
|
|
|
91
91
|
return `$${totals.cost.toFixed(3)}`;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
export function buildSessionDurationLabel(startEpoch: number): string {
|
|
95
|
+
const totalSeconds = Math.max(0, Math.floor((Date.now() - startEpoch) / 1000));
|
|
96
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
97
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
98
|
+
const seconds = totalSeconds % 60;
|
|
99
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
100
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
101
|
+
return `${seconds}s`;
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
export function buildContextLabel(ctx: ExtensionContext): string {
|
|
95
105
|
const usage = ctx.getContextUsage();
|
|
96
106
|
const contextWindow = ctx.model?.contextWindow ?? usage?.contextWindow;
|
package/extensions/zentui/git.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type GitStatusSummary = {
|
|
|
11
11
|
behind: number;
|
|
12
12
|
conflicted: number;
|
|
13
13
|
untracked: number;
|
|
14
|
-
stashed:
|
|
14
|
+
stashed: number;
|
|
15
15
|
modified: number;
|
|
16
16
|
staged: number;
|
|
17
17
|
renamed: number;
|
|
@@ -27,7 +27,7 @@ export function emptyGitStatus(): GitStatusSummary {
|
|
|
27
27
|
behind: 0,
|
|
28
28
|
conflicted: 0,
|
|
29
29
|
untracked: 0,
|
|
30
|
-
stashed:
|
|
30
|
+
stashed: 0,
|
|
31
31
|
modified: 0,
|
|
32
32
|
staged: 0,
|
|
33
33
|
renamed: 0,
|
|
@@ -36,9 +36,9 @@ export function emptyGitStatus(): GitStatusSummary {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function parseGitStatusPorcelain(stdoutText: string,
|
|
39
|
+
export function parseGitStatusPorcelain(stdoutText: string, stashCount: number): GitStatusSummary {
|
|
40
40
|
const status = emptyGitStatus();
|
|
41
|
-
status.stashed =
|
|
41
|
+
status.stashed = stashCount;
|
|
42
42
|
|
|
43
43
|
for (const line of stdoutText.split(/\r?\n/)) {
|
|
44
44
|
if (!line) continue;
|
|
@@ -93,7 +93,7 @@ export async function readGitStatus(cwd: string): Promise<GitStatusSummary> {
|
|
|
93
93
|
cwd,
|
|
94
94
|
timeout: GIT_COMMAND_TIMEOUT_MS,
|
|
95
95
|
}),
|
|
96
|
-
execFileAsync("git", ["
|
|
96
|
+
execFileAsync("git", ["stash", "list"], {
|
|
97
97
|
cwd,
|
|
98
98
|
timeout: GIT_COMMAND_TIMEOUT_MS,
|
|
99
99
|
}).catch(() => ({ stdout: "" })),
|
|
@@ -101,7 +101,8 @@ export async function readGitStatus(cwd: string): Promise<GitStatusSummary> {
|
|
|
101
101
|
const stdoutText = typeof statusStdout === "string" ? statusStdout : String(statusStdout);
|
|
102
102
|
const stashStdout =
|
|
103
103
|
typeof stashResult.stdout === "string" ? stashResult.stdout : String(stashResult.stdout);
|
|
104
|
-
|
|
104
|
+
const stashCount = stashStdout.split(/\r?\n/).filter((line) => line.trim().length > 0).length;
|
|
105
|
+
return parseGitStatusPorcelain(stdoutText, stashCount);
|
|
105
106
|
} catch {
|
|
106
107
|
return emptyGitStatus();
|
|
107
108
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type UiFeaturesConfig,
|
|
22
22
|
} from "./config";
|
|
23
23
|
import { installFooter } from "./footer";
|
|
24
|
+
import { buildSessionDurationLabel } from "./format";
|
|
24
25
|
import { emptyGitStatus, readGitStatus } from "./git";
|
|
25
26
|
import {
|
|
26
27
|
createProjectRefreshScheduler,
|
|
@@ -83,6 +84,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
83
84
|
let installedEditorFactory: EditorFactory | undefined;
|
|
84
85
|
let wrappedEditorFactory: EditorFactory | undefined;
|
|
85
86
|
let prototypePatchesInstalled = false;
|
|
87
|
+
let stopSessionTimer: () => void = () => {};
|
|
88
|
+
let lastDurationLabel = "";
|
|
86
89
|
|
|
87
90
|
const refresh = () => requestFooterRender?.();
|
|
88
91
|
const getActiveTheme = () => activeTheme;
|
|
@@ -117,6 +120,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
117
120
|
projectRefreshScheduler.stop();
|
|
118
121
|
};
|
|
119
122
|
|
|
123
|
+
const startSessionTimer = () => {
|
|
124
|
+
stopSessionTimer();
|
|
125
|
+
lastDurationLabel = "";
|
|
126
|
+
const timer = setInterval(() => {
|
|
127
|
+
if (!(currentConfig.features.statusLine && currentConfig.footerSegments.sessionDuration))
|
|
128
|
+
return;
|
|
129
|
+
const label = state.sessionStartEpoch
|
|
130
|
+
? buildSessionDurationLabel(state.sessionStartEpoch)
|
|
131
|
+
: "";
|
|
132
|
+
if (label === lastDurationLabel) return;
|
|
133
|
+
lastDurationLabel = label;
|
|
134
|
+
refresh();
|
|
135
|
+
}, 1000);
|
|
136
|
+
stopSessionTimer = () => {
|
|
137
|
+
clearInterval(timer);
|
|
138
|
+
stopSessionTimer = () => {};
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
120
142
|
const installPrototypePatches = () => {
|
|
121
143
|
if (prototypePatchesInstalled) return;
|
|
122
144
|
const cleanupSelectorBorderStyle = installSelectorBorderStyle(getActiveTheme, getCurrentConfig);
|
|
@@ -239,9 +261,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
239
261
|
);
|
|
240
262
|
scheduleProjectRefresh(ctx, { force: true });
|
|
241
263
|
refresh();
|
|
264
|
+
startSessionTimer();
|
|
242
265
|
};
|
|
243
266
|
|
|
244
267
|
const uninstallStatusLine = (ctx: ExtensionContext) => {
|
|
268
|
+
stopSessionTimer();
|
|
245
269
|
stopProjectRefresh();
|
|
246
270
|
ctx.ui.setFooter(undefined);
|
|
247
271
|
footerInstalled = false;
|
|
@@ -297,6 +321,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
297
321
|
|
|
298
322
|
const cleanupUi = (ctx?: ExtensionContext) => {
|
|
299
323
|
uninstallPrototypePatches();
|
|
324
|
+
stopSessionTimer();
|
|
300
325
|
stopProjectRefresh();
|
|
301
326
|
requestFooterRender = undefined;
|
|
302
327
|
getActiveExtensionStatuses = () => new Map();
|
|
@@ -328,6 +353,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
328
353
|
};
|
|
329
354
|
|
|
330
355
|
pi.on("session_start", async (_event, ctx) => {
|
|
356
|
+
state.sessionStartEpoch = Date.now();
|
|
331
357
|
installUi(ctx);
|
|
332
358
|
scheduleEditorReconciliation(ctx);
|
|
333
359
|
});
|
|
@@ -88,6 +88,8 @@ const footerSegmentSettingLabels: Record<FooterSegmentSettingId, string> = {
|
|
|
88
88
|
cwd: "Current directory",
|
|
89
89
|
gitBranch: "Git branch",
|
|
90
90
|
gitStatus: "Git status",
|
|
91
|
+
gitCounts: "Git counts",
|
|
92
|
+
sessionDuration: "Session duration",
|
|
91
93
|
runtime: "Runtime",
|
|
92
94
|
context: "Context usage",
|
|
93
95
|
tokens: "Token counts",
|
|
@@ -98,6 +100,9 @@ const footerSegmentSettingDescriptions: Record<FooterSegmentSettingId, string> =
|
|
|
98
100
|
cwd: "Show or hide the current working directory segment on the left.",
|
|
99
101
|
gitBranch: "Show or hide the git branch name on the left.",
|
|
100
102
|
gitStatus: "Show or hide git status icons and ahead/behind markers.",
|
|
103
|
+
gitCounts:
|
|
104
|
+
"Show numeric ahead/behind and stash counts (requires the Git status segment to be enabled).",
|
|
105
|
+
sessionDuration: "Show session running time on the left, after the runtime.",
|
|
101
106
|
runtime: "Show or hide the detected runtime/language segment on the left.",
|
|
102
107
|
context: "Show or hide context usage on the right.",
|
|
103
108
|
tokens: "Show or hide input/output token counts on the right.",
|
|
@@ -144,6 +149,8 @@ function isFooterSegmentSettingId(value: string): value is FooterSegmentSettingI
|
|
|
144
149
|
value === "cwd" ||
|
|
145
150
|
value === "gitBranch" ||
|
|
146
151
|
value === "gitStatus" ||
|
|
152
|
+
value === "gitCounts" ||
|
|
153
|
+
value === "sessionDuration" ||
|
|
147
154
|
value === "runtime" ||
|
|
148
155
|
value === "context" ||
|
|
149
156
|
value === "tokens" ||
|
|
@@ -16,6 +16,7 @@ export type FooterState = GitStatusSummary & {
|
|
|
16
16
|
tokenLabel: string;
|
|
17
17
|
costLabel: string;
|
|
18
18
|
runtime?: RuntimeInfo;
|
|
19
|
+
sessionStartEpoch?: number;
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
export function createInitialState(gitDefaults: GitStatusSummary): FooterState {
|
|
@@ -26,6 +27,7 @@ export function createInitialState(gitDefaults: GitStatusSummary): FooterState {
|
|
|
26
27
|
tokenLabel: "↑0 ↓0",
|
|
27
28
|
costLabel: "$0.000",
|
|
28
29
|
runtime: undefined,
|
|
30
|
+
sessionStartEpoch: Date.now(),
|
|
29
31
|
...gitDefaults,
|
|
30
32
|
};
|
|
31
33
|
}
|