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
30 changes: 16 additions & 14 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,30 @@ export function HomeRouteScreen() {
const { layout } = useAdaptiveWorkspaceLayout();
const projects = useProjects();
const threads = useThreadShells();
const { state: catalogState } = useWorkspaceState();
const { environments: workspaceEnvironments, state: catalogState } = useWorkspaceState();
const { savedConnectionsById } = useSavedRemoteConnections();
const navigation = useNavigation();
const [searchQuery, setSearchQuery] = useState("");
const { archiveThread, confirmDeleteThread, settleThread, unsettleThread } =
useThreadListActions();
const pendingTasks = usePendingNewTasks();
const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions();
const environments = useMemo(
() =>
Arr.sort(
Object.values(savedConnectionsById).map((connection) => ({
environmentId: connection.environmentId,
label: connection.environmentLabel,
})),
Order.mapInput(
Order.String,
(environment: { readonly label: string }) => environment.label,
),
const environments = useMemo(() => {
const connectionStateByEnvironmentId = new Map(
workspaceEnvironments.map(
(environment) => [environment.environmentId, environment.connectionState] as const,
),
[savedConnectionsById],
);
);
return Arr.sort(
Object.values(savedConnectionsById).map((connection) => ({
environmentId: connection.environmentId,
label: connection.environmentLabel,
connectionState:
connectionStateByEnvironmentId.get(connection.environmentId) ?? "available",
})),
Order.mapInput(Order.String, (environment: { readonly label: string }) => environment.label),
);
}, [savedConnectionsById, workspaceEnvironments]);
const availableEnvironmentIds = useMemo(
() => new Set(environments.map((environment) => environment.environmentId)),
[environments],
Expand Down
124 changes: 97 additions & 27 deletions apps/mobile/src/features/home/HomeScreen.tsx
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
type EnvironmentProject,
type EnvironmentThreadShell,
} from "@t3tools/client-runtime/state/shell";
import {
threadSearchMatchKey,
type EnvironmentThreadSearchMatch,
} from "@t3tools/client-runtime/state/thread-search";
import type {
EnvironmentId,
SidebarProjectGroupingMode,
Expand All @@ -22,11 +26,12 @@ import { useThemeColor } from "../../lib/useThemeColor";

import { AppText as Text } from "../../components/AppText";
import { EmptyState } from "../../components/EmptyState";
import type { WorkspaceState } from "../../state/workspaceModel";
import type { WorkspaceEnvironment, WorkspaceState } from "../../state/workspaceModel";
import type { SavedRemoteConnection } from "../../lib/connection";
import { scopedProjectKey } from "../../lib/scopedEntities";
import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
import { useThreadSearch } from "../../state/queries";
import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
import { environmentServerConfigsAtom } from "../../state/server";
import type { PendingNewTask } from "../../state/use-pending-new-tasks";
Expand Down Expand Up @@ -72,7 +77,9 @@ interface HomeScreenProps {
readonly pendingTasks: ReadonlyArray<PendingNewTask>;
readonly catalogState: WorkspaceState;
readonly savedConnectionsById: Readonly<Record<string, SavedRemoteConnection>>;
readonly environments: ReadonlyArray<HomeListFilterMenuEnvironment>;
readonly environments: ReadonlyArray<
HomeListFilterMenuEnvironment & Pick<WorkspaceEnvironment, "connectionState">
>;
readonly searchQuery: string;
readonly selectedEnvironmentId: EnvironmentId | null;
readonly selectedProjectKey: string | null;
Expand Down Expand Up @@ -189,6 +196,35 @@ export function HomeScreen(props: HomeScreenProps) {
const listRef = useRef<LegendListRef | null>(null);
const insets = useSafeAreaInsets();
const accentColor = useThemeColor("--color-icon-muted");
const searchEnvironmentIds = useMemo(
() =>
props.selectedEnvironmentId === null
? props.environments
.filter((environment) => environment.connectionState === "connected")
.map((environment) => environment.environmentId)
: props.environments.some(
(environment) =>
environment.environmentId === props.selectedEnvironmentId &&
environment.connectionState === "connected",
)
? [props.selectedEnvironmentId]
: [],
[props.environments, props.selectedEnvironmentId],
);
const threadSearch = useThreadSearch(searchEnvironmentIds, props.searchQuery);
const threadSearchMatchByKey = useMemo(() => {
const matches = new Map<string, EnvironmentThreadSearchMatch>();
for (const match of threadSearch.matches) {
if (match.source === "user" || match.source === "assistant") {
matches.set(threadSearchMatchKey(match), match);
}
}
return matches;
}, [threadSearch.matches]);
const matchedThreadKeys = useMemo(
() => new Set(threadSearch.matches.map(threadSearchMatchKey)),
[threadSearch.matches],
);
const effectiveGroupDisplayStates = useMemo(() => {
const next = new Map(groupDisplayStates);
if (!AsyncResult.isSuccess(preferencesResult)) {
Expand Down Expand Up @@ -318,6 +354,7 @@ export function HomeScreen(props: HomeScreenProps) {
pendingTasks: scopedPendingTasks,
environmentId: props.selectedEnvironmentId,
searchQuery: props.searchQuery,
matchedThreadKeys,
projectSortOrder: props.projectSortOrder,
threadSortOrder: props.threadSortOrder,
projectGroupingMode: props.projectGroupingMode,
Expand All @@ -328,6 +365,7 @@ export function HomeScreen(props: HomeScreenProps) {
props.searchQuery,
props.selectedEnvironmentId,
props.threadSortOrder,
matchedThreadKeys,
scopedPendingTasks,
scopedProjects,
scopedThreads,
Expand Down Expand Up @@ -516,6 +554,7 @@ export function HomeScreen(props: HomeScreenProps) {
environmentId: props.selectedEnvironmentId,
projectRefs: v2ScopedProjectGroup === null ? null : v2ScopedProjectGroup.projectRefs,
searchQuery: props.searchQuery,
matchedThreadKeys,
changeRequestStateByKey,
settlementEnvironmentIds,
snoozeEnvironmentIds,
Expand All @@ -533,6 +572,7 @@ export function HomeScreen(props: HomeScreenProps) {
props.searchQuery,
props.selectedEnvironmentId,
props.threads,
matchedThreadKeys,
threadListV2Enabled,
v2ScopedProjectGroup,
]);
Expand Down Expand Up @@ -629,6 +669,13 @@ export function HomeScreen(props: HomeScreenProps) {
? (props.savedConnectionsById[thread.environmentId]?.environmentLabel ?? null)
: null
}
searchMatch={threadSearchMatchByKey.get(
threadSearchMatchKey({
environmentId: thread.environmentId,
threadId: thread.id,
}),
)}
searchQuery={props.searchQuery}
onSelectThread={props.onSelectThread}
onDeleteThread={handleDeleteThread}
onArchiveThread={props.onArchiveThread}
Expand Down Expand Up @@ -660,7 +707,9 @@ export function HomeScreen(props: HomeScreenProps) {
props.savedConnectionsById,
serverConfigs,
settlementEnvironmentIds,
threadSearchMatchByKey,
v2ProjectTitleByProjectKey,
props.searchQuery,
],
);
const v2KeyExtractor = useCallback((item: ThreadListV2ListItem) => item.key, []);
Expand All @@ -675,19 +724,28 @@ export function HomeScreen(props: HomeScreenProps) {
projectTitleByProjectKey: v2ProjectTitleByProjectKey,
serverConfigs,
savedConnectionsById: props.savedConnectionsById,
searchQuery: props.searchQuery,
threadSearchMatchByKey,
}),
[
projectByKey,
projectCwdByKey,
props.searchQuery,
props.savedConnectionsById,
serverConfigs,
threadSearchMatchByKey,
Comment thread
t3dotgg marked this conversation as resolved.
v2ProjectTitleByProjectKey,
],
);

const extraData = useMemo(
() => ({ savedConnectionsById: props.savedConnectionsById, projectCwdByKey }),
[props.savedConnectionsById, projectCwdByKey],
() => ({
projectCwdByKey,
savedConnectionsById: props.savedConnectionsById,
searchQuery: props.searchQuery,
threadSearchMatchByKey,
}),
[projectCwdByKey, props.savedConnectionsById, props.searchQuery, threadSearchMatchByKey],
);

const renderItem = useCallback(
Expand Down Expand Up @@ -740,6 +798,13 @@ export function HomeScreen(props: HomeScreenProps) {
null
}
isLast={item.isLast}
searchMatch={threadSearchMatchByKey.get(
threadSearchMatchKey({
environmentId: thread.environmentId,
threadId: thread.id,
}),
)}
searchQuery={props.searchQuery}
onArchiveThread={props.onArchiveThread}
onDeleteThread={props.onDeleteThread}
onSelectThread={props.onSelectThread}
Expand Down Expand Up @@ -770,7 +835,9 @@ export function HomeScreen(props: HomeScreenProps) {
props.onNewThreadInProject,
props.onSelectPendingTask,
props.onSelectThread,
props.searchQuery,
props.savedConnectionsById,
threadSearchMatchByKey,
Comment thread
t3dotgg marked this conversation as resolved.
updateGroupDisplay,
],
);
Expand Down Expand Up @@ -863,7 +930,7 @@ export function HomeScreen(props: HomeScreenProps) {
const v2ListHeader = listHeader;

const listEmpty = !hasResults ? (
hasSearchQuery ? (
hasSearchQuery && threadSearch.isPending ? null : hasSearchQuery ? (
<EmptyState title="No results" detail={`No threads matching "${props.searchQuery}".`} />
) : selectedProjectScope !== null ? (
<EmptyState
Expand All @@ -886,31 +953,34 @@ export function HomeScreen(props: HomeScreenProps) {
// threads yet" over an inbox that is merely all-snoozed reads as data
// loss.
const v2SnoozedCount = threadListV2Layout.snoozedCount;
const v2ListEmpty = hasSearchQuery ? (
v2SnoozedCount > 0 ? (
// The snoozed threads already passed this search filter: "No
// results" would claim nothing matched when matches are merely
// parked.
const v2ListEmpty =
hasSearchQuery && threadSearch.isPending && v2SnoozedCount === 0 ? null : hasSearchQuery ? (
v2SnoozedCount > 0 ? (
// The snoozed threads already passed this search filter: "No
// results" would claim nothing matched when matches are merely
// parked.
<EmptyState
title={
v2SnoozedCount === 1 ? "1 matching thread snoozed" : `All matching threads snoozed`
}
detail={`Threads matching "${props.searchQuery}" are snoozed and return when their wake time passes.`}
/>
) : (
<EmptyState title="No results" detail={`No threads matching "${props.searchQuery}".`} />
)
) : v2SnoozedCount > 0 ? (
<EmptyState
title={v2SnoozedCount === 1 ? "1 thread snoozed" : `${v2SnoozedCount} threads snoozed`}
detail="Snoozed threads return when their wake time passes."
/>
) : v2ScopedProjectGroup !== null ? (
<EmptyState
title={v2SnoozedCount === 1 ? "1 matching thread snoozed" : `All matching threads snoozed`}
detail={`Threads matching "${props.searchQuery}" are snoozed and return when their wake time passes.`}
title={`No threads in ${v2ScopedProjectGroup.title}`}
detail="Choose another project or create a new task."
/>
) : (
<EmptyState title="No results" detail={`No threads matching "${props.searchQuery}".`} />
)
) : v2SnoozedCount > 0 ? (
<EmptyState
title={v2SnoozedCount === 1 ? "1 thread snoozed" : `${v2SnoozedCount} threads snoozed`}
detail="Snoozed threads return when their wake time passes."
/>
) : v2ScopedProjectGroup !== null ? (
<EmptyState
title={`No threads in ${v2ScopedProjectGroup.title}`}
detail="Choose another project or create a new task."
/>
) : (
listEmpty
);
listEmpty
);

if (threadListV2Enabled) {
return (
Expand Down
28 changes: 28 additions & 0 deletions apps/mobile/src/features/home/homeThreadList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
EnvironmentProject,
EnvironmentThreadShell,
} from "@t3tools/client-runtime/state/shell";
import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search";
import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

Expand Down Expand Up @@ -661,6 +662,33 @@ describe("buildHomeThreadGroups", () => {
);
});

it("includes a thread matched by message content", () => {
const environmentId = EnvironmentId.make("environment-1");
const project = makeProject({
environmentId,
id: ProjectId.make("project-1"),
title: "T3 Code",
});
const thread = makeThread({
environmentId,
id: ThreadId.make("thread-content"),
projectId: project.id,
title: "Unrelated title",
});

const groups = buildGroups([project], [thread], {
searchQuery: "relay reconnect",
matchedThreadKeys: new Set([
threadSearchMatchKey({
environmentId,
threadId: thread.id,
}),
]),
});

expect(groups[0]?.threads.map((candidate) => candidate.id)).toEqual(["thread-content"]);
});

it("targets quick new threads at the group member with the newest thread", () => {
const laptopEnv = EnvironmentId.make("environment-laptop");
const desktopEnv = EnvironmentId.make("environment-desktop");
Expand Down
13 changes: 12 additions & 1 deletion apps/mobile/src/features/home/homeThreadList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
sortThreads,
toSortableTimestamp,
} from "@t3tools/client-runtime/state/thread-sort";
import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search";
import type {
EnvironmentId,
ScopedProjectRef,
Expand Down Expand Up @@ -254,6 +255,7 @@ export function buildHomeThreadGroups(input: {
readonly pendingTasks?: ReadonlyArray<PendingNewTask>;
readonly environmentId: EnvironmentId | null;
readonly searchQuery: string;
readonly matchedThreadKeys?: ReadonlySet<string>;
readonly projectSortOrder: HomeProjectSortOrder;
readonly threadSortOrder: SidebarThreadSortOrder;
readonly projectGroupingMode: SidebarProjectGroupingMode;
Expand Down Expand Up @@ -350,7 +352,16 @@ export function buildHomeThreadGroups(input: {
group.projects.some((project) => project.title.toLocaleLowerCase().includes(query));
const matchingThreads = groupMatches
? group.threads
: group.threads.filter((thread) => thread.title.toLocaleLowerCase().includes(query));
: group.threads.filter(
(thread) =>
thread.title.toLocaleLowerCase().includes(query) ||
input.matchedThreadKeys?.has(
threadSearchMatchKey({
environmentId: thread.environmentId,
threadId: thread.id,
}),
) === true,
);
const matchingPendingTasks = groupMatches
? group.pendingTasks
: group.pendingTasks.filter((pendingTask) =>
Expand Down
Loading
Loading