@hk_net/pi-timestamp 0.1.8 → 0.1.9

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.
Files changed (3) hide show
  1. package/README.md +41 -41
  2. package/package.json +37 -37
  3. package/timestamp.ts +237 -237
package/README.md CHANGED
@@ -1,41 +1,41 @@
1
- # pi-timestamp
2
-
3
- Shows timestamps for user input and agent completion timing. Requires pi `>=0.81.1`.
4
-
5
- ## What it does
6
-
7
- - **User input**: Shows `Sent HH:MM:SS` as a dim status line in the chat display after each user message
8
- - **Agent completion**: Shows `Done at HH:MM:SS · duration` as a dim status line in the chat display after each agent turn (e.g., `Done at 14:32:05 · 3.2s`)
9
- - **Session/runtime summaries**: Shows accent-colored summaries when switching sessions and at final Pi exit, including start/end times and durations. The final summary includes the complete Pi process runtime and every session interval.
10
-
11
- All timestamps and summaries are **display-only** — session-switch summaries render in Pi's UI and the final runtime summary prints after Pi restores the terminal in TUI mode. They never enter the LLM context.
12
-
13
- ## Display behavior
14
-
15
- Timestamps appear inline in the **chat display**, similar to Pi's built-in tool timing lines such as `Took 1.9s`. They are not appended to the session as user or custom messages, so they do not pollute the model context.
16
-
17
- ## Installation
18
-
19
- ```bash
20
- pi install npm:@hk_net/pi-timestamp
21
- ```
22
-
23
- Or copy manually:
24
-
25
- ```bash
26
- mkdir -p ~/.pi/agent/extensions
27
- cp packages/pi-timestamp/timestamp.ts ~/.pi/agent/extensions/timestamp.ts
28
- ```
29
-
30
- ## Duration measurement
31
-
32
- Captured from `agent_start` (when the model starts processing) to `agent_end` (when all tool calls and model processing are done).
33
-
34
- ## Issues and feedback
35
-
36
- Found a bug or have a feature request? Please report it on
37
- [GitHub Issues](https://github.com/hknet/pi-extensions/issues).
38
-
39
- For security vulnerabilities, please use
40
- [GitHub's private vulnerability reporting](https://github.com/hknet/pi-extensions/security/advisories/new)
41
- instead of opening a public issue.
1
+ # pi-timestamp
2
+
3
+ Shows timestamps for user input and agent completion timing. Requires pi `>=0.84.1`.
4
+
5
+ ## What it does
6
+
7
+ - **User input**: Shows `Sent HH:MM:SS` as a dim status line in the chat display after each user message
8
+ - **Agent completion**: Shows `Done at HH:MM:SS · duration` as a dim status line in the chat display after each agent turn (e.g., `Done at 14:32:05 · 3.2s`)
9
+ - **Session/runtime summaries**: Shows accent-colored summaries when switching sessions and at final Pi exit, including start/end times and durations. The final summary includes the complete Pi process runtime and every session interval.
10
+
11
+ All timestamps and summaries are **display-only** — session-switch summaries render in Pi's UI and the final runtime summary prints after Pi restores the terminal in TUI mode. They never enter the LLM context.
12
+
13
+ ## Display behavior
14
+
15
+ Timestamps appear inline in the **chat display**, similar to Pi's built-in tool timing lines such as `Took 1.9s`. They are not appended to the session as user or custom messages, so they do not pollute the model context.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pi install npm:@hk_net/pi-timestamp
21
+ ```
22
+
23
+ Or copy manually:
24
+
25
+ ```bash
26
+ mkdir -p ~/.pi/agent/extensions
27
+ cp packages/pi-timestamp/timestamp.ts ~/.pi/agent/extensions/timestamp.ts
28
+ ```
29
+
30
+ ## Duration measurement
31
+
32
+ Captured from `agent_start` (when the model starts processing) to `agent_end` (when all tool calls and model processing are done).
33
+
34
+ ## Issues and feedback
35
+
36
+ Found a bug or have a feature request? Please report it on
37
+ [GitHub Issues](https://github.com/hknet/pi-extensions/issues).
38
+
39
+ For security vulnerabilities, please use
40
+ [GitHub's private vulnerability reporting](https://github.com/hknet/pi-extensions/security/advisories/new)
41
+ instead of opening a public issue.
package/package.json CHANGED
@@ -1,37 +1,37 @@
1
- {
2
- "name": "@hk_net/pi-timestamp",
3
- "version": "0.1.8",
4
- "description": "Pi extension that shows timestamps for user input and agent completion timing",
5
- "type": "module",
6
- "private": false,
7
- "keywords": [
8
- "pi-package",
9
- "pi",
10
- "extension",
11
- "timestamp"
12
- ],
13
- "license": "EUPL-1.2",
14
- "author": "hknet",
15
- "repository": {
16
- "type": "git",
17
- "url": "git+https://github.com/hknet/pi-extensions.git",
18
- "directory": "packages/pi-timestamp"
19
- },
20
- "bugs": {
21
- "url": "https://github.com/hknet/pi-extensions/issues"
22
- },
23
- "homepage": "https://github.com/hknet/pi-extensions/tree/main/packages/pi-timestamp#readme",
24
- "files": [
25
- "timestamp.ts",
26
- "README.md"
27
- ],
28
- "peerDependencies": {
29
- "@earendil-works/pi-coding-agent": ">=0.81.1",
30
- "@earendil-works/pi-tui": ">=0.81.1"
31
- },
32
- "pi": {
33
- "extensions": [
34
- "./timestamp.ts"
35
- ]
36
- }
37
- }
1
+ {
2
+ "name": "@hk_net/pi-timestamp",
3
+ "version": "0.1.9",
4
+ "description": "Pi extension that shows timestamps for user input and agent completion timing",
5
+ "type": "module",
6
+ "private": false,
7
+ "keywords": [
8
+ "pi-package",
9
+ "pi",
10
+ "extension",
11
+ "timestamp"
12
+ ],
13
+ "license": "EUPL-1.2",
14
+ "author": "hknet",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/hknet/pi-extensions.git",
18
+ "directory": "packages/pi-timestamp"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/hknet/pi-extensions/issues"
22
+ },
23
+ "homepage": "https://github.com/hknet/pi-extensions/tree/main/packages/pi-timestamp#readme",
24
+ "files": [
25
+ "timestamp.ts",
26
+ "README.md"
27
+ ],
28
+ "peerDependencies": {
29
+ "@earendil-works/pi-coding-agent": ">=0.84.1",
30
+ "@earendil-works/pi-tui": ">=0.84.1"
31
+ },
32
+ "pi": {
33
+ "extensions": [
34
+ "./timestamp.ts"
35
+ ]
36
+ }
37
+ }
package/timestamp.ts CHANGED
@@ -1,237 +1,237 @@
1
- /**
2
- * Timestamp Extension
3
- *
4
- * Displays timestamps for user input and agent completion timing.
5
- * All timestamps are display-only — they never enter the LLM context.
6
- *
7
- * - Shows `Sent HH:MM:SS` after each user message in the chat UI
8
- * - Shows `Done at HH:MM:SS · duration` after each agent turn in the chat UI
9
- * - Summarizes completed sessions and the complete Pi process runtime
10
- */
11
-
12
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
13
-
14
- export interface SessionInterval {
15
- id: string;
16
- startedAt: number;
17
- endedAt: number;
18
- }
19
-
20
- export interface TimestampRuntimeState {
21
- processStartedAt: number;
22
- activeSession: { id: string; startedAt: number } | undefined;
23
- completedSessions: SessionInterval[];
24
- pendingSessionSummary: SessionInterval | undefined;
25
- }
26
-
27
- const RUNTIME_STATE_KEY = Symbol.for("hknet.pi-timestamp.runtime-state");
28
-
29
- export function createRuntimeState(processStartedAt: number): TimestampRuntimeState {
30
- return { processStartedAt, activeSession: undefined, completedSessions: [], pendingSessionSummary: undefined };
31
- }
32
-
33
- function getRuntimeState(): TimestampRuntimeState {
34
- const globalStore = globalThis as Record<symbol, unknown>;
35
- const existing = globalStore[RUNTIME_STATE_KEY];
36
- if (existing && typeof existing === "object") return existing as TimestampRuntimeState;
37
-
38
- // The extension can load after Pi has initialized. Anchor the summary to the
39
- // Node process start rather than the extension load time.
40
- const state = createRuntimeState(Date.now() - process.uptime() * 1000);
41
- globalStore[RUNTIME_STATE_KEY] = state;
42
- return state;
43
- }
44
-
45
- export function beginSession(state: TimestampRuntimeState, id: string, startedAt: number): boolean {
46
- if (state.activeSession?.id === id) return false;
47
- state.activeSession = { id, startedAt };
48
- return true;
49
- }
50
-
51
- export function finishSession(state: TimestampRuntimeState, endedAt: number): SessionInterval | undefined {
52
- const active = state.activeSession;
53
- if (!active) return undefined;
54
-
55
- const interval = { ...active, endedAt };
56
- state.completedSessions.push(interval);
57
- state.activeSession = undefined;
58
- return interval;
59
- }
60
-
61
- export function takePendingSessionSummary(state: TimestampRuntimeState): SessionInterval | undefined {
62
- const pending = state.pendingSessionSummary;
63
- state.pendingSessionSummary = undefined;
64
- return pending;
65
- }
66
-
67
- function formatTime(ts: number): string {
68
- const d = new Date(ts);
69
- const hh = String(d.getHours()).padStart(2, "0");
70
- const mm = String(d.getMinutes()).padStart(2, "0");
71
- const ss = String(d.getSeconds()).padStart(2, "0");
72
- return `${hh}:${mm}:${ss}`;
73
- }
74
-
75
- function formatDateTime(ts: number): string {
76
- const d = new Date(ts);
77
- const yyyy = d.getFullYear();
78
- const mm = String(d.getMonth() + 1).padStart(2, "0");
79
- const dd = String(d.getDate()).padStart(2, "0");
80
- return `${yyyy}-${mm}-${dd} ${formatTime(ts)}`;
81
- }
82
-
83
- function isSameLocalDate(left: number, right: number): boolean {
84
- const a = new Date(left);
85
- const b = new Date(right);
86
- return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
87
- }
88
-
89
- function formatSessionRange(session: SessionInterval): string {
90
- if (isSameLocalDate(session.startedAt, session.endedAt)) {
91
- return `${formatTime(session.startedAt)}–${formatTime(session.endedAt)}`;
92
- }
93
- return `${formatDateTime(session.startedAt)} → ${formatDateTime(session.endedAt)}`;
94
- }
95
-
96
- function formatDuration(ms: number): string {
97
- if (ms < 1000) return `${ms}ms`;
98
- const totalSecs = ms / 1000;
99
- if (totalSecs < 60) return `${totalSecs.toFixed(1)}s`;
100
- const totalMins = totalSecs / 60;
101
- if (totalMins < 60) {
102
- const m = Math.floor(totalMins);
103
- const s = (totalSecs % 60).toFixed(1);
104
- return `${m}m ${s}s`;
105
- }
106
- const hrs = totalMins / 60;
107
- return `${Math.floor(hrs)}h ${Math.floor(totalMins % 60)}m`;
108
- }
109
-
110
- export function formatRuntimeDuration(ms: number): string {
111
- let remainingSeconds = Math.max(0, Math.floor(ms / 1000));
112
- const weeks = Math.floor(remainingSeconds / (7 * 24 * 60 * 60));
113
- remainingSeconds %= 7 * 24 * 60 * 60;
114
- const days = Math.floor(remainingSeconds / (24 * 60 * 60));
115
- remainingSeconds %= 24 * 60 * 60;
116
- const hours = Math.floor(remainingSeconds / (60 * 60));
117
- remainingSeconds %= 60 * 60;
118
- const minutes = Math.floor(remainingSeconds / 60);
119
- const seconds = remainingSeconds % 60;
120
-
121
- const parts = [
122
- weeks > 0 ? `${weeks}w` : undefined,
123
- days > 0 ? `${days}d` : undefined,
124
- hours > 0 ? `${hours}h` : undefined,
125
- minutes > 0 ? `${minutes}m` : undefined,
126
- `${seconds}s`,
127
- ];
128
- return parts.filter((part): part is string => part !== undefined).join(" ");
129
- }
130
-
131
- function isTimeoutErrorMessage(message: string | undefined): boolean {
132
- return /timed? out|timeout/i.test(message ?? "");
133
- }
134
-
135
- export default function (pi: ExtensionAPI) {
136
- let taskStartTime: number | undefined;
137
- let waitingForRetryAfterTimeout = false;
138
-
139
- function notifyAccent(ctx: ExtensionContext, message: string): void {
140
- ctx.ui.notify(ctx.ui.theme.fg("accent", message), "info");
141
- }
142
-
143
- // Track when the agent starts processing. If Pi is auto-retrying after a timeout,
144
- // keep the original start time so the eventual completion covers the full task.
145
- pi.on("agent_start", async () => {
146
- if (waitingForRetryAfterTimeout) return;
147
- taskStartTime = Date.now();
148
- });
149
-
150
- // Show "Sent HH:MM:SS" after each user message.
151
- // notify(..., "info") renders a display-only status line in the TUI chat; it is not
152
- // appended to the session and does not enter the LLM context.
153
- pi.on("message_end", async (event, ctx) => {
154
- if (event.message.role !== "user") return;
155
-
156
- if (waitingForRetryAfterTimeout) {
157
- taskStartTime = undefined;
158
- waitingForRetryAfterTimeout = false;
159
- }
160
-
161
- const ts = event.message.timestamp;
162
- if (!ts) return;
163
-
164
- ctx.ui.notify(`Sent ${formatTime(ts)}`, "info");
165
- });
166
-
167
- // Show completion timing after the whole task finishes. If Pi ended this agent loop
168
- // with a timeout error and will likely auto-retry, wait for the later completion.
169
- pi.on("agent_end", async (event, ctx) => {
170
- const lastAssistantMessage = [...event.messages].reverse().find((message) => message.role === "assistant");
171
- if (
172
- lastAssistantMessage?.role === "assistant" &&
173
- lastAssistantMessage.stopReason === "error" &&
174
- isTimeoutErrorMessage(lastAssistantMessage.errorMessage)
175
- ) {
176
- waitingForRetryAfterTimeout = true;
177
- return;
178
- }
179
-
180
- const startTime = taskStartTime;
181
- taskStartTime = undefined;
182
- waitingForRetryAfterTimeout = false;
183
-
184
- if (startTime === undefined) return;
185
-
186
- const endTime = Date.now();
187
- const duration = endTime - startTime;
188
-
189
- ctx.ui.notify(`Done at ${formatTime(endTime)} · ${formatDuration(duration)}`, "info");
190
- });
191
-
192
- pi.on("session_start", (_event, ctx) => {
193
- const state = getRuntimeState();
194
- const previousSession = takePendingSessionSummary(state);
195
- if (previousSession) {
196
- notifyAccent(
197
- ctx,
198
- `Previous session complete · ${formatSessionRange(previousSession)} · ${formatRuntimeDuration(previousSession.endedAt - previousSession.startedAt)}`,
199
- );
200
- }
201
-
202
- const sessionId = ctx.sessionManager.getSessionId();
203
- // A reload rebinds the extension to the same session. Keep its timer running.
204
- beginSession(state, sessionId, Date.now());
205
- });
206
-
207
- pi.on("session_shutdown", (event, ctx) => {
208
- // Reload replaces the extension runtime but not the Pi session.
209
- if (event.reason === "reload") return;
210
-
211
- const endTime = Date.now();
212
- const interval = finishSession(getRuntimeState(), endTime);
213
-
214
- if (event.reason !== "quit") {
215
- if (interval) getRuntimeState().pendingSessionSummary = interval;
216
- return;
217
- }
218
-
219
- const state = getRuntimeState();
220
- const sessionRows = state.completedSessions.map(
221
- (session, index) =>
222
- ` ${index + 1}. ${formatSessionRange(session)} · ${formatRuntimeDuration(session.endedAt - session.startedAt)}`,
223
- );
224
- // On normal Ctrl+D shutdown Pi has already stopped the TUI, so ui.notify()
225
- // cannot render. Write after terminal restoration instead.
226
- const summary = [
227
- "Pi runtime complete",
228
- ` Started: ${formatDateTime(state.processStartedAt)}`,
229
- ` Ended: ${formatDateTime(endTime)}`,
230
- ` Total: ${formatRuntimeDuration(endTime - state.processStartedAt)}`,
231
- ...(sessionRows.length > 0 ? [" Sessions:", ...sessionRows] : []),
232
- ].join("\n");
233
- if (ctx.mode === "tui") {
234
- process.stdout.write(`${ctx.ui.theme.fg("accent", summary)}\n`);
235
- }
236
- });
237
- }
1
+ /**
2
+ * Timestamp Extension
3
+ *
4
+ * Displays timestamps for user input and agent completion timing.
5
+ * All timestamps are display-only — they never enter the LLM context.
6
+ *
7
+ * - Shows `Sent HH:MM:SS` after each user message in the chat UI
8
+ * - Shows `Done at HH:MM:SS · duration` after each agent turn in the chat UI
9
+ * - Summarizes completed sessions and the complete Pi process runtime
10
+ */
11
+
12
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
13
+
14
+ export interface SessionInterval {
15
+ id: string;
16
+ startedAt: number;
17
+ endedAt: number;
18
+ }
19
+
20
+ export interface TimestampRuntimeState {
21
+ processStartedAt: number;
22
+ activeSession: { id: string; startedAt: number } | undefined;
23
+ completedSessions: SessionInterval[];
24
+ pendingSessionSummary: SessionInterval | undefined;
25
+ }
26
+
27
+ const RUNTIME_STATE_KEY = Symbol.for("hknet.pi-timestamp.runtime-state");
28
+
29
+ export function createRuntimeState(processStartedAt: number): TimestampRuntimeState {
30
+ return { processStartedAt, activeSession: undefined, completedSessions: [], pendingSessionSummary: undefined };
31
+ }
32
+
33
+ function getRuntimeState(): TimestampRuntimeState {
34
+ const globalStore = globalThis as Record<symbol, unknown>;
35
+ const existing = globalStore[RUNTIME_STATE_KEY];
36
+ if (existing && typeof existing === "object") return existing as TimestampRuntimeState;
37
+
38
+ // The extension can load after Pi has initialized. Anchor the summary to the
39
+ // Node process start rather than the extension load time.
40
+ const state = createRuntimeState(Date.now() - process.uptime() * 1000);
41
+ globalStore[RUNTIME_STATE_KEY] = state;
42
+ return state;
43
+ }
44
+
45
+ export function beginSession(state: TimestampRuntimeState, id: string, startedAt: number): boolean {
46
+ if (state.activeSession?.id === id) return false;
47
+ state.activeSession = { id, startedAt };
48
+ return true;
49
+ }
50
+
51
+ export function finishSession(state: TimestampRuntimeState, endedAt: number): SessionInterval | undefined {
52
+ const active = state.activeSession;
53
+ if (!active) return undefined;
54
+
55
+ const interval = { ...active, endedAt };
56
+ state.completedSessions.push(interval);
57
+ state.activeSession = undefined;
58
+ return interval;
59
+ }
60
+
61
+ export function takePendingSessionSummary(state: TimestampRuntimeState): SessionInterval | undefined {
62
+ const pending = state.pendingSessionSummary;
63
+ state.pendingSessionSummary = undefined;
64
+ return pending;
65
+ }
66
+
67
+ function formatTime(ts: number): string {
68
+ const d = new Date(ts);
69
+ const hh = String(d.getHours()).padStart(2, "0");
70
+ const mm = String(d.getMinutes()).padStart(2, "0");
71
+ const ss = String(d.getSeconds()).padStart(2, "0");
72
+ return `${hh}:${mm}:${ss}`;
73
+ }
74
+
75
+ function formatDateTime(ts: number): string {
76
+ const d = new Date(ts);
77
+ const yyyy = d.getFullYear();
78
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
79
+ const dd = String(d.getDate()).padStart(2, "0");
80
+ return `${yyyy}-${mm}-${dd} ${formatTime(ts)}`;
81
+ }
82
+
83
+ function isSameLocalDate(left: number, right: number): boolean {
84
+ const a = new Date(left);
85
+ const b = new Date(right);
86
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
87
+ }
88
+
89
+ function formatSessionRange(session: SessionInterval): string {
90
+ if (isSameLocalDate(session.startedAt, session.endedAt)) {
91
+ return `${formatTime(session.startedAt)}–${formatTime(session.endedAt)}`;
92
+ }
93
+ return `${formatDateTime(session.startedAt)} → ${formatDateTime(session.endedAt)}`;
94
+ }
95
+
96
+ function formatDuration(ms: number): string {
97
+ if (ms < 1000) return `${ms}ms`;
98
+ const totalSecs = ms / 1000;
99
+ if (totalSecs < 60) return `${totalSecs.toFixed(1)}s`;
100
+ const totalMins = totalSecs / 60;
101
+ if (totalMins < 60) {
102
+ const m = Math.floor(totalMins);
103
+ const s = (totalSecs % 60).toFixed(1);
104
+ return `${m}m ${s}s`;
105
+ }
106
+ const hrs = totalMins / 60;
107
+ return `${Math.floor(hrs)}h ${Math.floor(totalMins % 60)}m`;
108
+ }
109
+
110
+ export function formatRuntimeDuration(ms: number): string {
111
+ let remainingSeconds = Math.max(0, Math.floor(ms / 1000));
112
+ const weeks = Math.floor(remainingSeconds / (7 * 24 * 60 * 60));
113
+ remainingSeconds %= 7 * 24 * 60 * 60;
114
+ const days = Math.floor(remainingSeconds / (24 * 60 * 60));
115
+ remainingSeconds %= 24 * 60 * 60;
116
+ const hours = Math.floor(remainingSeconds / (60 * 60));
117
+ remainingSeconds %= 60 * 60;
118
+ const minutes = Math.floor(remainingSeconds / 60);
119
+ const seconds = remainingSeconds % 60;
120
+
121
+ const parts = [
122
+ weeks > 0 ? `${weeks}w` : undefined,
123
+ days > 0 ? `${days}d` : undefined,
124
+ hours > 0 ? `${hours}h` : undefined,
125
+ minutes > 0 ? `${minutes}m` : undefined,
126
+ `${seconds}s`,
127
+ ];
128
+ return parts.filter((part): part is string => part !== undefined).join(" ");
129
+ }
130
+
131
+ function isTimeoutErrorMessage(message: string | undefined): boolean {
132
+ return /timed? out|timeout/i.test(message ?? "");
133
+ }
134
+
135
+ export default function (pi: ExtensionAPI) {
136
+ let taskStartTime: number | undefined;
137
+ let waitingForRetryAfterTimeout = false;
138
+
139
+ function notifyAccent(ctx: ExtensionContext, message: string): void {
140
+ ctx.ui.notify(ctx.ui.theme.fg("accent", message), "info");
141
+ }
142
+
143
+ // Track when the agent starts processing. If Pi is auto-retrying after a timeout,
144
+ // keep the original start time so the eventual completion covers the full task.
145
+ pi.on("agent_start", async () => {
146
+ if (waitingForRetryAfterTimeout) return;
147
+ taskStartTime = Date.now();
148
+ });
149
+
150
+ // Show "Sent HH:MM:SS" after each user message.
151
+ // notify(..., "info") renders a display-only status line in the TUI chat; it is not
152
+ // appended to the session and does not enter the LLM context.
153
+ pi.on("message_end", async (event, ctx) => {
154
+ if (event.message.role !== "user") return;
155
+
156
+ if (waitingForRetryAfterTimeout) {
157
+ taskStartTime = undefined;
158
+ waitingForRetryAfterTimeout = false;
159
+ }
160
+
161
+ const ts = event.message.timestamp;
162
+ if (!ts) return;
163
+
164
+ ctx.ui.notify(`Sent ${formatTime(ts)}`, "info");
165
+ });
166
+
167
+ // Show completion timing after the whole task finishes. If Pi ended this agent loop
168
+ // with a timeout error and will likely auto-retry, wait for the later completion.
169
+ pi.on("agent_end", async (event, ctx) => {
170
+ const lastAssistantMessage = [...event.messages].reverse().find((message) => message.role === "assistant");
171
+ if (
172
+ lastAssistantMessage?.role === "assistant" &&
173
+ lastAssistantMessage.stopReason === "error" &&
174
+ isTimeoutErrorMessage(lastAssistantMessage.errorMessage)
175
+ ) {
176
+ waitingForRetryAfterTimeout = true;
177
+ return;
178
+ }
179
+
180
+ const startTime = taskStartTime;
181
+ taskStartTime = undefined;
182
+ waitingForRetryAfterTimeout = false;
183
+
184
+ if (startTime === undefined) return;
185
+
186
+ const endTime = Date.now();
187
+ const duration = endTime - startTime;
188
+
189
+ ctx.ui.notify(`Done at ${formatTime(endTime)} · ${formatDuration(duration)}`, "info");
190
+ });
191
+
192
+ pi.on("session_start", (_event, ctx) => {
193
+ const state = getRuntimeState();
194
+ const previousSession = takePendingSessionSummary(state);
195
+ if (previousSession) {
196
+ notifyAccent(
197
+ ctx,
198
+ `Previous session complete · ${formatSessionRange(previousSession)} · ${formatRuntimeDuration(previousSession.endedAt - previousSession.startedAt)}`,
199
+ );
200
+ }
201
+
202
+ const sessionId = ctx.sessionManager.getSessionId();
203
+ // A reload rebinds the extension to the same session. Keep its timer running.
204
+ beginSession(state, sessionId, Date.now());
205
+ });
206
+
207
+ pi.on("session_shutdown", (event, ctx) => {
208
+ // Reload replaces the extension runtime but not the Pi session.
209
+ if (event.reason === "reload") return;
210
+
211
+ const endTime = Date.now();
212
+ const interval = finishSession(getRuntimeState(), endTime);
213
+
214
+ if (event.reason !== "quit") {
215
+ if (interval) getRuntimeState().pendingSessionSummary = interval;
216
+ return;
217
+ }
218
+
219
+ const state = getRuntimeState();
220
+ const sessionRows = state.completedSessions.map(
221
+ (session, index) =>
222
+ ` ${index + 1}. ${formatSessionRange(session)} · ${formatRuntimeDuration(session.endedAt - session.startedAt)}`,
223
+ );
224
+ // On normal Ctrl+D shutdown Pi has already stopped the TUI, so ui.notify()
225
+ // cannot render. Write after terminal restoration instead.
226
+ const summary = [
227
+ "Pi runtime complete",
228
+ ` Started: ${formatDateTime(state.processStartedAt)}`,
229
+ ` Ended: ${formatDateTime(endTime)}`,
230
+ ` Total: ${formatRuntimeDuration(endTime - state.processStartedAt)}`,
231
+ ...(sessionRows.length > 0 ? [" Sessions:", ...sessionRows] : []),
232
+ ].join("\n");
233
+ if (ctx.mode === "tui") {
234
+ process.stdout.write(`${ctx.ui.theme.fg("accent", summary)}\n`);
235
+ }
236
+ });
237
+ }