timestamps are awesome
This commit is contained in:
@@ -2,29 +2,36 @@
|
|||||||
* Timestamps extension for Pi.
|
* Timestamps extension for Pi.
|
||||||
*
|
*
|
||||||
* - Shows elapsed session time in footer (updates every second)
|
* - Shows elapsed session time in footer (updates every second)
|
||||||
* - Shows how long the last turn took (from your message to agent completion)
|
* - Shows how long the last turn took
|
||||||
|
* - Injects timestamp markers without triggering extra turns
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { Box, Text } from "@mariozechner/pi-tui";
|
||||||
|
|
||||||
// Track session time and turn durations
|
// Track session time
|
||||||
let sessionStart = Date.now();
|
let sessionStart = Date.now();
|
||||||
let timerHandle: ReturnType<typeof setInterval> | null = null;
|
let timerHandle: ReturnType<typeof setInterval> | null = null;
|
||||||
let turnStartTime: number | null = null;
|
let turnStartTime: number | null = null;
|
||||||
let lastTurnDuration: number | null = null;
|
let lastTurnDuration: number | null = null;
|
||||||
|
|
||||||
|
function formatTime(date: Date): string {
|
||||||
|
return date.toLocaleTimeString("en-US", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function formatElapsed(ms: number): string {
|
function formatElapsed(ms: number): string {
|
||||||
const seconds = Math.floor(ms / 1000);
|
const seconds = Math.floor(ms / 1000);
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
|
|
||||||
if (hours > 0) {
|
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
||||||
return `${hours}h ${minutes % 60}m`;
|
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
||||||
} else if (minutes > 0) {
|
return `${seconds}s`;
|
||||||
return `${minutes}m ${seconds % 60}s`;
|
|
||||||
} else {
|
|
||||||
return `${seconds}s`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDuration(ms: number): string {
|
function formatDuration(ms: number): string {
|
||||||
@@ -37,25 +44,47 @@ export default function (pi: ExtensionAPI) {
|
|||||||
const updateStatus = (ctx: { ui: { setStatus: (id: string, text: string | undefined) => void; theme: { fg: (color: string, text: string) => string } } }) => {
|
const updateStatus = (ctx: { ui: { setStatus: (id: string, text: string | undefined) => void; theme: { fg: (color: string, text: string) => string } } }) => {
|
||||||
const elapsed = Date.now() - sessionStart;
|
const elapsed = Date.now() - sessionStart;
|
||||||
let status = ctx.ui.theme.fg("dim", `⏱ ${formatElapsed(elapsed)}`);
|
let status = ctx.ui.theme.fg("dim", `⏱ ${formatElapsed(elapsed)}`);
|
||||||
|
|
||||||
// Show last turn duration if available
|
|
||||||
if (lastTurnDuration !== null) {
|
if (lastTurnDuration !== null) {
|
||||||
status += ctx.ui.theme.fg("muted", ` | took ${formatDuration(lastTurnDuration)}`);
|
status += ctx.ui.theme.fg("muted", ` | took ${formatDuration(lastTurnDuration)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.ui.setStatus("timestamps", status);
|
ctx.ui.setStatus("timestamps", status);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Renderer for user timestamp
|
||||||
|
pi.registerMessageRenderer("timestamp-prefix", (message, _options, theme) => {
|
||||||
|
const details = message.details as { timestamp: number; elapsed: string } | undefined;
|
||||||
|
if (!details) return new Text("");
|
||||||
|
|
||||||
|
const timeStr = formatTime(new Date(details.timestamp));
|
||||||
|
const line = theme.fg("dim", `◆ ${timeStr} (+${details.elapsed})`);
|
||||||
|
const box = new Box(0, 0, undefined);
|
||||||
|
box.addChild(new Text(line, 0, 0));
|
||||||
|
return box;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Renderer for assistant timestamp
|
||||||
|
pi.registerMessageRenderer("timestamp-suffix", (message, _options, theme) => {
|
||||||
|
const details = message.details as { timestamp: number; elapsed: string; duration?: number } | undefined;
|
||||||
|
if (!details) return new Text("");
|
||||||
|
|
||||||
|
const timeStr = formatTime(new Date(details.timestamp));
|
||||||
|
let line = `○ ${timeStr} (+${details.elapsed})`;
|
||||||
|
if (details.duration && details.duration > 1000) {
|
||||||
|
line += ` • took ${formatDuration(details.duration)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const box = new Box(0, 0, undefined);
|
||||||
|
box.addChild(new Text(theme.fg("dim", line), 0, 0));
|
||||||
|
return box;
|
||||||
|
});
|
||||||
|
|
||||||
// Start timer on session start
|
// Start timer on session start
|
||||||
pi.on("session_start", async (_event, ctx) => {
|
pi.on("session_start", async (_event, ctx) => {
|
||||||
sessionStart = Date.now();
|
sessionStart = Date.now();
|
||||||
turnStartTime = null;
|
turnStartTime = null;
|
||||||
lastTurnDuration = null;
|
lastTurnDuration = null;
|
||||||
|
|
||||||
// Clear any existing timer
|
|
||||||
if (timerHandle) clearInterval(timerHandle);
|
if (timerHandle) clearInterval(timerHandle);
|
||||||
|
|
||||||
// Update status every second
|
|
||||||
timerHandle = setInterval(() => updateStatus(ctx), 1000);
|
timerHandle = setInterval(() => updateStatus(ctx), 1000);
|
||||||
updateStatus(ctx);
|
updateStatus(ctx);
|
||||||
});
|
});
|
||||||
@@ -71,6 +100,35 @@ export default function (pi: ExtensionAPI) {
|
|||||||
turnStartTime = null;
|
turnStartTime = null;
|
||||||
updateStatus(ctx);
|
updateStatus(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lastTurnDuration !== null) {
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = formatElapsed(now - sessionStart);
|
||||||
|
pi.sendMessage(
|
||||||
|
{
|
||||||
|
customType: "timestamp-suffix",
|
||||||
|
content: "",
|
||||||
|
display: true,
|
||||||
|
details: { timestamp: now, elapsed, duration: lastTurnDuration },
|
||||||
|
},
|
||||||
|
{ deliverAs: "followUp", triggerTurn: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inject timestamp marker right before agent starts
|
||||||
|
pi.on("before_agent_start", async (event) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = formatElapsed(now - sessionStart);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
customType: "timestamp-prefix",
|
||||||
|
content: "",
|
||||||
|
display: true,
|
||||||
|
details: { timestamp: now, elapsed },
|
||||||
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up on shutdown
|
// Clean up on shutdown
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"lastChangelogVersion": "0.55.4",
|
"lastChangelogVersion": "0.55.4",
|
||||||
"defaultProvider": "openrouter",
|
"defaultProvider": "openrouter",
|
||||||
"defaultModel": "moonshotai/kimi-k2.5",
|
"defaultModel": "openai/gpt-5.2-codex",
|
||||||
"defaultThinkingLevel": "off",
|
"defaultThinkingLevel": "off",
|
||||||
"theme": "matugen"
|
"theme": "matugen"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user