# Files changed · PR \#22687 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22687/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[refactor\(web\): the command palette's thread items come from their own hook](https://git.cafe/pingdotgg/t3code/pull/22687.md)

Version: 5

Head commit: 0dd1a0aff8312a8fa4bf0467e2bb2eae5aa9b3e3

Base commit: 159fdb5320101963dad5015646ea3eb98b5b82c2

Comparison base: 159fdb5320101963dad5015646ea3eb98b5b82c2

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/web/src/components/CommandPalette.tsx

Status: modified; +14; −208

```diff
@@ -24,7 +24,6 @@
 } from "@t3tools/client-runtime/operations/projects";
 import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
 import { connectionStatusText } from "@t3tools/client-runtime/connection";
-import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search";
 import { resolveThreadReferenceCopyTarget } from "@t3tools/shared/threadReference";
 import {
   canPreloadBrowsePath,
@@ -42,11 +41,9 @@
   AuthSourceControlWriteScope,
   AuthFilesystemReadScope,
   type DesktopWslState,
-  type ClientSettings,
   type EnvironmentId,
   type EnvironmentMachineKind,
   type FilesystemBrowseResult,
-  type ProjectId,
   type SourceControlDiscoveryResult,
   SourceControlProviderKind,
   type SourceControlRepositoryInfo,
@@ -121,7 +118,6 @@
   usePrimaryEnvironmentId,
 } from "../state/environments";
 import { useProjects, useServerConfigs, useThreadShells, waitForProject } from "../state/entities";
-import { useThreadSearch } from "../state/queries";
 import { resolveThreadActionProjectRef, startNewThreadFromContext } from "../lib/chatThreadActions";
 import {
   appendBrowsePathSegment,
@@ -166,7 +162,6 @@
   buildCommandPaletteProjectMetadata,
   buildProjectActionItems,
   buildRootGroups,
-  buildThreadActionItems,
   buildLinkedThreadActionItems,
   buildCommandPaletteRows,
   enumerateCommandPaletteItems,
@@ -188,10 +183,16 @@
   reduceCommandPaletteSearchState,
   type SearchOverlayMode,
 } from "./CommandPalette.logic";
-import { orderItemsByPreferredIds, sortLogicalProjectsForSidebar } from "./Sidebar.logic";
 import { resolveEnvironmentOptionLabel } from "./BranchToolbar.logic";
 import { CommandPaletteContent } from "./CommandPaletteContent";
 import {
+  selectCommandPaletteClientSettings,
+  useEnvironmentLookups,
+  useProjectLookups,
+  useSortedProjectGroups,
+} from "./CommandPaletteLookups";
+import { useCommandPaletteThreadItems } from "./CommandPaletteThreadItems";
+import {
   APPEARANCE_OPTIONS,
   notifyThemeSaveFailure,
   useCommandPaletteThemeItems,
@@ -210,22 +211,8 @@
 import { ProjectContentSearchDialog } from "./search/ProjectContentSearchDialog";
 import { toggleThemeEditorForTheme } from "./settings/themeEditorStore";
 import { searchSettings, SETTINGS_SECTION_LABELS } from "./settings/settingsSearch";
-import {
-  COMMAND_PALETTE_META_ICON_CLASS,
-  CommandPaletteMetaDot,
-  ThreadCommandSubtitle,
-} from "./ThreadCommandSubtitle";
-import {
-  ThreadRowLeadingStatus,
-  ThreadRowTrailingStatus,
-  ThreadSearchPullRequestNumber,
-} from "./ThreadStatusIndicators";
-import { primaryServerKeybindingsAtom, primaryServerProvidersAtom } from "../state/server";
-import {
-  applyProviderInstanceSettings,
-  deriveProviderInstanceEntries,
-  type ProviderInstanceEntry,
-} from "../providerInstances";
+import { COMMAND_PALETTE_META_ICON_CLASS, CommandPaletteMetaDot } from "./ThreadCommandSubtitle";
+import { primaryServerKeybindingsAtom } from "../state/server";
 import { resolveShortcutCommand, threadJumpIndexFromCommand } from "../keybindings";
 import { CommandDialog, CommandDialogPopup, CommandFooterAction } from "./ui/command";
 import { Button } from "./ui/button";
@@ -235,12 +222,7 @@
 import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
 import { ComposerHandleContext, useComposerHandleContext } from "../composerHandleContext";
 import type { ChatComposerHandle } from "./chat/ChatComposer";
-import { getProjectOrderKey } from "../logicalProject";
-import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore";
-import {
-  buildSidebarProjectPickerEntries,
-  buildSidebarProjectSnapshots,
-} from "../sidebarProjectGrouping";
+import { buildSidebarProjectPickerEntries } from "../sidebarProjectGrouping";
 import type { Project } from "../types";
 import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons";
 import { readPullRequestListPreferences } from "~/components/pullRequest/pullRequestListPreferences";
@@ -350,99 +332,6 @@
     : null;
 }
 
-// Each helper reads its own source hook so the compiler caches the derived value on that
-// source. Taking the source as an argument instead leaves the helper hook-free, and the
-// compiler then treats it as a plain function and caches nothing.
-function useThreadSearchWithContentMatches(
-  environmentIds: ReadonlyArray<EnvironmentId>,
-  query: string,
-) {
-  const threadSearch = useThreadSearch(environmentIds, query);
-  const contentMatchByKey = new Map(
-    threadSearch.matches.flatMap((match) =>
-      match.source === "user" || match.source === "assistant"
-        ? [[threadSearchMatchKey(match), match] as const]
-        : [],
-    ),
-  );
-  return { threadSearch, contentMatchByKey };
-}
-
-function useEnvironmentLookups() {
-  const { environments } = useEnvironments();
-  const environmentLabelById = new Map(
-    environments.map((environment) => [environment.environmentId, environment.label] as const),
-  );
-  const projectEnvironmentLocationById = new Map(
-    environments.map((environment) => {
-      const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget";
-      const isLocal = isPrimary || isDesktopLocalConnectionTarget(environment.entry.target);
-      return [
-        environment.environmentId,
-        {
-          kind: isLocal ? "local" : "remote",
-          label: isPrimary ? "Local" : isLocal ? `${environment.label} (Local)` : environment.label,
-          machine: resolveEnvironmentMachineKind(environment.serverConfig),
-        },
-      ] as const;
-    }),
-  );
-  return { environmentLabelById, projectEnvironmentLocationById };
-}
-
-function useProjectLookups() {
-  const projects = useProjects();
-  const projectCwdById = new Map<ProjectId, string>(
-    projects.map((project) => [project.id, project.workspaceRoot]),
-  );
-  const projectByKey = new Map(
-    projects.map((project) => [`${project.environmentId}:${project.id}`, project]),
-  );
-  const projectTitleById = new Map<ProjectId, string>(
-    projects.map((project) => [project.id, project.title]),
-  );
-  return { projectCwdById, projectByKey, projectTitleById };
-}
-
-const selectCommandPaletteClientSettings = (settings: ClientSettings) => ({
-  sidebarProjectGroupingMode: settings.sidebarProjectGroupingMode,
-  sidebarProjectGroupingOverrides: settings.sidebarProjectGroupingOverrides,
-  sidebarProjectSortOrder: settings.sidebarProjectSortOrder,
-  sidebarThreadSortOrder: settings.sidebarThreadSortOrder,
-});
-
-function useSortedProjectGroups(environmentLabelById: ReadonlyMap<EnvironmentId, string>) {
-  const projects = useProjects();
-  const threads = useThreadShells();
-  const projectOrder = useUiStateStore((store) => store.projectOrder);
-  const clientSettings = useClientSettings(selectCommandPaletteClientSettings);
-  const primaryEnvironmentId = usePrimaryEnvironmentId();
-  const projectGroupingSettings = {
-    sidebarProjectGroupingMode: clientSettings.sidebarProjectGroupingMode,
-    sidebarProjectGroupingOverrides: clientSettings.sidebarProjectGroupingOverrides,
-  };
-  const orderedProjects = orderItemsByPreferredIds({
-    items: projects,
-    preferredIds: projectOrder,
-    getId: getProjectOrderKey,
-    getPreferenceIds: (project) => [
-      getProjectOrderKey(project),
-      legacyProjectCwdPreferenceKey(project.workspaceRoot),
-    ],
-  });
-  const unsortedProjectGroups = buildSidebarProjectSnapshots({
-    projects: clientSettings.sidebarProjectSortOrder === "manual" ? orderedProjects : projects,
-    settings: projectGroupingSettings,
-    primaryEnvironmentId,
-    resolveEnvironmentLabel: (environmentId) => environmentLabelById.get(environmentId) ?? null,
-  });
-  return sortLogicalProjectsForSidebar(
-    unsortedProjectGroups,
-    threads,
-    clientSettings.sidebarProjectSortOrder,
-  );
-}
-
 export function CommandPalette({ children }: { children: ReactNode }) {
   const navigate = useNavigate();
   const [state, dispatch] = useReducer(reduceCommandPaletteUiState, {
@@ -778,31 +667,10 @@
   };
   const threads = useThreadShells();
   const keybindings = useAtomValue(primaryServerKeybindingsAtom);
-  const { changeThemeItem, changeAppearanceItem, themeEditorItem } = useCommandPaletteThemeItems();
-  const providers = useAtomValue(primaryServerProvidersAtom);
   const providersByEnvironment = useEnvironmentProviders();
-  const providerEntryByEnvironmentAndInstanceId = new Map<string, ProviderInstanceEntry>(
-    environments.flatMap((environment) => {
-      const serverConfig = environment.serverConfig;
-      const derived = deriveProviderInstanceEntries(
-        providersByEnvironment.get(environment.environmentId) ??
-          (environment.environmentId === primaryEnvironmentId ? providers : []),
-      );
-      const entries = serverConfig
-        ? applyProviderInstanceSettings(derived, serverConfig.settings, null)
-        : derived;
-      return entries.map(
-        (entry) => [`${environment.environmentId}:${entry.instanceId}`, entry] as const,
-      );
-    }),
-  );
+  const { changeThemeItem, changeAppearanceItem, themeEditorItem } = useCommandPaletteThemeItems();
   const currentView = viewStack.at(-1)?.view ?? null;
-  const environmentIds = environments
-    .filter((environment) => environment.connection.phase === "connected")
-    .map((environment) => environment.environmentId);
   const threadSearchQuery = currentView === null && !isActionsOnly ? deferredQuery : "";
-  const { threadSearch, contentMatchByKey: threadContentMatchByKey } =
-    useThreadSearchWithContentMatches(environmentIds, threadSearchQuery);
   const [browseGeneration, setBrowseGeneration] = useState(0);
   const [browseNavigation] = useState(createBrowseNavigationCoordinator);
   const [addProjectEnvironmentId, setAddProjectEnvironmentId] = useState<EnvironmentId | null>(
@@ -1183,70 +1051,8 @@
     ];
   })();
 
-  const allThreadItems = buildThreadActionItems({
-    threads,
-    ...(activeThreadId ? { activeThreadId } : {}),
-    projectTitleById,
-    sortOrder: clientSettings.sidebarThreadSortOrder,
-    icon: <MessageSquareIcon className={ITEM_ICON_CLASS} />,
-    renderLeadingContent: (thread) => <ThreadRowLeadingStatus thread={thread} />,
-    renderTrailingContent: (thread) => (
+  const { threadItems: allThreadItems, isSearchPending: isThreadSearchPending } =
+    useCommandPaletteThreadItems({ activeThreadId, threadSearchQuery });
-      <>
-        <ThreadSearchPullRequestNumber
-          thread={thread}
-          query={threadSearchQuery}
-          settled={thread.settledOverride === "settled"}
-        />
-        <ThreadRowTrailingStatus thread={thread} />
-      </>
-    ),
-    renderDescription: (thread, { projectTitle }) => {
-      const modelInstanceId =
-        thread.runtime?.providerInstanceId ?? thread.modelSelection.instanceId;
-      const providerEntry =
-        providerEntryByEnvironmentAndInstanceId.get(`${thread.environmentId}:${modelInstanceId}`) ??
-        null;
-      return (
-        <ThreadCommandSubtitle
-          project={projectByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null}
-          projectTitle={projectTitle ?? null}
-          environmentLabel={
-            projectEnvironmentLocationById.get(thread.environmentId)?.label ?? "Remote"
-          }
-          branch={thread.branch}
-          worktreePath={thread.worktreePath}
-          isCurrent={thread.id === activeThreadId}
-          driverKind={providerEntry?.driverKind ?? null}
-          providerDisplayName={
-            thread.runtime?.providerName ?? providerEntry?.displayName ?? modelInstanceId
-          }
-          acpRegistryAgentId={providerEntry?.acpRegistryAgentId}
-          acpRegistryIconUrl={providerEntry?.acpRegistryIconUrl}
-        />
-      );
-    },
-    getContentMatch: (thread) => {
-      const match = threadContentMatchByKey.get(
-        threadSearchMatchKey({
-          environmentId: thread.environmentId,
-          threadId: thread.id,
-        }),
-      );
-      return match && (match.source === "user" || match.source === "assistant")
-        ? {
-            source: match.source,
-            snippet: match.snippet,
-            query: threadSearch.query,
-          }
-        : undefined;
-    },
-    runThread: async (thread) => {
-      await navigate({
-        to: "/$environmentId/$threadId",
-        params: buildThreadRouteParams(scopeThreadRef(thread.environmentId, thread.id)),
-      });
-    },
-  });
   const recentThreadItems = allThreadItems.slice(0, RECENT_THREAD_LIMIT);
 
   // Stable so the item builders that capture it during render never see the
@@ -3208,7 +3014,7 @@
                 ? {
                     emptyStateMessage: "Press Enter to create this folder and add it as a project.",
                   }
+                : isThreadSearchPending
-                : threadSearch.isPending
                   ? { emptyStateMessage: "Searching thread messages…" }
                   : {})}
       />
```

