pi-execution-time 0.1.4 → 0.3.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 +17 -5
- package/extensions/execution-time.ts +151 -26
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# pi-execution-time
|
|
2
2
|
|
|
3
|
-
[pi](https://github.com/badlogic/pi-mono) package that adds
|
|
3
|
+
[pi](https://github.com/badlogic/pi-mono) package that adds live task and session timers.
|
|
4
4
|
|
|
5
|
-
The timer starts when a prompt begins running and counts up until the agent finishes.
|
|
5
|
+
The task timer starts when a prompt begins running and counts up until the agent finishes. A separate session timer keeps counting from session start. Both render as compact status items in the bottom bar next to pi's built-in cost and context usage indicators.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -26,13 +26,25 @@ Reload or restart pi after installation. When the agent is working, the footer s
|
|
|
26
26
|
⏱ 4.2s
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
When the agent finishes, it keeps the final duration visible:
|
|
29
|
+
When the agent finishes, it keeps the final duration and completion time visible:
|
|
30
30
|
|
|
31
31
|
```text
|
|
32
|
-
✓ task 18s
|
|
32
|
+
✓ task 18s · 17:42
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
The bottom bar also shows the total elapsed session time and keeps updating it every second:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
Σ session 12m 04s
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Completed standard user prompts are rendered above the next standard user prompt:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
✓ step 1 18s · 17:42
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Steering messages are ignored for step history. Longer durations are formatted as `1m 05s` or `1h 02m 03s`. Completion time is shown as local `HH:mm`.
|
|
36
48
|
|
|
37
49
|
## Local development
|
|
38
50
|
|
|
@@ -1,62 +1,183 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
4
|
+
const TASK_STATUS_KEY = "execution-time";
|
|
5
|
+
const TOTAL_STATUS_KEY = "zz-execution-time-total";
|
|
6
|
+
const STEP_MESSAGE_TYPE = "execution-time-step";
|
|
7
|
+
const TASK_UPDATE_INTERVAL_MS = 250;
|
|
8
|
+
const TOTAL_UPDATE_INTERVAL_MS = 1000;
|
|
5
9
|
|
|
6
10
|
type TimerState = {
|
|
7
11
|
startedAt: number;
|
|
8
12
|
interval: ReturnType<typeof setInterval>;
|
|
9
13
|
};
|
|
10
14
|
|
|
15
|
+
type CompletedStep = {
|
|
16
|
+
step: number;
|
|
17
|
+
elapsedMs: number;
|
|
18
|
+
completedAt: Date;
|
|
19
|
+
};
|
|
20
|
+
|
|
11
21
|
export default function (pi: ExtensionAPI) {
|
|
12
|
-
let
|
|
22
|
+
let sessionStartedAt = Date.now();
|
|
23
|
+
let totalInterval: ReturnType<typeof setInterval> | undefined;
|
|
24
|
+
let taskTimer: TimerState | undefined;
|
|
25
|
+
let activeStep: { step: number; startedAt: number } | undefined;
|
|
26
|
+
let nextStep = 1;
|
|
27
|
+
let standardInputsQueued = 0;
|
|
28
|
+
let completedSteps: CompletedStep[] = [];
|
|
29
|
+
|
|
30
|
+
function stopTaskTimer() {
|
|
31
|
+
if (!taskTimer) return;
|
|
32
|
+
clearInterval(taskTimer.interval);
|
|
33
|
+
taskTimer = undefined;
|
|
34
|
+
}
|
|
13
35
|
|
|
14
|
-
function
|
|
15
|
-
if (!
|
|
16
|
-
clearInterval(
|
|
17
|
-
|
|
36
|
+
function stopTotalTimer() {
|
|
37
|
+
if (!totalInterval) return;
|
|
38
|
+
clearInterval(totalInterval);
|
|
39
|
+
totalInterval = undefined;
|
|
18
40
|
}
|
|
19
41
|
|
|
20
|
-
function
|
|
21
|
-
if (!
|
|
22
|
-
const elapsedMs = Date.now() -
|
|
42
|
+
function renderTaskRunning(ctx: ExtensionContext) {
|
|
43
|
+
if (!taskTimer) return;
|
|
44
|
+
const elapsedMs = Date.now() - taskTimer.startedAt;
|
|
23
45
|
const icon = ctx.ui.theme.fg("accent", "⏱");
|
|
24
46
|
const text = ctx.ui.theme.fg("dim", ` ${formatElapsed(elapsedMs)}`);
|
|
25
|
-
ctx.ui.setStatus(
|
|
47
|
+
ctx.ui.setStatus(TASK_STATUS_KEY, icon + text);
|
|
26
48
|
}
|
|
27
49
|
|
|
28
|
-
function
|
|
50
|
+
function renderTaskDone(ctx: ExtensionContext, elapsedMs: number, completedAt: Date) {
|
|
29
51
|
const icon = ctx.ui.theme.fg("success", "✓");
|
|
30
52
|
const label = ctx.ui.theme.fg("dim", " task ");
|
|
31
|
-
const
|
|
32
|
-
ctx.ui.
|
|
53
|
+
const duration = ctx.ui.theme.fg("muted", formatElapsed(elapsedMs));
|
|
54
|
+
const separator = ctx.ui.theme.fg("dim", " · ");
|
|
55
|
+
const completedTime = ctx.ui.theme.fg("muted", formatCompletedAt(completedAt));
|
|
56
|
+
ctx.ui.setStatus(TASK_STATUS_KEY, icon + label + duration + separator + completedTime);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderTotal(ctx: ExtensionContext) {
|
|
60
|
+
const icon = ctx.ui.theme.fg("accent", "Σ");
|
|
61
|
+
const label = ctx.ui.theme.fg("dim", " session ");
|
|
62
|
+
const elapsed = ctx.ui.theme.fg("muted", formatElapsed(Date.now() - sessionStartedAt));
|
|
63
|
+
ctx.ui.setStatus(TOTAL_STATUS_KEY, icon + label + elapsed);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderCompletedSteps() {
|
|
67
|
+
for (const { step, elapsedMs, completedAt } of completedSteps) {
|
|
68
|
+
pi.sendMessage({
|
|
69
|
+
customType: STEP_MESSAGE_TYPE,
|
|
70
|
+
content: "",
|
|
71
|
+
display: true,
|
|
72
|
+
details: { step, elapsedMs, completedAt: completedAt.toISOString() },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
completedSteps = [];
|
|
33
76
|
}
|
|
34
77
|
|
|
78
|
+
pi.registerMessageRenderer<{ step: number; elapsedMs: number; completedAt: string }>(
|
|
79
|
+
STEP_MESSAGE_TYPE,
|
|
80
|
+
(message, _options, theme) => {
|
|
81
|
+
const details = message.details;
|
|
82
|
+
const step = details?.step ?? "?";
|
|
83
|
+
const elapsed = typeof details?.elapsedMs === "number" ? formatElapsed(details.elapsedMs) : "?";
|
|
84
|
+
const completedAt = details?.completedAt ? formatCompletedAt(new Date(details.completedAt)) : "?";
|
|
85
|
+
const text = `${theme.fg("success", "✓")} ${theme.fg("dim", `step ${step}`)} ${theme.fg("muted", elapsed)} ${theme.fg("dim", "·")} ${theme.fg("muted", completedAt)}`;
|
|
86
|
+
return new Text(text, 0, 0);
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
91
|
+
sessionStartedAt = Date.now();
|
|
92
|
+
activeStep = undefined;
|
|
93
|
+
standardInputsQueued = 0;
|
|
94
|
+
completedSteps = [];
|
|
95
|
+
nextStep = getNextStep(ctx);
|
|
96
|
+
stopTotalTimer();
|
|
97
|
+
totalInterval = setInterval(() => renderTotal(ctx), TOTAL_UPDATE_INTERVAL_MS);
|
|
98
|
+
renderTotal(ctx);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
pi.on("context", async (event) => ({
|
|
102
|
+
messages: event.messages.filter(
|
|
103
|
+
(message) => message.role !== "custom" || message.customType !== STEP_MESSAGE_TYPE,
|
|
104
|
+
),
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
pi.on("before_agent_start", async () => {
|
|
108
|
+
renderCompletedSteps();
|
|
109
|
+
standardInputsQueued++;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
pi.on("input", async (event) => {
|
|
113
|
+
if (event.source === "extension" || event.streamingBehavior !== "followUp") return;
|
|
114
|
+
|
|
115
|
+
standardInputsQueued++;
|
|
116
|
+
});
|
|
117
|
+
|
|
35
118
|
pi.on("agent_start", async (_event, ctx) => {
|
|
36
|
-
|
|
119
|
+
stopTaskTimer();
|
|
37
120
|
|
|
38
|
-
|
|
121
|
+
taskTimer = {
|
|
39
122
|
startedAt: Date.now(),
|
|
40
|
-
interval: setInterval(() =>
|
|
123
|
+
interval: setInterval(() => renderTaskRunning(ctx), TASK_UPDATE_INTERVAL_MS),
|
|
41
124
|
};
|
|
42
125
|
|
|
43
|
-
|
|
126
|
+
renderTaskRunning(ctx);
|
|
44
127
|
});
|
|
45
128
|
|
|
46
|
-
pi.on("
|
|
47
|
-
if (
|
|
129
|
+
pi.on("message_start", async (event) => {
|
|
130
|
+
if (event.message.role !== "user" || standardInputsQueued === 0) return;
|
|
48
131
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
132
|
+
if (activeStep) {
|
|
133
|
+
completedSteps.push(completeStep(activeStep, new Date()));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
standardInputsQueued--;
|
|
137
|
+
activeStep = { step: nextStep++, startedAt: Date.now() };
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
141
|
+
if (!taskTimer) return;
|
|
142
|
+
|
|
143
|
+
const completedAt = new Date();
|
|
144
|
+
const elapsedMs = completedAt.getTime() - taskTimer.startedAt;
|
|
145
|
+
const completedStep = activeStep;
|
|
146
|
+
stopTaskTimer();
|
|
147
|
+
activeStep = undefined;
|
|
148
|
+
renderTaskDone(ctx, elapsedMs, completedAt);
|
|
149
|
+
|
|
150
|
+
if (completedStep) {
|
|
151
|
+
completedSteps.push(completeStep(completedStep, completedAt));
|
|
152
|
+
}
|
|
52
153
|
});
|
|
53
154
|
|
|
54
155
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
55
|
-
|
|
56
|
-
|
|
156
|
+
stopTaskTimer();
|
|
157
|
+
stopTotalTimer();
|
|
158
|
+
ctx.ui.setStatus(TASK_STATUS_KEY, undefined);
|
|
159
|
+
ctx.ui.setStatus(TOTAL_STATUS_KEY, undefined);
|
|
57
160
|
});
|
|
58
161
|
}
|
|
59
162
|
|
|
163
|
+
function getNextStep(ctx: ExtensionContext) {
|
|
164
|
+
let maxStep = 0;
|
|
165
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
166
|
+
if (entry.type !== "custom_message" || entry.customType !== STEP_MESSAGE_TYPE) continue;
|
|
167
|
+
const step = (entry.details as { step?: unknown } | undefined)?.step;
|
|
168
|
+
if (typeof step === "number") maxStep = Math.max(maxStep, step);
|
|
169
|
+
}
|
|
170
|
+
return maxStep + 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function completeStep(step: { step: number; startedAt: number }, completedAt: Date): CompletedStep {
|
|
174
|
+
return {
|
|
175
|
+
step: step.step,
|
|
176
|
+
elapsedMs: completedAt.getTime() - step.startedAt,
|
|
177
|
+
completedAt,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
60
181
|
function formatElapsed(ms: number) {
|
|
61
182
|
const totalSeconds = Math.max(0, ms / 1000);
|
|
62
183
|
|
|
@@ -81,6 +202,10 @@ function formatElapsed(ms: number) {
|
|
|
81
202
|
return `${roundedSeconds}s`;
|
|
82
203
|
}
|
|
83
204
|
|
|
205
|
+
function formatCompletedAt(date: Date) {
|
|
206
|
+
return `${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
84
209
|
function pad2(value: number) {
|
|
85
210
|
return value.toString().padStart(2, "0");
|
|
86
211
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-execution-time",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Pi extension that shows prompt execution time in the footer while the agent works.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,16 +32,18 @@
|
|
|
32
32
|
"exports": {
|
|
33
33
|
"./package.json": "./package.json"
|
|
34
34
|
},
|
|
35
|
-
"peerDependencies": {
|
|
36
|
-
"@mariozechner/pi-coding-agent": "*"
|
|
37
|
-
},
|
|
38
35
|
"devDependencies": {
|
|
39
|
-
"@
|
|
40
|
-
"
|
|
36
|
+
"@earendil-works/pi-coding-agent": "^1.4.0",
|
|
37
|
+
"@earendil-works/pi-tui": "^1.4.0",
|
|
38
|
+
"typescript": "^7.0.2"
|
|
41
39
|
},
|
|
42
40
|
"pi": {
|
|
43
41
|
"extensions": [
|
|
44
42
|
"./extensions/execution-time.ts"
|
|
45
43
|
]
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
47
|
+
"@earendil-works/pi-tui": "*"
|
|
46
48
|
}
|
|
47
49
|
}
|