@hk_net/pi-timestamp 0.1.10 → 0.1.12

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.12 - 2026-08-27
4
+
5
+ ### Changed
6
+
7
+ - Verify development, type compatibility, and tests against Pi SDK 0.84.3 while retaining runtime compatibility with Pi 0.84.1 and newer.
8
+
9
+ ## 0.1.11 - 2026-08-23
10
+
11
+ ### Changed
12
+
13
+ - Add the copyright holder and EUPL licensing notice to the package license and canonical TypeScript source.
14
+ - Identify KAPPER NETWORK-COMMUNICATIONS GmbH and kapper.net in the npm author metadata.
15
+
3
16
  ## 0.1.10 - 2026-08-23
4
17
 
5
18
  ### Changed
package/LICENSE CHANGED
@@ -1,3 +1,6 @@
1
+ Copyright © 2026 kapper.net - KAPPER NETWORK-COMMUNICATIONS GmbH
2
+ Licensed under the EUPL
3
+
1
4
  EUROPEAN UNION PUBLIC LICENCE v. 1.2
2
5
  EUPL © the European Union 2007, 2016
3
6
 
package/README.md CHANGED
@@ -1,43 +1,43 @@
1
- # pi-timestamp
2
-
3
- Shows timestamps for user input and agent completion timing. Requires Pi 0.84.1 or newer (tested with the current Pi 0.84.2 release).
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
- > **Avoid duplicate installation.** Install this npm package or the GitHub bundle, not both. Loading both copies can duplicate timestamps and runtime summaries.
20
-
21
- ```bash
22
- pi install npm:@hk_net/pi-timestamp
23
- ```
24
-
25
- Or copy manually:
26
-
27
- ```bash
28
- mkdir -p ~/.pi/agent/extensions
29
- cp packages/pi-timestamp/timestamp.ts ~/.pi/agent/extensions/timestamp.ts
30
- ```
31
-
32
- ## Duration measurement
33
-
34
- Captured from the first `agent_start` to `agent_settled`, so automatic retries, compaction recovery, tool calls, and queued continuations are included in one complete task duration.
35
-
36
- ## Issues and feedback
37
-
38
- Found a bug or have a feature request? Please report it on
39
- [GitHub Issues](https://github.com/hknet/pi-extensions/issues).
40
-
41
- For security vulnerabilities, please use
42
- [GitHub's private vulnerability reporting](https://github.com/hknet/pi-extensions/security/advisories/new)
43
- 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 or newer (tested with the current Pi 0.84.3 release).
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
+ > **Avoid duplicate installation.** Install this npm package or the GitHub bundle, not both. Loading both copies can duplicate timestamps and runtime summaries.
20
+
21
+ ```bash
22
+ pi install npm:@hk_net/pi-timestamp
23
+ ```
24
+
25
+ Or copy manually:
26
+
27
+ ```bash
28
+ mkdir -p ~/.pi/agent/extensions
29
+ cp packages/pi-timestamp/timestamp.ts ~/.pi/agent/extensions/timestamp.ts
30
+ ```
31
+
32
+ ## Duration measurement
33
+
34
+ Captured from the first `agent_start` to `agent_settled`, so automatic retries, compaction recovery, tool calls, and queued continuations are included in one complete task duration.
35
+
36
+ ## Issues and feedback
37
+
38
+ Found a bug or have a feature request? Please report it on
39
+ [GitHub Issues](https://github.com/hknet/pi-extensions/issues).
40
+
41
+ For security vulnerabilities, please use
42
+ [GitHub's private vulnerability reporting](https://github.com/hknet/pi-extensions/security/advisories/new)
43
+ instead of opening a public issue.
package/package.json CHANGED
@@ -1,49 +1,52 @@
1
- {
2
- "name": "@hk_net/pi-timestamp",
3
- "version": "0.1.10",
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
- "CHANGELOG.md"
28
- ],
29
- "engines": {
30
- "node": ">=22.19.0"
31
- },
32
- "peerDependencies": {
33
- "@earendil-works/pi-coding-agent": "*",
34
- "@earendil-works/pi-tui": "*"
35
- },
36
- "peerDependenciesMeta": {
37
- "@earendil-works/pi-coding-agent": {
38
- "optional": true
39
- },
40
- "@earendil-works/pi-tui": {
41
- "optional": true
42
- }
43
- },
44
- "pi": {
45
- "extensions": [
46
- "./timestamp.ts"
47
- ]
48
- }
49
- }
1
+ {
2
+ "name": "@hk_net/pi-timestamp",
3
+ "version": "0.1.12",
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": {
15
+ "name": "KAPPER NETWORK-COMMUNICATIONS GmbH",
16
+ "url": "https://kapper.net"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/hknet/pi-extensions.git",
21
+ "directory": "packages/pi-timestamp"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/hknet/pi-extensions/issues"
25
+ },
26
+ "homepage": "https://github.com/hknet/pi-extensions/tree/main/packages/pi-timestamp#readme",
27
+ "files": [
28
+ "timestamp.ts",
29
+ "README.md",
30
+ "CHANGELOG.md"
31
+ ],
32
+ "engines": {
33
+ "node": ">=22.19.0"
34
+ },
35
+ "peerDependencies": {
36
+ "@earendil-works/pi-coding-agent": "*",
37
+ "@earendil-works/pi-tui": "*"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "@earendil-works/pi-coding-agent": {
41
+ "optional": true
42
+ },
43
+ "@earendil-works/pi-tui": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "pi": {
48
+ "extensions": [
49
+ "./timestamp.ts"
50
+ ]
51
+ }
52
+ }
package/timestamp.ts CHANGED
@@ -1,215 +1,218 @@
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
- export default function (pi: ExtensionAPI) {
132
- let taskStartTime: number | undefined;
133
-
134
- function notifyAccent(ctx: ExtensionContext, message: string): void {
135
- ctx.ui.notify(ctx.ui.theme.fg("accent", message), "info");
136
- }
137
-
138
- // Track the complete run. Automatic retries and compaction recovery can emit
139
- // additional agent_start events before agent_settled, so retain the first start.
140
- pi.on("agent_start", async () => {
141
- taskStartTime ??= Date.now();
142
- });
143
-
144
- // Show "Sent HH:MM:SS" after each user message.
145
- // notify(..., "info") renders a display-only status line in the TUI chat; it is not
146
- // appended to the session and does not enter the LLM context.
147
- pi.on("message_end", async (event, ctx) => {
148
- if (event.message.role !== "user") return;
149
-
150
- const ts = event.message.timestamp;
151
- if (!ts) return;
152
-
153
- ctx.ui.notify(`Sent ${formatTime(ts)}`, "info");
154
- });
155
-
156
- // Show completion timing only after retries, compaction recovery, and queued
157
- // continuations have fully settled.
158
- pi.on("agent_settled", async (_event, ctx) => {
159
- const startTime = taskStartTime;
160
- taskStartTime = undefined;
161
-
162
- if (startTime === undefined) return;
163
-
164
- const endTime = Date.now();
165
- const duration = endTime - startTime;
166
-
167
- ctx.ui.notify(`Done at ${formatTime(endTime)} · ${formatDuration(duration)}`, "info");
168
- });
169
-
170
- pi.on("session_start", (_event, ctx) => {
171
- const state = getRuntimeState();
172
- const previousSession = takePendingSessionSummary(state);
173
- if (previousSession) {
174
- notifyAccent(
175
- ctx,
176
- `Previous session complete · ${formatSessionRange(previousSession)} · ${formatRuntimeDuration(previousSession.endedAt - previousSession.startedAt)}`,
177
- );
178
- }
179
-
180
- const sessionId = ctx.sessionManager.getSessionId();
181
- // A reload rebinds the extension to the same session. Keep its timer running.
182
- beginSession(state, sessionId, Date.now());
183
- });
184
-
185
- pi.on("session_shutdown", (event, ctx) => {
186
- // Reload replaces the extension runtime but not the Pi session.
187
- if (event.reason === "reload") return;
188
-
189
- const endTime = Date.now();
190
- const interval = finishSession(getRuntimeState(), endTime);
191
-
192
- if (event.reason !== "quit") {
193
- if (interval) getRuntimeState().pendingSessionSummary = interval;
194
- return;
195
- }
196
-
197
- const state = getRuntimeState();
198
- const sessionRows = state.completedSessions.map(
199
- (session, index) =>
200
- ` ${index + 1}. ${formatSessionRange(session)} · ${formatRuntimeDuration(session.endedAt - session.startedAt)}`,
201
- );
202
- // On normal Ctrl+D shutdown Pi has already stopped the TUI, so ui.notify()
203
- // cannot render. Write after terminal restoration instead.
204
- const summary = [
205
- "Pi runtime complete",
206
- ` Started: ${formatDateTime(state.processStartedAt)}`,
207
- ` Ended: ${formatDateTime(endTime)}`,
208
- ` Total: ${formatRuntimeDuration(endTime - state.processStartedAt)}`,
209
- ...(sessionRows.length > 0 ? [" Sessions:", ...sessionRows] : []),
210
- ].join("\n");
211
- if (ctx.mode === "tui") {
212
- process.stdout.write(`${ctx.ui.theme.fg("accent", summary)}\n`);
213
- }
214
- });
215
- }
1
+ // Copyright © 2026 kapper.net - KAPPER NETWORK-COMMUNICATIONS GmbH
2
+ // SPDX-License-Identifier: EUPL-1.2
3
+
4
+ /**
5
+ * Timestamp Extension
6
+ *
7
+ * Displays timestamps for user input and agent completion timing.
8
+ * All timestamps are display-only they never enter the LLM context.
9
+ *
10
+ * - Shows `Sent HH:MM:SS` after each user message in the chat UI
11
+ * - Shows `Done at HH:MM:SS · duration` after each agent turn in the chat UI
12
+ * - Summarizes completed sessions and the complete Pi process runtime
13
+ */
14
+
15
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
+
17
+ export interface SessionInterval {
18
+ id: string;
19
+ startedAt: number;
20
+ endedAt: number;
21
+ }
22
+
23
+ export interface TimestampRuntimeState {
24
+ processStartedAt: number;
25
+ activeSession: { id: string; startedAt: number } | undefined;
26
+ completedSessions: SessionInterval[];
27
+ pendingSessionSummary: SessionInterval | undefined;
28
+ }
29
+
30
+ const RUNTIME_STATE_KEY = Symbol.for("hknet.pi-timestamp.runtime-state");
31
+
32
+ export function createRuntimeState(processStartedAt: number): TimestampRuntimeState {
33
+ return { processStartedAt, activeSession: undefined, completedSessions: [], pendingSessionSummary: undefined };
34
+ }
35
+
36
+ function getRuntimeState(): TimestampRuntimeState {
37
+ const globalStore = globalThis as Record<symbol, unknown>;
38
+ const existing = globalStore[RUNTIME_STATE_KEY];
39
+ if (existing && typeof existing === "object") return existing as TimestampRuntimeState;
40
+
41
+ // The extension can load after Pi has initialized. Anchor the summary to the
42
+ // Node process start rather than the extension load time.
43
+ const state = createRuntimeState(Date.now() - process.uptime() * 1000);
44
+ globalStore[RUNTIME_STATE_KEY] = state;
45
+ return state;
46
+ }
47
+
48
+ export function beginSession(state: TimestampRuntimeState, id: string, startedAt: number): boolean {
49
+ if (state.activeSession?.id === id) return false;
50
+ state.activeSession = { id, startedAt };
51
+ return true;
52
+ }
53
+
54
+ export function finishSession(state: TimestampRuntimeState, endedAt: number): SessionInterval | undefined {
55
+ const active = state.activeSession;
56
+ if (!active) return undefined;
57
+
58
+ const interval = { ...active, endedAt };
59
+ state.completedSessions.push(interval);
60
+ state.activeSession = undefined;
61
+ return interval;
62
+ }
63
+
64
+ export function takePendingSessionSummary(state: TimestampRuntimeState): SessionInterval | undefined {
65
+ const pending = state.pendingSessionSummary;
66
+ state.pendingSessionSummary = undefined;
67
+ return pending;
68
+ }
69
+
70
+ function formatTime(ts: number): string {
71
+ const d = new Date(ts);
72
+ const hh = String(d.getHours()).padStart(2, "0");
73
+ const mm = String(d.getMinutes()).padStart(2, "0");
74
+ const ss = String(d.getSeconds()).padStart(2, "0");
75
+ return `${hh}:${mm}:${ss}`;
76
+ }
77
+
78
+ function formatDateTime(ts: number): string {
79
+ const d = new Date(ts);
80
+ const yyyy = d.getFullYear();
81
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
82
+ const dd = String(d.getDate()).padStart(2, "0");
83
+ return `${yyyy}-${mm}-${dd} ${formatTime(ts)}`;
84
+ }
85
+
86
+ function isSameLocalDate(left: number, right: number): boolean {
87
+ const a = new Date(left);
88
+ const b = new Date(right);
89
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
90
+ }
91
+
92
+ function formatSessionRange(session: SessionInterval): string {
93
+ if (isSameLocalDate(session.startedAt, session.endedAt)) {
94
+ return `${formatTime(session.startedAt)}–${formatTime(session.endedAt)}`;
95
+ }
96
+ return `${formatDateTime(session.startedAt)} ${formatDateTime(session.endedAt)}`;
97
+ }
98
+
99
+ function formatDuration(ms: number): string {
100
+ if (ms < 1000) return `${ms}ms`;
101
+ const totalSecs = ms / 1000;
102
+ if (totalSecs < 60) return `${totalSecs.toFixed(1)}s`;
103
+ const totalMins = totalSecs / 60;
104
+ if (totalMins < 60) {
105
+ const m = Math.floor(totalMins);
106
+ const s = (totalSecs % 60).toFixed(1);
107
+ return `${m}m ${s}s`;
108
+ }
109
+ const hrs = totalMins / 60;
110
+ return `${Math.floor(hrs)}h ${Math.floor(totalMins % 60)}m`;
111
+ }
112
+
113
+ export function formatRuntimeDuration(ms: number): string {
114
+ let remainingSeconds = Math.max(0, Math.floor(ms / 1000));
115
+ const weeks = Math.floor(remainingSeconds / (7 * 24 * 60 * 60));
116
+ remainingSeconds %= 7 * 24 * 60 * 60;
117
+ const days = Math.floor(remainingSeconds / (24 * 60 * 60));
118
+ remainingSeconds %= 24 * 60 * 60;
119
+ const hours = Math.floor(remainingSeconds / (60 * 60));
120
+ remainingSeconds %= 60 * 60;
121
+ const minutes = Math.floor(remainingSeconds / 60);
122
+ const seconds = remainingSeconds % 60;
123
+
124
+ const parts = [
125
+ weeks > 0 ? `${weeks}w` : undefined,
126
+ days > 0 ? `${days}d` : undefined,
127
+ hours > 0 ? `${hours}h` : undefined,
128
+ minutes > 0 ? `${minutes}m` : undefined,
129
+ `${seconds}s`,
130
+ ];
131
+ return parts.filter((part): part is string => part !== undefined).join(" ");
132
+ }
133
+
134
+ export default function (pi: ExtensionAPI) {
135
+ let taskStartTime: number | undefined;
136
+
137
+ function notifyAccent(ctx: ExtensionContext, message: string): void {
138
+ ctx.ui.notify(ctx.ui.theme.fg("accent", message), "info");
139
+ }
140
+
141
+ // Track the complete run. Automatic retries and compaction recovery can emit
142
+ // additional agent_start events before agent_settled, so retain the first start.
143
+ pi.on("agent_start", async () => {
144
+ taskStartTime ??= Date.now();
145
+ });
146
+
147
+ // Show "Sent HH:MM:SS" after each user message.
148
+ // notify(..., "info") renders a display-only status line in the TUI chat; it is not
149
+ // appended to the session and does not enter the LLM context.
150
+ pi.on("message_end", async (event, ctx) => {
151
+ if (event.message.role !== "user") return;
152
+
153
+ const ts = event.message.timestamp;
154
+ if (!ts) return;
155
+
156
+ ctx.ui.notify(`Sent ${formatTime(ts)}`, "info");
157
+ });
158
+
159
+ // Show completion timing only after retries, compaction recovery, and queued
160
+ // continuations have fully settled.
161
+ pi.on("agent_settled", async (_event, ctx) => {
162
+ const startTime = taskStartTime;
163
+ taskStartTime = undefined;
164
+
165
+ if (startTime === undefined) return;
166
+
167
+ const endTime = Date.now();
168
+ const duration = endTime - startTime;
169
+
170
+ ctx.ui.notify(`Done at ${formatTime(endTime)} · ${formatDuration(duration)}`, "info");
171
+ });
172
+
173
+ pi.on("session_start", (_event, ctx) => {
174
+ const state = getRuntimeState();
175
+ const previousSession = takePendingSessionSummary(state);
176
+ if (previousSession) {
177
+ notifyAccent(
178
+ ctx,
179
+ `Previous session complete · ${formatSessionRange(previousSession)} · ${formatRuntimeDuration(previousSession.endedAt - previousSession.startedAt)}`,
180
+ );
181
+ }
182
+
183
+ const sessionId = ctx.sessionManager.getSessionId();
184
+ // A reload rebinds the extension to the same session. Keep its timer running.
185
+ beginSession(state, sessionId, Date.now());
186
+ });
187
+
188
+ pi.on("session_shutdown", (event, ctx) => {
189
+ // Reload replaces the extension runtime but not the Pi session.
190
+ if (event.reason === "reload") return;
191
+
192
+ const endTime = Date.now();
193
+ const interval = finishSession(getRuntimeState(), endTime);
194
+
195
+ if (event.reason !== "quit") {
196
+ if (interval) getRuntimeState().pendingSessionSummary = interval;
197
+ return;
198
+ }
199
+
200
+ const state = getRuntimeState();
201
+ const sessionRows = state.completedSessions.map(
202
+ (session, index) =>
203
+ ` ${index + 1}. ${formatSessionRange(session)} · ${formatRuntimeDuration(session.endedAt - session.startedAt)}`,
204
+ );
205
+ // On normal Ctrl+D shutdown Pi has already stopped the TUI, so ui.notify()
206
+ // cannot render. Write after terminal restoration instead.
207
+ const summary = [
208
+ "Pi runtime complete",
209
+ ` Started: ${formatDateTime(state.processStartedAt)}`,
210
+ ` Ended: ${formatDateTime(endTime)}`,
211
+ ` Total: ${formatRuntimeDuration(endTime - state.processStartedAt)}`,
212
+ ...(sessionRows.length > 0 ? [" Sessions:", ...sessionRows] : []),
213
+ ].join("\n");
214
+ if (ctx.mode === "tui") {
215
+ process.stdout.write(`${ctx.ui.theme.fg("accent", summary)}\n`);
216
+ }
217
+ });
218
+ }