## apps/web/src/components/CommandPaletteLookups.ts

Status: added; +93; −0

```diff
@@ -1,0 +1,93 @@
+import {
+  type ClientSettings,
+  type EnvironmentId,
+  type ProjectId,
+  resolveEnvironmentMachineKind,
+} from "@t3tools/contracts";
+
+import { isDesktopLocalConnectionTarget } from "../connection/desktopLocal";
+import { useClientSettings } from "../hooks/useSettings";
+import { getProjectOrderKey } from "../logicalProject";
+import { useProjects, useThreadShells } from "../state/entities";
+import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments";
+import { buildSidebarProjectSnapshots } from "../sidebarProjectGrouping";
+import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore";
+import { orderItemsByPreferredIds, sortLogicalProjectsForSidebar } from "./Sidebar.logic";
+
+// Each helper reads its own source hook so the compiler caches the derived value on that
+// source. Taking the source as an argument instead leaves the helper hook-free, and the
+// compiler then treats it as a plain function and caches nothing.
+export function useEnvironmentLookups() {
+  const { environments } = useEnvironments();
+  const environmentLabelById = new Map(
+    environments.map((environment) => [environment.environmentId, environment.label] as const),
+  );
+  const projectEnvironmentLocationById = new Map(
+    environments.map((environment) => {
+      const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget";
+      const isLocal = isPrimary || isDesktopLocalConnectionTarget(environment.entry.target);
+      return [
+        environment.environmentId,
+        {
+          kind: isLocal ? "local" : "remote",
+          label: isPrimary ? "Local" : isLocal ? `${environment.label} (Local)` : environment.label,
+          machine: resolveEnvironmentMachineKind(environment.serverConfig),
+        },
+      ] as const;
+    }),
+  );
+  return { environmentLabelById, projectEnvironmentLocationById };
+}
+
+export function useProjectLookups() {
+  const projects = useProjects();
+  const projectCwdById = new Map<ProjectId, string>(
+    projects.map((project) => [project.id, project.workspaceRoot]),
+  );
+  const projectByKey = new Map(
+    projects.map((project) => [`${project.environmentId}:${project.id}`, project]),
+  );
+  const projectTitleById = new Map<ProjectId, string>(
+    projects.map((project) => [project.id, project.title]),
+  );
+  return { projectCwdById, projectByKey, projectTitleById };
+}
+
+export const selectCommandPaletteClientSettings = (settings: ClientSettings) => ({
+  sidebarProjectGroupingMode: settings.sidebarProjectGroupingMode,
+  sidebarProjectGroupingOverrides: settings.sidebarProjectGroupingOverrides,
+  sidebarProjectSortOrder: settings.sidebarProjectSortOrder,
+  sidebarThreadSortOrder: settings.sidebarThreadSortOrder,
+});
+
+export function useSortedProjectGroups(environmentLabelById: ReadonlyMap<EnvironmentId, string>) {
+  const projects = useProjects();
+  const threads = useThreadShells();
+  const projectOrder = useUiStateStore((store) => store.projectOrder);
+  const clientSettings = useClientSettings(selectCommandPaletteClientSettings);
+  const primaryEnvironmentId = usePrimaryEnvironmentId();
+  const projectGroupingSettings = {
+    sidebarProjectGroupingMode: clientSettings.sidebarProjectGroupingMode,
+    sidebarProjectGroupingOverrides: clientSettings.sidebarProjectGroupingOverrides,
+  };
+  const orderedProjects = orderItemsByPreferredIds({
+    items: projects,
+    preferredIds: projectOrder,
+    getId: getProjectOrderKey,
+    getPreferenceIds: (project) => [
+      getProjectOrderKey(project),
+      legacyProjectCwdPreferenceKey(project.workspaceRoot),
+    ],
+  });
+  const unsortedProjectGroups = buildSidebarProjectSnapshots({
+    projects: clientSettings.sidebarProjectSortOrder === "manual" ? orderedProjects : projects,
+    settings: projectGroupingSettings,
+    primaryEnvironmentId,
+    resolveEnvironmentLabel: (environmentId) => environmentLabelById.get(environmentId) ?? null,
+  });
+  return sortLogicalProjectsForSidebar(
+    unsortedProjectGroups,
+    threads,
+    clientSettings.sidebarProjectSortOrder,
+  );
+}
```

