feat: integrate Pi backend and Pi Web

This commit is contained in:
luckyyzh
2026-07-30 19:37:53 +08:00
commit 7392ab9dd7
1390 changed files with 337197 additions and 0 deletions
+351
View File
@@ -0,0 +1,351 @@
/**
* Import a pi session shared as a gist by the issue-analysis CI workflow
* (.github/workflows/issue-analysis.yml) and switch to it.
*
* The CI job runs in a high-entropy checkout directory; this command rewrites
* the recorded cwd to the local checkout, installs the session file into the
* current session directory, and switches to it.
*
* Usage:
* /ir b4d100022aefb12f25dd2d8485e0a82a
* /ir https://gist.github.com/mitsuhiko/b4d100022aefb12f25dd2d8485e0a82a
* /ir https://pi.dev/session/#b4d100022aefb12f25dd2d8485e0a82a
* /ir https://github.com/earendil-works/pi/issues/123
*
* pi "/ir <gist-id>"
*/
import { Buffer } from "node:buffer";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { basename, isAbsolute, join, resolve } from "node:path";
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
const GIST_ID_RE = /^[0-9a-fA-F]{20,}$/;
const GIST_URL_RE = /^https:\/\/gist\.github\.com\/(?:[^/]+\/)?([0-9a-fA-F]{20,})(?:[/#?].*)?$/;
const SHARE_URL_RE = /^https:\/\/pi\.dev\/session\/#([0-9a-fA-F]{20,})(?:[/#?].*)?$/;
const ISSUE_URL_RE = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)(?:[/#?].*)?$/;
const GIST_URL_IN_TEXT_RE = /https:\/\/gist\.github\.com\/(?:[^/\s]+\/)?([0-9a-fA-F]{20,})\b/g;
const SESSION_DATA_RE = /<script id="session-data" type="application\/json">([^<]+)<\/script>/;
interface SessionHeader {
type: "session";
id: string;
cwd: string;
[key: string]: unknown;
}
interface ExportedSessionData {
header: SessionHeader | null;
entries: Array<Record<string, unknown>>;
}
interface GistFile {
filename?: string;
raw_url?: string;
content?: string;
truncated?: boolean;
}
interface GistResponse {
files?: Record<string, GistFile>;
}
interface IssueComment {
body?: string | null;
user?: { login?: string } | null;
}
function parseRef(
ref: string,
cwd: string,
): { type: "gist"; id: string } | { type: "file"; path: string } | { type: "issue"; owner: string; repo: string; issue: string } {
if (ref.endsWith(".html") || ref.endsWith(".jsonl")) {
return { type: "file", path: isAbsolute(ref) ? ref : resolve(cwd, ref) };
}
const shareMatch = ref.match(SHARE_URL_RE);
if (shareMatch) return { type: "gist", id: shareMatch[1] };
const gistMatch = ref.match(GIST_URL_RE);
if (gistMatch) return { type: "gist", id: gistMatch[1] };
const issueMatch = ref.match(ISSUE_URL_RE);
if (issueMatch) return { type: "issue", owner: issueMatch[1], repo: issueMatch[2], issue: issueMatch[3] };
if (GIST_ID_RE.test(ref)) return { type: "gist", id: ref };
throw new Error(`expected a gist ID, gist URL, pi.dev share URL, issue URL, .html file, or .jsonl file: ${ref}`);
}
function parseSessionJsonl(raw: string): { header: SessionHeader; jsonl: string } {
const newlineIndex = raw.indexOf("\n");
const firstLine = newlineIndex === -1 ? raw : raw.slice(0, newlineIndex);
let parsed: unknown;
try {
parsed = JSON.parse(firstLine);
} catch {
throw new Error("first line of session file is not valid JSON");
}
const header = parsed as Partial<SessionHeader>;
if (header.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string" || header.cwd === "") {
throw new Error("session file has no valid session header with a cwd");
}
return { header: header as SessionHeader, jsonl: raw };
}
function decodeExportedHtml(html: string): { header: SessionHeader; jsonl: string } {
const match = html.match(SESSION_DATA_RE);
if (!match) throw new Error("HTML does not contain embedded pi session data");
let data: unknown;
try {
data = JSON.parse(Buffer.from(match[1], "base64").toString("utf8"));
} catch {
throw new Error("embedded pi session data is not valid JSON");
}
const sessionData = data as Partial<ExportedSessionData>;
const header = sessionData.header;
if (!header || header.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string") {
throw new Error("embedded pi session data has no valid session header");
}
if (!Array.isArray(sessionData.entries)) {
throw new Error("embedded pi session data has no entries array");
}
const lines = [header, ...sessionData.entries].map((entry) => JSON.stringify(entry));
return { header, jsonl: `${lines.join("\n")}\n` };
}
type SessionPlatform = "windows" | "unix" | "unknown";
function escapeJsonString(value: string): string {
return JSON.stringify(value).slice(1, -1);
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function trimTrailingPathSeparators(value: string): string {
return value.replace(/[\\/]+$/, "");
}
function getPathTailName(value: string): string {
const trimmed = trimTrailingPathSeparators(value);
return trimmed.split(/[\\/]/).filter(Boolean).at(-1) ?? "";
}
function getWindowsDrivePathParts(value: string): { drive: string; rest: string } | undefined {
const trimmed = trimTrailingPathSeparators(value);
const driveMatch = trimmed.match(/^([A-Za-z]):[\\/](.*)$/);
if (driveMatch) {
return { drive: driveMatch[1].toUpperCase(), rest: driveMatch[2].replace(/[\\/]+/g, "/") };
}
const msysMatch = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
if (msysMatch) {
return { drive: msysMatch[1].toUpperCase(), rest: msysMatch[2].replace(/[\\/]+/g, "/") };
}
return undefined;
}
function getCwdRewriteVariants(sourceCwd: string): string[] {
const trimmed = trimTrailingPathSeparators(sourceCwd);
const variants = new Set<string>();
if (trimmed) variants.add(trimmed);
const driveParts = getWindowsDrivePathParts(trimmed);
if (driveParts) {
const rest = driveParts.rest.replace(/^\/+|\/+$/g, "");
const backslashRest = rest.replace(/\//g, "\\");
variants.add(`${driveParts.drive}:\\${backslashRest}`);
variants.add(`${driveParts.drive}:/${rest}`);
variants.add(`/${driveParts.drive.toLowerCase()}/${rest}`);
variants.add(`/${driveParts.drive}/${rest}`);
}
return Array.from(variants).filter(Boolean).sort((a, b) => b.length - a.length);
}
function getCiWorkdirName(sourceCwd: string): string | undefined {
const name = getPathTailName(sourceCwd);
return /^pi-ci-[0-9a-f]{32}$/i.test(name) ? name : undefined;
}
function detectSessionPlatform(cwd: string): SessionPlatform {
if (/^[A-Za-z]:[\\/]/.test(cwd) || /^\/[A-Za-z]\//.test(cwd)) return "windows";
if (cwd.startsWith("/")) return "unix";
return "unknown";
}
function getLocalPlatform(): Exclude<SessionPlatform, "unknown"> {
return process.platform === "win32" ? "windows" : "unix";
}
function getPlatformContinuationNotice(sourceCwd: string): string | undefined {
const sourcePlatform = detectSessionPlatform(sourceCwd);
const localPlatform = getLocalPlatform();
if (sourcePlatform === "unknown" || sourcePlatform === localPlatform) return undefined;
if (localPlatform === "unix") {
return "This session was continued on a non-Windows machine; paths are now Unix style.";
}
return "This session was continued on a Windows machine; paths are now Windows style.";
}
/** Rewrite occurrences of the recorded CI cwd (JSON-escaped) to the target cwd. */
function rewriteSessionCwd(raw: string, sourceCwd: string, targetCwd: string): string {
const target = escapeJsonString(targetCwd);
let rewritten = raw;
for (const sourceVariant of getCwdRewriteVariants(sourceCwd)) {
if (sourceVariant === targetCwd) continue;
rewritten = rewritten.split(escapeJsonString(sourceVariant)).join(target);
}
const ciWorkdirName = getCiWorkdirName(sourceCwd);
if (ciWorkdirName) {
const escapedName = escapeRegExp(ciWorkdirName);
const windowsPathPatterns = [
new RegExp(`[A-Za-z]:(?:[^"\\r\\n])*?${escapedName}`, "g"),
new RegExp(`/[A-Za-z]/(?:[^"\\r\\n])*?${escapedName}`, "g"),
];
for (const pattern of windowsPathPatterns) {
rewritten = rewritten.replace(pattern, target);
}
}
return rewritten;
}
async function fetchText(url: string): Promise<string> {
const response = await fetch(url, { headers: { Accept: "application/vnd.github+json" } });
if (!response.ok) {
throw new Error(`failed to fetch ${url}: HTTP ${response.status}`);
}
return await response.text();
}
async function readGistFile(file: GistFile): Promise<string> {
if (file.content && !file.truncated) return file.content;
if (!file.raw_url) throw new Error(`gist file ${file.filename ?? "<unknown>"} has no raw URL`);
return await fetchText(file.raw_url);
}
async function findIssueGistId(owner: string, repo: string, issue: string): Promise<string> {
const gistIds: string[] = [];
let page = 1;
while (true) {
const response = await fetch(
`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${encodeURIComponent(issue)}/comments?per_page=100&page=${page}`,
{ headers: { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" } },
);
if (!response.ok) throw new Error(`failed to fetch issue comments: HTTP ${response.status}`);
const comments = (await response.json()) as IssueComment[];
for (const comment of comments) {
if (comment.user?.login !== "github-actions[bot]") continue;
for (const match of (comment.body ?? "").matchAll(GIST_URL_IN_TEXT_RE)) {
gistIds.push(match[1]);
}
}
if (comments.length < 100) break;
page++;
}
const gistId = gistIds.at(-1);
if (!gistId) throw new Error(`no github-actions gist link found in comments on ${owner}/${repo}#${issue}`);
return gistId;
}
async function fetchGistSession(gistId: string): Promise<{ header: SessionHeader; jsonl: string }> {
const response = await fetch(`https://api.github.com/gists/${gistId}`, {
headers: {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (!response.ok) throw new Error(`failed to fetch gist ${gistId}: HTTP ${response.status}`);
const gist = (await response.json()) as GistResponse;
const files = Object.values(gist.files ?? {});
const jsonlFile = files.find((file) => file.filename?.endsWith(".jsonl"));
if (jsonlFile) return parseSessionJsonl(await readGistFile(jsonlFile));
const htmlFile = files.find((file) => file.filename?.endsWith(".html"));
if (htmlFile) return decodeExportedHtml(await readGistFile(htmlFile));
throw new Error(`gist ${gistId} has no .jsonl or .html session file`);
}
export default function (pi: ExtensionAPI) {
pi.registerCommand("ir", {
description: "Import a CI issue-analysis session from a gist ID, share URL, or issue URL and switch to it",
handler: async (args: string, ctx: ExtensionCommandContext) => {
const ref = args.trim();
if (!ref) {
ctx.ui.notify("Usage: /ir <gist-id | gist-url | pi.dev/session URL | issue URL>", "error");
return;
}
try {
const targetCwd = ctx.sessionManager.getCwd();
const sessionDir = ctx.sessionManager.getSessionDir();
const parsedRef = parseRef(ref, targetCwd);
ctx.ui.notify(`Importing repro session from ${ref}...`, "info");
let sourceName: string;
let decoded: { header: SessionHeader; jsonl: string };
if (parsedRef.type === "gist") {
decoded = await fetchGistSession(parsedRef.id);
sourceName = `${parsedRef.id}.jsonl`;
} else if (parsedRef.type === "issue") {
const gistId = await findIssueGistId(parsedRef.owner, parsedRef.repo, parsedRef.issue);
decoded = await fetchGistSession(gistId);
sourceName = `${gistId}.jsonl`;
} else {
if (!existsSync(parsedRef.path)) throw new Error(`session file not found: ${parsedRef.path}`);
const raw = readFileSync(parsedRef.path, "utf8");
decoded = parsedRef.path.endsWith(".html") ? decodeExportedHtml(raw) : parseSessionJsonl(raw);
sourceName = basename(parsedRef.path).replace(/\.html$/, ".jsonl");
}
const platformNotice = getPlatformContinuationNotice(decoded.header.cwd);
const rewritten = rewriteSessionCwd(decoded.jsonl, decoded.header.cwd, targetCwd);
const destination = join(sessionDir, sourceName);
if (existsSync(destination)) {
const overwrite = await ctx.ui.confirm(
"Session already imported",
`Overwrite ${destination}? Local changes to that session will be lost.`,
);
if (!overwrite) {
ctx.ui.notify("Import cancelled", "warning");
return;
}
}
writeFileSync(destination, rewritten);
ctx.ui.notify(`Imported session ${decoded.header.id} (cwd ${decoded.header.cwd} -> ${targetCwd})`, "info");
await ctx.switchSession(destination, {
withSession: async (nextCtx) => {
if (!platformNotice) return;
await nextCtx.sendMessage(
{
customType: "import-repro",
content: platformNotice,
display: true,
details: { sourceCwd: decoded.header.cwd, targetCwd },
},
{ triggerTurn: false },
);
},
});
} catch (error) {
ctx.ui.notify(`ir: ${error instanceof Error ? error.message : String(error)}`, "error");
}
},
});
}
+270
View File
@@ -0,0 +1,270 @@
import { readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { resolve } from "node:path";
import { DynamicBorder, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
import { Container, Text } from "@earendil-works/pi-tui";
const PR_PROMPT_PATTERN = /^\s*You are given one or more GitHub PR URLs:\s*(\S+)/im;
const ISSUE_PROMPT_PATTERN = /^\s*Analyze GitHub issue\(s\):\s*(\S+)/im;
const ADVISORY_PROMPT_PATTERN = /^\s*Update a GitHub security advisory for publication:\s*(\S+)/im;
type PromptMatch = {
kind: "pr" | "issue" | "advisory";
target: string;
};
type GhMetadata = {
title?: string;
detail?: string;
displayUrl?: string;
author?: {
login?: string;
name?: string | null;
};
};
type GitHubAdvisoryMetadata = {
ghsa_id?: string;
summary?: string;
severity?: string;
state?: string;
html_url?: string;
cve_id?: string | null;
};
type AdvisoryRef = {
owner: string;
repo: string;
ghsaId: string;
url: string;
};
function extractPromptMatch(prompt: string): PromptMatch | undefined {
const prMatch = prompt.match(PR_PROMPT_PATTERN);
if (prMatch?.[1]) {
return { kind: "pr", target: prMatch[1].trim() };
}
const issueMatch = prompt.match(ISSUE_PROMPT_PATTERN);
if (issueMatch?.[1]) {
return { kind: "issue", target: issueMatch[1].trim() };
}
const advisoryMatch = prompt.match(ADVISORY_PROMPT_PATTERN);
if (advisoryMatch?.[1]) {
return { kind: "advisory", target: advisoryMatch[1].trim() };
}
return undefined;
}
function getPromptLabel(kind: PromptMatch["kind"]): string {
if (kind === "pr") return "PR";
if (kind === "issue") return "Issue";
return "Advisory";
}
function parseAdvisoryUrl(value: string): AdvisoryRef | undefined {
const match = value.match(
/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/security\/advisories\/(GHSA-[A-Za-z0-9-]+)(?:[/?#].*)?$/i,
);
if (!match?.[1] || !match[2] || !match[3]) return undefined;
return {
owner: match[1],
repo: match[2],
ghsaId: match[3],
url: `https://github.com/${match[1]}/${match[2]}/security/advisories/${match[3]}`,
};
}
function unquoteYamlValue(value: string): string {
const trimmed = value.trim();
if (
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
(trimmed.startsWith("'") && trimmed.endsWith("'"))
) {
return trimmed.slice(1, -1);
}
return trimmed;
}
function resolveDraftPath(cwd: string, target: string): string {
if (target === "~") return homedir();
if (target.startsWith("~/")) return resolve(homedir(), target.slice(2));
return resolve(cwd, target);
}
async function readAdvisoryRefFromDraft(cwd: string, target: string): Promise<AdvisoryRef | undefined> {
try {
const content = await readFile(resolveDraftPath(cwd, target), "utf8");
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
const body = frontmatter?.[1] ?? content;
const urlMatch = body.match(/^advisory_url:\s*(.+)$/m);
if (!urlMatch?.[1]) return undefined;
return parseAdvisoryUrl(unquoteYamlValue(urlMatch[1]));
} catch {
return undefined;
}
}
function formatAdvisoryDetail(advisory: GitHubAdvisoryMetadata): string | undefined {
const parts = [advisory.ghsa_id, advisory.cve_id ?? undefined, advisory.severity, advisory.state]
.map((part) => part?.trim())
.filter((part): part is string => part !== undefined && part.length > 0);
return parts.length > 0 ? parts.join(" · ") : undefined;
}
async function fetchAdvisoryMetadata(pi: ExtensionAPI, cwd: string, target: string): Promise<GhMetadata | undefined> {
const advisoryRef = parseAdvisoryUrl(target) ?? (await readAdvisoryRefFromDraft(cwd, target));
if (!advisoryRef) return undefined;
try {
const result = await pi.exec("gh", [
"api",
`repos/${advisoryRef.owner}/${advisoryRef.repo}/security-advisories/${advisoryRef.ghsaId}`,
]);
if (result.code !== 0 || !result.stdout) return { displayUrl: advisoryRef.url };
const advisory = JSON.parse(result.stdout) as GitHubAdvisoryMetadata;
return {
title: advisory.summary,
detail: formatAdvisoryDetail(advisory),
displayUrl: advisory.html_url ?? advisoryRef.url,
};
} catch {
return { displayUrl: advisoryRef.url };
}
}
async function fetchGhMetadata(
pi: ExtensionAPI,
kind: PromptMatch["kind"],
target: string,
cwd: string,
): Promise<GhMetadata | undefined> {
if (kind === "advisory") {
return fetchAdvisoryMetadata(pi, cwd, target);
}
const args =
kind === "pr"
? ["pr", "view", target, "--json", "title,author"]
: ["issue", "view", target, "--json", "title,author"];
try {
const result = await pi.exec("gh", args);
if (result.code !== 0 || !result.stdout) return undefined;
return JSON.parse(result.stdout) as GhMetadata;
} catch {
return undefined;
}
}
function formatAuthor(author?: GhMetadata["author"]): string | undefined {
if (!author) return undefined;
const name = author.name?.trim();
const login = author.login?.trim();
if (name && login) return `${name} (@${login})`;
if (login) return `@${login}`;
if (name) return name;
return undefined;
}
export default function promptUrlWidgetExtension(pi: ExtensionAPI) {
const setWidget = (ctx: ExtensionContext, match: PromptMatch, metadata?: GhMetadata) => {
ctx.ui.setWidget("prompt-url", (_tui, thm) => {
const displayTarget = metadata?.displayUrl ?? match.target;
const titleText = metadata?.title
? thm.fg("accent", metadata.title)
: thm.fg("accent", displayTarget);
const detailText = metadata?.detail ?? formatAuthor(metadata?.author);
const detailLine = detailText ? thm.fg("muted", detailText) : undefined;
const urlLine = thm.fg("dim", displayTarget);
const lines = [titleText];
if (detailLine) lines.push(detailLine);
lines.push(urlLine);
const container = new Container();
container.addChild(new DynamicBorder((s: string) => thm.fg("muted", s)));
container.addChild(new Text(lines.join("\n"), 1, 0));
return container;
});
};
const applySessionName = (ctx: ExtensionContext, match: PromptMatch, metadata?: GhMetadata) => {
const label = getPromptLabel(match.kind);
const displayTarget = metadata?.displayUrl ?? match.target;
const trimmedTitle = metadata?.title?.trim();
const fallbackName = `${label}: ${match.target}`;
const desiredFallbackName = `${label}: ${displayTarget}`;
const desiredName = trimmedTitle ? `${label}: ${trimmedTitle} (${displayTarget})` : desiredFallbackName;
const currentName = pi.getSessionName()?.trim();
if (!currentName) {
pi.setSessionName(desiredName);
return;
}
if (currentName === match.target || currentName === fallbackName || currentName === desiredFallbackName) {
pi.setSessionName(desiredName);
}
};
const updatePromptContext = (ctx: ExtensionContext, match: PromptMatch) => {
setWidget(ctx, match);
applySessionName(ctx, match);
void fetchGhMetadata(pi, match.kind, match.target, ctx.cwd).then((meta) => {
setWidget(ctx, match, meta);
applySessionName(ctx, match, meta);
});
};
pi.on("before_agent_start", async (event, ctx) => {
if (!ctx.hasUI) return;
const match = extractPromptMatch(event.prompt);
if (!match) {
return;
}
updatePromptContext(ctx, match);
});
pi.on("session_switch", async (_event, ctx) => {
rebuildFromSession(ctx);
});
const getUserText = (content: string | { type: string; text?: string }[] | undefined): string => {
if (!content) return "";
if (typeof content === "string") return content;
return (
content
.filter((block): block is { type: "text"; text: string } => block.type === "text")
.map((block) => block.text)
.join("\n") ?? ""
);
};
const rebuildFromSession = (ctx: ExtensionContext) => {
if (!ctx.hasUI) return;
const entries = ctx.sessionManager.getEntries();
const lastMatch = [...entries].reverse().find((entry) => {
if (entry.type !== "message" || entry.message.role !== "user") return false;
const text = getUserText(entry.message.content);
return !!extractPromptMatch(text);
});
const content =
lastMatch?.type === "message" && lastMatch.message.role === "user" ? lastMatch.message.content : undefined;
const text = getUserText(content);
const match = text ? extractPromptMatch(text) : undefined;
if (!match) {
ctx.ui.setWidget("prompt-url", undefined);
return;
}
updatePromptContext(ctx, match);
};
pi.on("session_start", async (_event, ctx) => {
rebuildFromSession(ctx);
});
}
+24
View File
@@ -0,0 +1,24 @@
/**
* Redraws Extension
*
* Exposes /tui to show TUI redraw stats.
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Text } from "@earendil-works/pi-tui";
export default function (pi: ExtensionAPI) {
pi.registerCommand("tui", {
description: "Show TUI stats",
handler: async (_args, ctx) => {
if (!ctx.hasUI) return;
let redraws = 0;
await ctx.ui.custom<void>((tui, _theme, _keybindings, done) => {
redraws = tui.fullRedraws;
done(undefined);
return new Text("", 0, 0);
});
ctx.ui.notify(`TUI full redraws: ${redraws}`, "info");
},
});
}
+47
View File
@@ -0,0 +1,47 @@
import type { AssistantMessage } from "@earendil-works/pi-ai";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
function isAssistantMessage(message: unknown): message is AssistantMessage {
if (!message || typeof message !== "object") return false;
const role = (message as { role?: unknown }).role;
return role === "assistant";
}
export default function (pi: ExtensionAPI) {
let agentStartMs: number | null = null;
pi.on("agent_start", () => {
agentStartMs = Date.now();
});
pi.on("agent_end", (event, ctx) => {
if (!ctx.hasUI) return;
if (agentStartMs === null) return;
const elapsedMs = Date.now() - agentStartMs;
agentStartMs = null;
if (elapsedMs <= 0) return;
let input = 0;
let output = 0;
let cacheRead = 0;
let cacheWrite = 0;
let totalTokens = 0;
for (const message of event.messages) {
if (!isAssistantMessage(message)) continue;
input += message.usage.input || 0;
output += message.usage.output || 0;
cacheRead += message.usage.cacheRead || 0;
cacheWrite += message.usage.cacheWrite || 0;
totalTokens += message.usage.totalTokens || 0;
}
if (output <= 0) return;
const elapsedSeconds = elapsedMs / 1000;
const tokensPerSecond = output / elapsedSeconds;
const message = `TPS ${tokensPerSecond.toFixed(1)} tok/s. out ${output.toLocaleString()}, in ${input.toLocaleString()}, cache r/w ${cacheRead.toLocaleString()}/${cacheWrite.toLocaleString()}, total ${totalTokens.toLocaleString()}, ${elapsedSeconds.toFixed(1)}s`;
ctx.ui.notify(message, "info");
});
}