fix(inbox): address Greptile review findings

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-02 12:16:34 -05:00
parent 50e9f69010
commit 833842b391
8 changed files with 49 additions and 29 deletions

View file

@ -143,7 +143,6 @@ export interface Issue {
mentionedProjects?: Project[];
myLastTouchAt?: Date | null;
lastExternalCommentAt?: Date | null;
lastActivityAt?: Date | null;
isUnreadForMe?: boolean;
createdAt: Date;
updatedAt: Date;

View file

@ -1075,7 +1075,6 @@ export function agentRoutes(db: Db) {
projectId: issue.projectId,
goalId: issue.goalId,
parentId: issue.parentId,
lastActivityAt: (issue as typeof issue & { lastActivityAt?: Date | null }).lastActivityAt ?? issue.updatedAt,
updatedAt: issue.updatedAt,
activeRun: issue.activeRun,
})),

View file

@ -17,6 +17,7 @@ import { MobileBottomNav } from "./MobileBottomNav";
import { WorktreeBanner } from "./WorktreeBanner";
import { DevRestartBanner } from "./DevRestartBanner";
import { useDialog } from "../context/DialogContext";
import { GeneralSettingsProvider } from "../context/GeneralSettingsContext";
import { usePanel } from "../context/PanelContext";
import { useCompany } from "../context/CompanyContext";
import { useSidebar } from "../context/SidebarContext";
@ -265,12 +266,13 @@ export function Layout() {
}, [location.hash, location.pathname, location.search]);
return (
<div
<GeneralSettingsProvider value={{ keyboardShortcutsEnabled }}>
<div
className={cn(
"bg-background text-foreground pt-[env(safe-area-inset-top)]",
isMobile ? "min-h-dvh" : "flex h-dvh flex-col overflow-hidden",
)}
>
>
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:left-3 focus:top-3 focus:z-[200] focus:rounded-md focus:bg-background focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
@ -442,6 +444,7 @@ export function Layout() {
<NewGoalDialog />
<NewAgentDialog />
<ToastViewport />
</div>
</div>
</GeneralSettingsProvider>
);
}

View file

@ -0,0 +1,28 @@
import type { ReactNode } from "react";
import { createContext, useContext } from "react";
export interface GeneralSettingsContextValue {
keyboardShortcutsEnabled: boolean;
}
const GeneralSettingsContext = createContext<GeneralSettingsContextValue>({
keyboardShortcutsEnabled: false,
});
export function GeneralSettingsProvider({
value,
children,
}: {
value: GeneralSettingsContextValue;
children: ReactNode;
}) {
return (
<GeneralSettingsContext.Provider value={value}>
{children}
</GeneralSettingsContext.Provider>
);
}
export function useGeneralSettings() {
return useContext(GeneralSettingsContext);
}

View file

