Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/web/src/components/CommandPalette.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ export function buildThreadActionItems(input: {
searchTerms: [thread.title, projectTitle ?? "", thread.branch ?? ""],
title: thread.title,
description: descriptionParts.join(" · "),
timestamp: formatRelativeTimeLabel(thread.updatedAt ?? thread.createdAt),
timestamp: formatRelativeTimeLabel(
thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt,
),
icon: input.icon,
run: async () => {
await input.runThread(thread);
Expand Down
22 changes: 11 additions & 11 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
: "text-muted-foreground/40"
}`}
>
{formatRelativeTimeLabel(thread.updatedAt ?? thread.createdAt)}
{formatRelativeTimeLabel(
thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt,
)}
</span>
)}
</span>
Expand Down Expand Up @@ -1101,6 +1103,11 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
),
[allSidebarThreads],
);
// Keep a ref so callbacks can read the latest map without appearing in
// dependency arrays (avoids invalidating every thread-row memo on each
// thread-list change).
const sidebarThreadByKeyRef = useRef(sidebarThreadByKey);
sidebarThreadByKeyRef.current = sidebarThreadByKey;
// All threads from the representative + other member environments are
// already fetched into allSidebarThreads, so we can use them directly.
const projectThreads = allSidebarThreads;
Expand Down Expand Up @@ -1444,7 +1451,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec

if (clicked === "mark-unread") {
for (const threadKey of threadKeys) {
const thread = sidebarThreadByKey.get(threadKey);
const thread = sidebarThreadByKeyRef.current.get(threadKey);
markThreadUnread(threadKey, thread?.latestTurn?.completedAt);
}
clearSelection();
Expand All @@ -1465,7 +1472,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec

const deletedThreadKeys = new Set(threadKeys);
for (const threadKey of threadKeys) {
const thread = sidebarThreadByKey.get(threadKey);
const thread = sidebarThreadByKeyRef.current.get(threadKey);
if (!thread) continue;
await deleteThread(scopeThreadRef(thread.environmentId, thread.id), {
deletedThreadKeys,
Expand All @@ -1479,7 +1486,6 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
deleteThread,
markThreadUnread,
removeFromSelection,
sidebarThreadByKey,
],
);

Expand Down Expand Up @@ -1608,12 +1614,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
const api = readLocalApi();
if (!api) return;
const threadKey = scopedThreadKey(threadRef);
const thread =
projectThreads.find(
(projectThread) =>
projectThread.environmentId === threadRef.environmentId &&
projectThread.id === threadRef.threadId,
) ?? null;
const thread = sidebarThreadByKeyRef.current.get(threadKey) ?? null;
if (!thread) return;
const threadWorkspacePath = thread.worktreePath ?? project.cwd ?? null;
const clicked = await api.contextMenu.show(
Expand Down Expand Up @@ -1675,7 +1676,6 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
deleteThread,
markThreadUnread,
project.cwd,
projectThreads,
],
);

Expand Down
12 changes: 9 additions & 3 deletions apps/web/src/hooks/useThreadActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { parseScopedThreadKey, scopeProjectRef, scopeThreadRef } from "@t3tools/
import { type ScopedThreadRef, ThreadId } from "@t3tools/contracts";
import { useQueryClient } from "@tanstack/react-query";
import { useRouter } from "@tanstack/react-router";
import { useCallback } from "react";
import { useCallback, useRef } from "react";

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

const resolveThreadTarget = useCallback((target: ScopedThreadRef) => {
Expand Down Expand Up @@ -73,10 +79,10 @@ export function useThreadActions() {
currentRouteThreadRef?.threadId === threadRef.threadId &&
currentRouteThreadRef.environmentId === threadRef.environmentId
) {
await handleNewThread(scopeProjectRef(thread.environmentId, thread.projectId));
await handleNewThreadRef.current(scopeProjectRef(thread.environmentId, thread.projectId));
}
},
[getCurrentRouteThreadRef, handleNewThread, resolveThreadTarget],
[getCurrentRouteThreadRef, resolveThreadTarget],
);

const unarchiveThread = useCallback(async (target: ScopedThreadRef) => {
Expand Down
Loading
Loading