Skip to content

Commit 6f69934

Browse files
Use latest user message time for thread timestamps (#1996)
1 parent f5ecca4 commit 6f69934

7 files changed

Lines changed: 165 additions & 555 deletions

File tree

apps/web/src/components/CommandPalette.logic.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,9 @@ export function buildThreadActionItems(input: {
114114
searchTerms: [thread.title, projectTitle ?? "", thread.branch ?? ""],
115115
title: thread.title,
116116
description: descriptionParts.join(" · "),
117-
timestamp: formatRelativeTimeLabel(thread.updatedAt ?? thread.createdAt),
117+
timestamp: formatRelativeTimeLabel(
118+
thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt,
119+
),
118120
icon: input.icon,
119121
run: async () => {
120122
await input.runThread(thread);

apps/web/src/components/Sidebar.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
759759
: "text-muted-foreground/40"
760760
}`}
761761
>
762-
{formatRelativeTimeLabel(thread.updatedAt ?? thread.createdAt)}
762+
{formatRelativeTimeLabel(
763+
thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt,
764+
)}
763765
</span>
764766
)}
765767
</span>
@@ -1115,6 +1117,11 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
11151117
),
11161118
[allSidebarThreads],
11171119
);
1120+
// Keep a ref so callbacks can read the latest map without appearing in
1121+
// dependency arrays (avoids invalidating every thread-row memo on each
1122+
// thread-list change).
1123+
const sidebarThreadByKeyRef = useRef(sidebarThreadByKey);
1124+
sidebarThreadByKeyRef.current = sidebarThreadByKey;
11181125
// All threads from the representative + other member environments are
11191126
// already fetched into allSidebarThreads, so we can use them directly.
11201127
const projectThreads = allSidebarThreads;
@@ -1458,7 +1465,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
14581465

14591466
if (clicked === "mark-unread") {
14601467
for (const threadKey of threadKeys) {
1461-
const thread = sidebarThreadByKey.get(threadKey);
1468+
const thread = sidebarThreadByKeyRef.current.get(threadKey);
14621469
markThreadUnread(threadKey, thread?.latestTurn?.completedAt);
14631470
}
14641471
clearSelection();
@@ -1479,7 +1486,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
14791486

14801487
const deletedThreadKeys = new Set(threadKeys);
14811488
for (const threadKey of threadKeys) {
1482-
const thread = sidebarThreadByKey.get(threadKey);
1489+
const thread = sidebarThreadByKeyRef.current.get(threadKey);
14831490
if (!thread) continue;
14841491
await deleteThread(scopeThreadRef(thread.environmentId, thread.id), {
14851492
deletedThreadKeys,
@@ -1493,7 +1500,6 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
14931500
deleteThread,
14941501
markThreadUnread,
14951502
removeFromSelection,
1496-
sidebarThreadByKey,
14971503
],
14981504
);
14991505

@@ -1622,12 +1628,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
16221628
const api = readLocalApi();
16231629
if (!api) return;
16241630
const threadKey = scopedThreadKey(threadRef);
1625-
const thread =
1626-
projectThreads.find(
1627-
(projectThread) =>
1628-
projectThread.environmentId === threadRef.environmentId &&
1629-
projectThread.id === threadRef.threadId,
1630-
) ?? null;
1631+
const thread = sidebarThreadByKeyRef.current.get(threadKey) ?? null;
16311632
if (!thread) return;
16321633
const threadWorkspacePath = thread.worktreePath ?? project.cwd ?? null;
16331634
const clicked = await api.contextMenu.show(
@@ -1689,7 +1690,6 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
16891690
deleteThread,
16901691
markThreadUnread,
16911692
project.cwd,
1692-
projectThreads,
16931693
],
16941694
);
16951695

apps/web/src/hooks/useThreadActions.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { parseScopedThreadKey, scopeProjectRef, scopeThreadRef } from "@t3tools/
22
import { type ScopedThreadRef, ThreadId } from "@t3tools/contracts";
33
import { useQueryClient } from "@tanstack/react-query";
44
import { useRouter } from "@tanstack/react-router";
5-
import { useCallback } from "react";
5+
import { useCallback, useRef } from "react";
66

77
import { getFallbackThreadIdAfterDelete } from "../components/Sidebar.logic";
88
import { useComposerDraftStore } from "../composerDraftStore";
@@ -33,6 +33,12 @@ export function useThreadActions() {
3333
const clearTerminalState = useTerminalStateStore((state) => state.clearTerminalState);
3434
const router = useRouter();
3535
const { handleNewThread } = useNewThreadHandler();
36+
// Keep a ref so archiveThread can call handleNewThread without appearing in
37+
// its dependency array — handleNewThread is inherently unstable (depends on
38+
// the projects list) and would otherwise cascade new references into every
39+
// sidebar row via archiveThread → attemptArchiveThread.
40+
const handleNewThreadRef = useRef(handleNewThread);
41+
handleNewThreadRef.current = handleNewThread;
3642
const queryClient = useQueryClient();
3743

3844
const resolveThreadTarget = useCallback((target: ScopedThreadRef) => {
@@ -73,10 +79,10 @@ export function useThreadActions() {
7379
currentRouteThreadRef?.threadId === threadRef.threadId &&
7480
currentRouteThreadRef.environmentId === threadRef.environmentId
7581
) {
76-
await handleNewThread(scopeProjectRef(thread.environmentId, thread.projectId));
82+
await handleNewThreadRef.current(scopeProjectRef(thread.environmentId, thread.projectId));
7783
}
7884
},
79-
[getCurrentRouteThreadRef, handleNewThread, resolveThreadTarget],
85+
[getCurrentRouteThreadRef, resolveThreadTarget],
8086
);
8187

8288
const unarchiveThread = useCallback(async (target: ScopedThreadRef) => {

0 commit comments

Comments
 (0)