From 55e2b473aa926f1bbd7bb13a76a2160f383436b8 Mon Sep 17 00:00:00 2001 From: roboomp Date: Sat, 4 Jul 2026 17:42:56 +0000 Subject: [PATCH] fix(tui): avoided auto-linked token rate format Rendered the status-line token rate as an explicit tok/s unit so Ghostty no longer auto-detects the numeric value as a URL. Added a regression test for the token_rate segment rendering contract. Fixes #4541 --- packages/coding-agent/CHANGELOG.md | 2 + .../modes/components/status-line/segments.ts | 2 +- .../test/status-line-token-rate.test.ts | 37 ++++++++++++++++++- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9c2395e3d..1503839a9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -12,6 +12,8 @@ ### Fixed +- Fixed the status-line token-rate segment rendering as `/s`, which Ghostty auto-detected as a hyperlink on Ctrl+hover. ([#4541](https://github.com/can1357/oh-my-pi/issues/4541)) + - Fixed raw `read` ranges not contributing to edit seen-line provenance, so re-reading an anchor range with `:raw` now unblocks hashline edits without adding non-raw line prefixes. - Fixed replan-driven session title refresh updating the statusline but not the terminal window title: terminal-title updates now fire from the session-name-changed listener, so every `setSessionName` path (first-input titling, `/rename`, plan seeding, replan refresh) sets the OSC title consistently. - Fixed user-interrupt aborts rendering the persisted `Interrupted by user` label in assistant transcripts; replay and live views now suppress that redundant line again while preserving generic/custom abort labels. diff --git a/packages/coding-agent/src/modes/components/status-line/segments.ts b/packages/coding-agent/src/modes/components/status-line/segments.ts index 0b1487a7d..a68c5ae23 100644 --- a/packages/coding-agent/src/modes/components/status-line/segments.ts +++ b/packages/coding-agent/src/modes/components/status-line/segments.ts @@ -379,7 +379,7 @@ const tokenRateSegment: StatusLineSegment = { const { tokensPerSecond } = ctx.usageStats; if (!tokensPerSecond) return { content: "", visible: false }; - const content = withIcon(theme.icon.throughput, `${tokensPerSecond.toFixed(1)}/s`); + const content = withIcon(theme.icon.throughput, `${tokensPerSecond.toFixed(1)} tok/s`); return { content: theme.fg("statusLineOutput", content), visible: true }; }, }; diff --git a/packages/coding-agent/test/status-line-token-rate.test.ts b/packages/coding-agent/test/status-line-token-rate.test.ts index d894216d1..8de667d3b 100644 --- a/packages/coding-agent/test/status-line-token-rate.test.ts +++ b/packages/coding-agent/test/status-line-token-rate.test.ts @@ -1,6 +1,14 @@ -import { describe, expect, it } from "bun:test"; +import { beforeAll, describe, expect, it } from "bun:test"; +import { stripVTControlCharacters } from "node:util"; import type { AssistantMessage } from "@oh-my-pi/pi-ai"; +import { renderSegment } from "@oh-my-pi/pi-coding-agent/modes/components/status-line/segments"; import { calculateTokensPerSecond } from "@oh-my-pi/pi-coding-agent/modes/components/status-line/token-rate"; +import type { SegmentContext } from "@oh-my-pi/pi-coding-agent/modes/components/status-line/types"; +import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; + +beforeAll(async () => { + await initTheme(); +}); function assistantMessage(overrides?: Partial): AssistantMessage { return { @@ -23,6 +31,33 @@ function assistantMessage(overrides?: Partial): AssistantMessa }; } +function ctxWithTokenRate(tokensPerSecond: number | null): SegmentContext { + return { + usageStats: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + premiumRequests: 0, + cost: 0, + tokensPerSecond, + }, + } as unknown as SegmentContext; +} + +describe("token_rate status-line segment", () => { + it("renders per-second throughput without a numeric slash path", () => { + const rendered = renderSegment("token_rate", ctxWithTokenRate(35.5)); + const content = stripVTControlCharacters(rendered.content); + + expect(rendered.visible).toBe(true); + expect(content).toContain("35.5"); + expect(content).toMatch(/(?:\/s|\bs\b|\bsec(?:ond)?s?\b|\btps\b)/i); + expect(content).not.toContain("35.5/s"); + expect(content).not.toMatch(/\b\d+(?:\.\d+)?\/s\b/); + }); +}); + describe("token rate calculation", () => { it("computes from completed message duration metadata", () => { const base = assistantMessage();