Skip to content
Source
Sign inSign up

[Bug]: Pull request comment tooltips ignore the Time format setting #35368

Send to my agent
Closedcoygeek(GitHub profile, opens in a new tab) opened this issue 0 comments

Before 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 settingPull request comment tooltipChat timestamp tooltip (control)
12-hour9/1/2026, 5:30:00 PM5:30 PM, 1st September 2026
24-hour9/1/2026, 5:30:00 PM17: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.tsx lines 1011-1050 (schema TimestampFormat in packages/contracts/src/settings.ts line 65); the setting-aware chat tooltip in apps/web/src/components/chat/TimelineRowParts.tsx lines 13-40; merged #22915's test expecting 5:30 PM and 17:30 for the usage-limit banner.
  • Failure source: the rendered tooltip strings under Actual behavior, captured from the production PullRequestSummaryTab (mounted in the app by PullRequestDetailPanel.tsx line 1874); CommentIdentity in apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx lines 105-145 renders new 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.

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.

  1. tanstack-source added labelsbug

  2. github-actions[bot](GitHub profile, opens in a new tab) added labelsvouch:denounced

  3. juliusmarminge closed this

Sign in to join this conversation.