@ -180,7 +180,6 @@ function makeIssue(id: string, isUnreadForMe: boolean): Issue {
labelIds: [],
myLastTouchAt: new Date("2026-03-11T00:00:00.000Z"),
lastExternalCommentAt: new Date("2026-03-11T01:00:00.000Z"),
lastActivityAt: new Date("2026-03-11T01:00:00.000Z"),
isUnreadForMe,
};
}
@ -358,10 +357,10 @@ describe("inbox helpers", () => {
it("mixes approvals into the inbox feed by most recent activity", () => {
const newerIssue = makeIssue("1", true);
newerIssue.lastActivityAt = new Date("2026-03-11T04:00:00.000Z");
newerIssue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z");
const olderIssue = makeIssue("2", false);
olderIssue.lastActivityAt = new Date("2026-03-11T02:00:00.000Z");
olderIssue.lastExternalCommentAt = new Date("2026-03-11T02:00:00.000Z");
const approval = makeApprovalWithTimestamps(
"approval-between",
@ -386,21 +385,19 @@ describe("inbox helpers", () => {
]);
});
it("prefers canonical lastActivityAt over comment-only timestamps", () => {
const activityIssue = makeIssue("1", true);
activityIssue.lastExternalCommentAt = new Date("2026-03-11T01:00:00.000Z");
activityIssue.lastActivityAt = new Date("2026-03-11T05:00:00.000Z");
it("sorts touched issues by latest external comment timestamp", () => {
const newerIssue = makeIssue("1", true);
newerIssue.lastExternalCommentAt = new Date("2026-03-11T05:00:00.000Z");
const commentIssue = makeIssue("2", true);
commentIssue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z");
commentIssue.lastActivityAt = new Date("2026-03-11T04:00:00.000Z");
const olderIssue = makeIssue("2", true);
olderIssue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z");
expect(getRecentTouchedIssues([commentIssue, activityIssue]).map((issue) => issue.id)).toEqual(["1", "2"]);
expect(getRecentTouchedIssues([olderIssue, newerIssue]).map((issue) => issue.id)).toEqual(["1", "2"]);
});
it("mixes join requests into the inbox feed by most recent activity", () => {
const issue = makeIssue("1", true);
issue.lastActivityAt = new Date("2026-03-11T04:00:00.000Z");
issue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z");
const joinRequest = makeJoinRequest("join-1");
joinRequest.createdAt = new Date("2026-03-11T03:00:00.000Z");
@ -485,7 +482,7 @@ describe("inbox helpers", () => {
it("limits recent touched issues before unread badge counting", () => {
const issues = Array.from({ length: RECENT_ISSUES_LIMIT + 5 }, (_, index) => {
const issue = makeIssue(String(index + 1), index < 3);
issue.lastActivityAt = new Date(Date.UTC(2026, 2, 31, 0, 0, 0, 0) - index * 60_000);
issue.lastExternalCommentAt = new Date(Date.UTC(2026, 2, 31, 0, 0, 0, 0) - index * 60_000);
return issue;
});

View file

@ -217,9 +217,6 @@ export function normalizeTimestamp(value: string | Date | null | undefined): num
}
export function issueLastActivityTimestamp(issue: Issue): number {
const lastActivityAt = normalizeTimestamp(issue.lastActivityAt);
if (lastActivityAt > 0) return lastActivityAt;
const lastExternalCommentAt = normalizeTimestamp(issue.lastExternalCommentAt);
if (lastExternalCommentAt > 0) return lastExternalCommentAt;

View file

@ -56,7 +56,6 @@ function createIssue(overrides: Partial<Issue> = {}): Issue {
labelIds: [],
myLastTouchAt: null,
lastExternalCommentAt: null,
lastActivityAt: new Date("2026-03-11T00:00:00.000Z"),
isUnreadForMe: false,
...overrides,
};

View file

@ -15,6 +15,7 @@ import { instanceSettingsApi } from "../api/instanceSettings";
import { projectsApi } from "../api/projects";
import { useCompany } from "../context/CompanyContext";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { useGeneralSettings } from "../context/GeneralSettingsContext";
import { queryKeys } from "../lib/queryKeys";
import {
armIssueDetailInboxQuickArchive,
@ -213,7 +214,7 @@ export function InboxIssueMetaLeading({
}
function issueActivityText(issue: Issue): string {
return `Updated ${timeAgo(issue.lastActivityAt ?? issue.lastExternalCommentAt ?? issue.updatedAt)}`;
return `Updated ${timeAgo(issue.lastExternalCommentAt ?? issue.updatedAt)}`;
}
function issueTrailingGridTemplate(columns: InboxIssueColumn[]): string {
@ -245,7 +246,7 @@ export function InboxIssueTrailingColumns({
assigneeName: string | null;
currentUserId: string | null;
}) {
const activityText = timeAgo(issue.lastActivityAt ?? issue.lastExternalCommentAt ?? issue.updatedAt);
const activityText = timeAgo(issue.lastExternalCommentAt ?? issue.updatedAt);
const userLabel = formatAssigneeUserLabel(issue.assigneeUserId, currentUserId) ?? "User";
return (
@ -791,10 +792,7 @@ export function Inbox() {
const location = useLocation();
const queryClient = useQueryClient();
const [actionError, setActionError] = useState<string | null>(null);
const keyboardShortcutsEnabled = useQuery({
queryKey: queryKeys.instance.generalSettings,
queryFn: () => instanceSettingsApi.getGeneral(),
}).data?.keyboardShortcuts === true;
const { keyboardShortcutsEnabled } = useGeneralSettings();
const { data: experimentalSettings } = useQuery({
queryKey: queryKeys.instance.experimentalSettings,
queryFn: () => instanceSettingsApi.getExperimental(),
@ -1778,7 +1776,7 @@ export function Inbox() {
<div key="today-divider" className="flex items-center gap-3 px-4 my-2">
<div className="flex-1 border-t border-zinc-600" />
<span className="shrink-0 text-[11px] font-medium uppercase tracking-wider text-zinc-500">
Today
Earlier
</span>
</div>,
);