pi-execution-time 0.2.0 → 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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # pi-execution-time
2
2
 
3
- [pi](https://github.com/badlogic/pi-mono) package that adds a live task execution timer to the footer.
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. It renders as a compact status item in the bottom bar next to pi's built-in cost and context usage indicators.
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
 
@@ -32,7 +32,19 @@ When the agent finishes, it keeps the final duration and completion time visible
32
32
  ✓ task 18s · 17:42
33
33
  ```
34
34
 
35
- Longer durations are formatted as `1m 05s` or `1h 02m 03s`. Completion time is shown as local `HH:mm`.
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,65 +1,183 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
2
3
 
3
- const STATUS_KEY = "execution-time";
4
- const UPDATE_INTERVAL_MS = 250;
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 timer: TimerState | undefined;
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 stopTimer() {
15
- if (!timer) return;
16
- clearInterval(timer.interval);
17
- timer = undefined;
36
+ function stopTotalTimer() {
37
+ if (!totalInterval) return;
38
+ clearInterval(totalInterval);
39
+ totalInterval = undefined;
18
40
  }
19
41
 
20
- function renderRunning(ctx: ExtensionContext) {
21
- if (!timer) return;
22
- const elapsedMs = Date.now() - timer.startedAt;
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(STATUS_KEY, icon + text);
47
+ ctx.ui.setStatus(TASK_STATUS_KEY, icon + text);
26
48
  }
27
49
 
28
- function renderDone(ctx: ExtensionContext, elapsedMs: number, completedAt: Date) {
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
53
  const duration = ctx.ui.theme.fg("muted", formatElapsed(elapsedMs));
32
54
  const separator = ctx.ui.theme.fg("dim", " · ");
33
55
  const completedTime = ctx.ui.theme.fg("muted", formatCompletedAt(completedAt));
34
- ctx.ui.setStatus(STATUS_KEY, icon + label + duration + separator + completedTime);
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 = [];
35
76
  }
36
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
+
37
118
  pi.on("agent_start", async (_event, ctx) => {
38
- stopTimer();
119
+ stopTaskTimer();
39
120
 
40
- timer = {
121
+ taskTimer = {
41
122
  startedAt: Date.now(),
42
- interval: setInterval(() => renderRunning(ctx), UPDATE_INTERVAL_MS),
123
+ interval: setInterval(() => renderTaskRunning(ctx), TASK_UPDATE_INTERVAL_MS),
43
124
  };
44
125
 
45
- renderRunning(ctx);
126
+ renderTaskRunning(ctx);
127
+ });
128
+
129
+ pi.on("message_start", async (event) => {
130
+ if (event.message.role !== "user" || standardInputsQueued === 0) return;
131
+
132
+ if (activeStep) {
133
+ completedSteps.push(completeStep(activeStep, new Date()));
134
+ }
135
+
136
+ standardInputsQueued--;
137
+ activeStep = { step: nextStep++, startedAt: Date.now() };
46
138
  });
47
139
 
48
140
  pi.on("agent_end", async (_event, ctx) => {
49
- if (!timer) return;
141
+ if (!taskTimer) return;
50
142
 
51
143
  const completedAt = new Date();
52
- const elapsedMs = completedAt.getTime() - timer.startedAt;
53
- stopTimer();
54
- renderDone(ctx, elapsedMs, completedAt);
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
+ }
55
153
  });
56
154
 
57
155
  pi.on("session_shutdown", async (_event, ctx) => {
58
- stopTimer();
59
- ctx.ui.setStatus(STATUS_KEY, undefined);
156
+ stopTaskTimer();
157
+ stopTotalTimer();
158
+ ctx.ui.setStatus(TASK_STATUS_KEY, undefined);
159
+ ctx.ui.setStatus(TOTAL_STATUS_KEY, undefined);
60
160
  });
61
161
  }
62
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
+
63
181
  function formatElapsed(ms: number) {
64
182
  const totalSeconds = Math.max(0, ms / 1000);
65
183
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-execution-time",
3
- "version": "0.2.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",
@@ -33,8 +33,9 @@
33
33
  "./package.json": "./package.json"
34
34
  },
35
35
  "devDependencies": {
36
- "@earendil-works/pi-coding-agent": "^0.79.10",
37
- "typescript": "^6.0.3"
36
+ "@earendil-works/pi-coding-agent": "^1.4.0",
37
+ "@earendil-works/pi-tui": "^1.4.0",
38
+ "typescript": "^7.0.2"
38
39
  },
39
40
  "pi": {
40
41
  "extensions": [
@@ -42,6 +43,7 @@
42
43
  ]
43
44
  },
44
45
  "peerDependencies": {
45
- "@earendil-works/pi-coding-agent": "*"
46
+ "@earendil-works/pi-coding-agent": "*",
47
+ "@earendil-works/pi-tui": "*"
46
48
  }
47
49
  }