[Bug]: Pull request comment tooltips ignore the Time format setting #35368
Send to my agentBefore submitting#
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area#
apps/web
Steps to reproduce#
In the app (not executed for this report; the script below drives the same production components): open Settings → Display, set Time format to 24-hour, open a pull request with at least one comment in the pull request panel, stay on the Summary tab and hover a comment's relative time. Compare with hovering a chat message's timestamp.
Scripted: with a checkout of the commit under Version or commit, its locked dependencies installed and Playwright's Chromium available, run sh repro.sh <path-to-checkout>. The script writes a temporary browser test beside PullRequestSummaryTab.tsx and deletes it on exit. The test renders the exported production PullRequestSummaryTab with one review comment whose createdAt is 2026-09-01T17:30:00Z, and beside it the production chat timestamp component TimelineRowTimestamp as a control. For each of 12-hour and 24-hour it changes the setting through the real persistClientSettingsPatch, confirms through a rendered useClientSettings subscriber that the setting took effect, then hovers the comment's <time> element and the control and reads each tooltip's text from the DOM. Date formatting, the tooltip and the settings hook are not mocked; only environment, permission and local-persistence boundaries are. It exits 0 and prints DEFECT_CONFIRMED while the defect is present.
Reproduction script: repro.sh
#!/bin/sh
set -eu
checkout=${1:?Usage: sh repro.sh /path/to/dependency-installed/t3code}
checkout=$(cd "$checkout" && pwd)
fixture=$(mktemp -d "${TMPDIR:-/tmp}/pr-tooltip.XXXXXX")
test_file="$checkout/apps/web/src/components/pullRequest/verifyPrTooltipTimeFormat.browser.test.tsx"
if [ -e "$test_file" ]; then echo 'refusing to overwrite existing test fixture' >&2; exit 2; fi
cleanup() { rm -f "$test_file"; rm -rf "$fixture"; }
trap cleanup EXIT HUP INT TERM
if [ -n "${PLAYWRIGHT_BROWSERS_PATH:-}" ]; then browser_cache=$PLAYWRIGHT_BROWSERS_PATH
elif [ "$(uname -s)" = Darwin ]; then browser_cache="$HOME/Library/Caches/ms-playwright"
else browser_cache="$HOME/.cache/ms-playwright"; fi
mkdir -p "$fixture/fake-home" "$fixture/tmp" "$fixture/t3home"
cat > "$test_file" <<'TSX'
import { SourceControlProviderKind } from "@t3tools/contracts";
import { EnvironmentId, ProjectId, type PullRequestDetailView } from "@t3tools/contracts";
import { expect, it, vi } from "vite-plus/test";
import { userEvent } from "vite-plus/test/browser";
const state = vi.hoisted(() => ({ update: vi.fn(), canUpdate: true }));
vi.mock(import("~/state/use-atom-command"), async (original) => ({
...(await original()),
useAtomCommand: (() => state.update) as never,
}));
vi.mock(import("~/state/session"), async (original) => ({
...(await original()),
readEnvironmentScope: () => true,
useEnvironmentScope: () => true,
}));
vi.mock(import("~/state/pullRequests"), async (original) => {
const actual = await original();
const { Atom } = await import("effect/reactivity");
const allowed = Atom.make(true);
const denied = Atom.make(false);
return {
...actual,
pullRequestEnvironment: {
...actual.pullRequestEnvironment,
update: {
...actual.pullRequestEnvironment.update,
permissionAtom: () => (state.canUpdate ? allowed : denied),
},
},
};
});
import { render as renderUi } from "../../../test/render";
import { PullRequestSummaryTab } from "./PullRequestSummaryTab";
import { TimelineRowTimestamp } from "../chat/TimelineRowParts";
import { useClientSettings, persistClientSettingsPatch } from "../../hooks/useSettings";
import { act } from "../../../test/render";
vi.mock(import("~/localApi"), async (original) => ({
...(await original()),
ensureLocalApi: () => ({ persistence: {
getClientSettings: async () => null,
setClientSettings: async () => {},
} }),
}));
const detail: PullRequestDetailView = {
provider: SourceControlProviderKind.make("github"),
projectId: ProjectId.make("project"),
projectTitle: "Project",
workspaceRoot: "/workspace",
repository: "owner/repo",
number: 1,
title: "Test pull request",
body: "Body",
url: "https://github.com/owner/repo/pull/1",
author: { login: "author", name: null, avatarUrl: null },
viewer: "author",
state: "open",
isDraft: false,
mergeability: "mergeable",
additions: 1,
deletions: 0,
changedFiles: 1,
headBranch: "feature",
baseBranch: "main",
createdAt: "2026-09-01T00:00:00Z",
updatedAt: "2026-09-01T00:00:00Z",
mergedAt: null,
closedAt: null,
reviewers: [],
labels: [],
checks: [],
comments: [],
commentCount: 0,
commentsTruncated: false,
reviewThreads: [],
commits: [],
mergeCapabilities: { merge: false, squash: false, rebase: false },
capabilities: {
diff: true,
comment: true,
search: true,
actions: [],
mergeMethods: [],
review: { inlineComment: false, reply: false, resolve: false, verdicts: [] },
reviewers: { request: false, listCandidates: false },
edit: { changeRequest: true, comment: true },
},
viewerPermissions: {
actions: [],
comment: true,
resolve: false,
verdicts: [],
requestReviewers: false,
},
};
const comment = {
id: "10",
kind: "review-comment" as const,
body: "A remark",
author: { login: "reviewer", name: null, avatarUrl: null },
createdAt: "2026-09-01T17:30:00Z",
url: null,
path: null,
reviewState: null,
};
function Fixture() {
const timestampFormat = useClientSettings(s => s.timestampFormat);
const value: PullRequestDetailView = { ...detail, comments: [comment], commentCount: 1 };
return <>
<div data-testid="setting">{timestampFormat}</div>
<div data-testid="control"><TimelineRowTimestamp createdAt={comment.createdAt} timestampFormat={timestampFormat} alwaysVisible /></div>
<PullRequestSummaryTab scrollerRef={() => {}} environmentId={EnvironmentId.make("environment")}
threadRef={null} reference={value} detail={value} activityPending={false} activityError={null} onRefresh={() => {}} />
</>;
}
async function tooltipFor(element: Element): Promise<string> {
await userEvent.hover(element);
await vi.waitFor(() => expect(document.querySelector('[data-slot="tooltip-popup"]')).not.toBeNull());
const value = document.querySelector('[data-slot="tooltip-popup"]')!.textContent!.trim();
await userEvent.hover(document.body);
await vi.waitFor(() => expect(document.querySelector('[data-slot="tooltip-popup"]')).toBeNull());
return value;
}
it("captures real PR and sibling chat timestamp tooltips for both settings", async () => {
state.update.mockReset(); state.canUpdate = true;
const rows: { setting: string; pr: string; control: string }[] = [];
await using screen = await renderUi(<Fixture />);
for (const setting of ["12-hour", "24-hour"] as const) {
await act(() => persistClientSettingsPatch({ timestampFormat: setting }));
await vi.waitFor(() => expect(screen.container.querySelector('[data-testid="setting"]')!.textContent).toBe(setting));
const pr = await tooltipFor(screen.container.querySelector("time")!);
const control = await tooltipFor(screen.container.querySelector('[data-testid="control"] [data-slot="tooltip-trigger"]') ?? screen.container.querySelector('[data-testid="control"] span')!);
const row = { setting, pr, control };
rows.push(row);
console.log("CAPTURE " + JSON.stringify(row));
}
console.log("BROWSER " + JSON.stringify({ userAgent: navigator.userAgent, locale: navigator.language, zone: Intl.DateTimeFormat().resolvedOptions().timeZone, raw: new Date(comment.createdAt).toLocaleString() }));
expect(rows[0]!.pr).toBe(new Date(comment.createdAt).toLocaleString());
expect(rows[1]!.pr).toBe(rows[0]!.pr);
expect(rows[0]!.control).not.toBe(rows[1]!.control);
const date = new Date(comment.createdAt);
const hour = date.getHours();
expect(rows[0]!.control).toContain(`${hour % 12 || 12}:30`);
expect(rows[0]!.control).toMatch(/(?:AM|PM)/i);
expect(rows[1]!.control).toContain(`${String(hour).padStart(2, "0")}:30`);
expect(rows[1]!.control).not.toMatch(/(?:AM|PM)/i);
console.log("DEFECT_CONFIRMED: PR tooltip ignores both explicit settings; rendered sibling timestamp honors them");
});
TSX
cd "$checkout/apps/web"
status=0
env -u NODE_ENV HOME="$fixture/fake-home" TMPDIR="$fixture/tmp" T3CODE_HOME="$fixture/t3home" \
PLAYWRIGHT_BROWSERS_PATH="$browser_cache" TZ=UTC PATH="$checkout/node_modules/.bin:$PATH" \
vp test run --project browser --silent=false --reporter=verbose src/components/pullRequest/verifyPrTooltipTimeFormat.browser.test.tsx > "$fixture/test.log" 2>&1 || status=$?
cat "$fixture/test.log"
[ "$status" -eq 0 ] && grep -q 'DEFECT_CONFIRMED:' "$fixture/test.log"
Expected behavior#
A clock time the app displays follows the Time format setting. The setting's own description says only System default "follows your browser or OS clock preference" (apps/web/src/components/settings/SettingsPanels.tsx lines 1011-1050), so an explicit 12-hour or 24-hour choice should decide the clock. The production chat timestamp tooltip does this for the same instant (TimelineRowTimestamp in apps/web/src/components/chat/TimelineRowParts.tsx lines 13-40 formats its tooltip with formatChatTimestampTooltip(createdAt, timestampFormat)). Merged #22915 (commit a27b246162a895cc94f96925c361ab73b094154e) replaced a raw toLocaleString() with setting-aware formatting for the usage-limit banner's reset time and tests both 5:30 PM and 17:30; #15570 fixed the same class of problem for the Usage hourly labels. With 24-hour selected, the pull request comment tooltip should show a 24-hour time (17:30 for this input, no AM/PM); with 12-hour selected, a 12-hour time. Which date layout and precision the tooltip uses is left to the maintainers.
Actual behavior#
Hovering a comment's relative time ("2 months ago") in the pull request panel's Summary tab opens a tooltip with the comment's exact date and time. That tooltip always uses the browser locale's own date and clock format and ignores the Time format setting (Settings → Display → Time format: System default, 12-hour, 24-hour). With the setting on 24-hour, the tooltip still reads 9/1/2026, 5:30:00 PM in an en-US browser, while the chat timestamp tooltip in the same app reads 17:30, 1st September 2026 for the same instant. By the same code path, a user who picks 12-hour in a browser whose locale defaults to a 24-hour clock would get 24-hour text in this tooltip (that case was not run). The relative time on the comment row and the instant shown are correct; only the clock presentation ignores the setting. CommentIdentity in apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx renders {new Date(comment.createdAt).toLocaleString()} inside its TooltipPopup (lines 137-140 on the baseline) and never reads timestampFormat; both the collapsed and expanded comment headers use CommentIdentity.
The script exited 0 on the commit under Version or commit from a clean checkout and left git status with no changes of its own. Rendered tooltip text, en-US Chromium, UTC:
| Time format setting | Pull request comment tooltip | Chat timestamp tooltip (control) |
|---|---|---|
| 12-hour | 9/1/2026, 5:30:00 PM | 5:30 PM, 1st September 2026 |
| 24-hour | 9/1/2026, 5:30:00 PM | 17:30, 1st September 2026 |
The pull request comment tooltip is identical under both settings and equals new Date("2026-09-01T17:30:00Z").toLocaleString() in that browser; the control changes with the setting. An earlier, independent run of the same script on the same baseline produced identical strings. The relevant output is under Logs or stack traces.
Evidence#
- Expected source: the Time format setting row and its description in
apps/web/src/components/settings/SettingsPanels.tsxlines 1011-1050 (schemaTimestampFormatinpackages/contracts/src/settings.tsline 65); the setting-aware chat tooltip inapps/web/src/components/chat/TimelineRowParts.tsxlines 13-40; merged #22915's test expecting5:30 PMand17:30for the usage-limit banner. - Failure source: the rendered tooltip strings under Actual behavior, captured from the production
PullRequestSummaryTab(mounted in the app byPullRequestDetailPanel.tsxline 1874);CommentIdentityinapps/web/src/components/pullRequest/PullRequestSummaryTab.tsxlines 105-145 rendersnew Date(comment.createdAt).toLocaleString()at line 139 and reads no setting. - Evidence provenance: observed
- Local verification: reproduced
- Reproduction completeness: complete
The reproduction covers en-US Chromium in UTC through the browser test harness. It does not cover the packaged desktop app, a live host-backed pull request view, other locales such as en-GB, or a browser whose locale defaults to a 24-hour clock; the reverse case in the Summary is inferred from toLocaleString() using the browser default.
Related#
Closed, unmerged #12867 (fix(web): honor clock preferences in detail views) proposed changing this tooltip to a setting-aware formatter as part of a broader change; it was closed for lacking current mounted-view evidence, and its closing comment also asked for maintainer agreement on the broader clock-preference contract carried from #12661. This report rests only on the existing Time format setting and the formatting already applied by merged #22915 and the chat timestamp tooltip. Open #31916 (fix(web): clock times in settings and toasts follow the Time format setting) names this tooltip as also ignoring the setting and leaves it out of scope until #28841 lands. Open #28841 (redesign stack, marked do not merge) moves this tooltip into a new PullRequestCommentTime component in PullRequestQuietEntries.tsx that still renders new Date(createdAt).toLocaleString().
Restoration check#
With the script's input, the pull request comment tooltip under 24-hour contains 17:30 and no AM/PM, and under 12-hour contains 5:30 with PM, matching the control's clock in each case while still identifying 1 September 2026. The script's assertions encode the current defect (the comment tooltip is unchanged between the two settings and equals the raw toLocaleString()), so they fail once it is fixed. The control keeps passing: the chat timestamp tooltip still reads 5:30 PM, 1st September 2026 and 17:30, 1st September 2026, and the comment row's relative time is unchanged.
Impact#
Cosmetic issue
Version or commit#
T3 Code 0.1.9 at d9805b2ca667be7caec8c9035950564ab96704c3, rerun from a clean checkout. Upstream main at 4e48d9e6a5aba8d0184ac2c953598086ed034c29 still renders {new Date(comment.createdAt).toLocaleString()} in CommentIdentity's tooltip (line 140), and apps/web/src/timestampFormat.ts and apps/web/src/components/chat/TimelineRowParts.tsx are unchanged there; the script was not run on main (an attempt on an earlier main commit stopped before running tests because the baseline's installed dependencies lack the newly imported gt-react).
Environment#
macOS on arm64, Node v26.12.1, Vitest browser mode (vp test run --project browser) from apps/web on headless Chromium 156, en-US locale, TZ=UTC; no running app, server or provider needed.
Logs or stack traces#
The script's relevant output (environment-discovery warnings from the test harness, which runs without a T3 server, omitted):
CAPTURE {"setting":"12-hour","pr":"9/1/2026, 5:30:00 PM","control":"5:30 PM, 1st September 2026"}
CAPTURE {"setting":"24-hour","pr":"9/1/2026, 5:30:00 PM","control":"17:30, 1st September 2026"}
BROWSER {"userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36","locale":"en-US","zone":"UTC","raw":"9/1/2026, 5:30:00 PM"}
DEFECT_CONFIRMED: PR tooltip ignores both explicit settings; rendered sibling timestamp honors them
✓ |browser (chromium)| src/components/pullRequest/verifyPrTooltipTimeFormat.browser.test.tsx > captures real PR and sibling chat timestamp tooltips for both settings 3006ms
Test Files 1 passed (1)
Tests 1 passed (1)
Screenshots, recordings, or supporting files#
No response
Workaround#
The tooltip's time is correct and readable in either clock, and because it uses the browser default locale, a user whose browser or OS clock preference matches their chosen Time format sees the matching clock.
tanstack-source added labelsbug
github-actions[bot](GitHub profile, opens in a new tab) added labelsvouch:denounced
juliusmarminge closed this
Sign in to join this conversation.