## apps/web/src/components/CommandPaletteThreadItems.tsx

Status: added; +150; −0

```diff
@@ -1,0 +1,150 @@
+import { scopeThreadRef } from "@t3tools/client-runtime/environment";
+import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search";
+import type { EnvironmentId, ThreadId } from "@t3tools/contracts";
+import { useAtomValue } from "@effect/atom-react";
+import { useNavigate } from "@tanstack/react-router";
+import { MessageSquareIcon } from "lucide-react";
+
+import { useClientSettings } from "../hooks/useSettings";
+import {
+  applyProviderInstanceSettings,
+  deriveProviderInstanceEntries,
+  type ProviderInstanceEntry,
+} from "../providerInstances";
+import { useThreadShells } from "../state/entities";
+import {
+  useEnvironmentProviders,
+  useEnvironments,
+  usePrimaryEnvironmentId,
+} from "../state/environments";
+import { useThreadSearch } from "../state/queries";
+import { primaryServerProvidersAtom } from "../state/server";
+import { buildThreadRouteParams } from "../threadRoutes";
+import { buildThreadActionItems, ITEM_ICON_CLASS } from "./CommandPalette.logic";
+import { useEnvironmentLookups, useProjectLookups } from "./CommandPaletteLookups";
+import { ThreadCommandSubtitle } from "./ThreadCommandSubtitle";
+import {
+  ThreadRowLeadingStatus,
+  ThreadRowTrailingStatus,
+  ThreadSearchPullRequestNumber,
+} from "./ThreadStatusIndicators";
+
+function useProviderEntryByEnvironmentAndInstanceId() {
+  const { environments } = useEnvironments();
+  const primaryEnvironmentId = usePrimaryEnvironmentId();
+  const providers = useAtomValue(primaryServerProvidersAtom);
+  const providersByEnvironment = useEnvironmentProviders();
+  return new Map<string, ProviderInstanceEntry>(
+    environments.flatMap((environment) => {
+      const serverConfig = environment.serverConfig;
+      const derived = deriveProviderInstanceEntries(
+        providersByEnvironment.get(environment.environmentId) ??
+          (environment.environmentId === primaryEnvironmentId ? providers : []),
+      );
+      const entries = serverConfig
+        ? applyProviderInstanceSettings(derived, serverConfig.settings, null)
+        : derived;
+      return entries.map(
+        (entry) => [`${environment.environmentId}:${entry.instanceId}`, entry] as const,
+      );
+    }),
+  );
+}
+
+function useThreadSearchWithContentMatches(query: string) {
+  const { environments } = useEnvironments();
+  const environmentIds: ReadonlyArray<EnvironmentId> = environments
+    .filter((environment) => environment.connection.phase === "connected")
+    .map((environment) => environment.environmentId);
+  const threadSearch = useThreadSearch(environmentIds, query);
+  const contentMatchByKey = new Map(
+    threadSearch.matches.flatMap((match) =>
+      match.source === "user" || match.source === "assistant"
+        ? [[threadSearchMatchKey(match), match] as const]
+        : [],
+    ),
+  );
+  return { threadSearch, contentMatchByKey };
+}
+
+/** Every thread as a palette item, with message matches for a non-empty `threadSearchQuery`. */
+export function useCommandPaletteThreadItems(input: {
+  readonly activeThreadId: ThreadId | undefined;
+  readonly threadSearchQuery: string;
+}) {
+  const { activeThreadId, threadSearchQuery } = input;
+  const navigate = useNavigate();
+  const threads = useThreadShells();
+  const sidebarThreadSortOrder = useClientSettings((settings) => settings.sidebarThreadSortOrder);
+  const { projectEnvironmentLocationById } = useEnvironmentLookups();
+  const providerEntryByEnvironmentAndInstanceId = useProviderEntryByEnvironmentAndInstanceId();
+  const { threadSearch, contentMatchByKey } = useThreadSearchWithContentMatches(threadSearchQuery);
+  const { projectByKey, projectTitleById } = useProjectLookups();
+
+  const threadItems = buildThreadActionItems({
+    threads,
+    ...(activeThreadId ? { activeThreadId } : {}),
+    projectTitleById,
+    sortOrder: sidebarThreadSortOrder,
+    icon: <MessageSquareIcon className={ITEM_ICON_CLASS} />,
+    renderLeadingContent: (thread) => <ThreadRowLeadingStatus thread={thread} />,
+    renderTrailingContent: (thread) => (
+      <>
+        <ThreadSearchPullRequestNumber
+          thread={thread}
+          query={threadSearchQuery}
+          settled={thread.settledOverride === "settled"}
+        />
+        <ThreadRowTrailingStatus thread={thread} />
+      </>
+    ),
+    renderDescription: (thread, { projectTitle }) => {
+      const modelInstanceId =
+        thread.runtime?.providerInstanceId ?? thread.modelSelection.instanceId;
+      const providerEntry =
+        providerEntryByEnvironmentAndInstanceId.get(`${thread.environmentId}:${modelInstanceId}`) ??
+        null;
+      return (
+        <ThreadCommandSubtitle
+          project={projectByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null}
+          projectTitle={projectTitle ?? null}
+          environmentLabel={
+            projectEnvironmentLocationById.get(thread.environmentId)?.label ?? "Remote"
+          }
+          branch={thread.branch}
+          worktreePath={thread.worktreePath}
+          isCurrent={thread.id === activeThreadId}
+          driverKind={providerEntry?.driverKind ?? null}
+          providerDisplayName={
+            thread.runtime?.providerName ?? providerEntry?.displayName ?? modelInstanceId
+          }
+          acpRegistryAgentId={providerEntry?.acpRegistryAgentId}
+          acpRegistryIconUrl={providerEntry?.acpRegistryIconUrl}
+        />
+      );
+    },
+    getContentMatch: (thread) => {
+      const match = contentMatchByKey.get(
+        threadSearchMatchKey({
+          environmentId: thread.environmentId,
+          threadId: thread.id,
+        }),
+      );
+      return match && (match.source === "user" || match.source === "assistant")
+        ? {
+            source: match.source,
+            snippet: match.snippet,
+            query: threadSearch.query,
+          }
+        : undefined;
+    },
+    runThread: async (thread) => {
+      await navigate({
+        to: "/$environmentId/$threadId",
+        params: buildThreadRouteParams(scopeThreadRef(thread.environmentId, thread.id)),
+      });
+    },
+  });
+
+  return { threadItems, isSearchPending: threadSearch.isPending };
+}
```
