Skip to content

Commit f1a68ac

Browse files
Fix Android showcase capture and rebuild v2 queued rows (#4730)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 55dd016 commit f1a68ac

8 files changed

Lines changed: 523 additions & 165 deletions

File tree

apps/mobile/src/features/home/HomeScreen.tsx

Lines changed: 124 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@ import {
3636
ThreadListRow,
3737
ThreadListShowMoreRow,
3838
} from "../threads/thread-list-items";
39-
import { ThreadListV2Row } from "../threads/thread-list-v2-items";
39+
import { ThreadListV2PendingRow, ThreadListV2Row } from "../threads/thread-list-v2-items";
4040
import {
4141
buildThreadListV2Items,
42+
buildThreadListV2ListItems,
4243
THREAD_LIST_V2_SETTLED_INITIAL_COUNT,
4344
THREAD_LIST_V2_SETTLED_PAGE_COUNT,
44-
type ThreadListV2Item,
45+
type ThreadListV2ListItem,
4546
} from "../threads/threadListV2";
4647
import type { HomeListFilterMenuEnvironment } from "./home-list-filter-menu";
4748
import {
@@ -549,50 +550,100 @@ export function HomeScreen(props: HomeScreenProps) {
549550
// unchanged: after a clamped fire (wake beyond the 32-bit setTimeout
550551
// range) the boundary string is identical and the chain would die.
551552
}, [nextSnoozeWakeAt, snoozeWakeTick]);
552-
const threadListV2Items = threadListV2Layout.items;
553+
// Queued tasks are not thread shells, so the v2 partition never sees them;
554+
// they are spliced in below the active block and stay visible and deletable
555+
// while their environment is offline. Same environment scope and search
556+
// filter as the list itself.
557+
const v2SearchQuery = props.searchQuery.trim().toLocaleLowerCase();
558+
const v2PendingTasks = useMemo(
559+
() =>
560+
props.pendingTasks.filter(
561+
(pendingTask) =>
562+
(props.selectedEnvironmentId === null ||
563+
pendingTask.message.environmentId === props.selectedEnvironmentId) &&
564+
(v2ScopedProjectKeys === null ||
565+
v2ScopedProjectKeys.has(
566+
scopedProjectKey(pendingTask.message.environmentId, pendingTask.creation.projectId),
567+
)) &&
568+
(v2SearchQuery.length === 0 ||
569+
pendingTask.title.toLocaleLowerCase().includes(v2SearchQuery)),
570+
),
571+
[props.pendingTasks, props.selectedEnvironmentId, v2ScopedProjectKeys, v2SearchQuery],
572+
);
573+
const threadListV2Items = useMemo(
574+
() =>
575+
buildThreadListV2ListItems({
576+
items: threadListV2Layout.items,
577+
pendingTasks: v2PendingTasks,
578+
}),
579+
[threadListV2Layout.items, v2PendingTasks],
580+
);
553581

554582
const renderV2Item = useCallback(
555-
({ item }: { readonly item: ThreadListV2Item }) => (
556-
<ThreadListV2Row
557-
thread={item.thread}
558-
variant={item.variant}
559-
showSettledDivider={item.showSettledDivider}
560-
project={
561-
projectByKey.get(scopedProjectKey(item.thread.environmentId, item.thread.projectId)) ??
562-
null
563-
}
564-
projectTitle={v2ProjectTitleByProjectKey.get(
565-
scopedProjectKey(item.thread.environmentId, item.thread.projectId),
566-
)}
567-
providerDriver={
568-
serverConfigs
569-
.get(item.thread.environmentId)
570-
?.providers.find(
571-
(provider) =>
572-
provider.instanceId ===
573-
(item.thread.session?.providerInstanceId ?? item.thread.modelSelection.instanceId),
574-
)?.driver ?? null
575-
}
576-
environmentLabel={
577-
Object.keys(props.savedConnectionsById).length > 1
578-
? (props.savedConnectionsById[item.thread.environmentId]?.environmentLabel ?? null)
579-
: null
580-
}
581-
onSelectThread={props.onSelectThread}
582-
onDeleteThread={handleDeleteThread}
583-
onArchiveThread={props.onArchiveThread}
584-
settlementSupported={settlementEnvironmentIds.has(item.thread.environmentId)}
585-
onSettleThread={handleSettleThread}
586-
onUnsettleThread={handleUnsettleThread}
587-
onChangeRequestState={handleChangeRequestState}
588-
projectCwd={
589-
projectCwdByKey.get(scopedProjectKey(item.thread.environmentId, item.thread.projectId)) ??
590-
null
591-
}
592-
onSwipeableClose={handleSwipeableClose}
593-
onSwipeableWillOpen={handleSwipeableWillOpen}
594-
/>
595-
),
583+
({ item }: { readonly item: ThreadListV2ListItem }) => {
584+
if (item.type === "v2-pending") {
585+
const pendingScopeKey = scopedProjectKey(
586+
item.pendingTask.message.environmentId,
587+
item.pendingTask.creation.projectId,
588+
);
589+
return (
590+
<ThreadListV2PendingRow
591+
pendingTask={item.pendingTask}
592+
project={projectByKey.get(pendingScopeKey) ?? null}
593+
projectTitle={v2ProjectTitleByProjectKey.get(pendingScopeKey)}
594+
environmentLabel={
595+
Object.keys(props.savedConnectionsById).length > 1
596+
? (props.savedConnectionsById[item.pendingTask.message.environmentId]
597+
?.environmentLabel ?? null)
598+
: null
599+
}
600+
showPendingDivider={item.showPendingDivider}
601+
onSelectPendingTask={props.onSelectPendingTask}
602+
onDeletePendingTask={props.onDeletePendingTask}
603+
/>
604+
);
605+
}
606+
const thread = item.item.thread;
607+
return (
608+
<ThreadListV2Row
609+
thread={thread}
610+
variant={item.item.variant}
611+
showSettledDivider={item.item.showSettledDivider}
612+
project={
613+
projectByKey.get(scopedProjectKey(thread.environmentId, thread.projectId)) ?? null
614+
}
615+
projectTitle={v2ProjectTitleByProjectKey.get(
616+
scopedProjectKey(thread.environmentId, thread.projectId),
617+
)}
618+
providerDriver={
619+
serverConfigs
620+
.get(thread.environmentId)
621+
?.providers.find(
622+
(provider) =>
623+
provider.instanceId ===
624+
(thread.session?.providerInstanceId ?? thread.modelSelection.instanceId),
625+
)?.driver ?? null
626+
}
627+
environmentLabel={
628+
Object.keys(props.savedConnectionsById).length > 1
629+
? (props.savedConnectionsById[thread.environmentId]?.environmentLabel ?? null)
630+
: null
631+
}
632+
onSelectThread={props.onSelectThread}
633+
onDeleteThread={handleDeleteThread}
634+
onArchiveThread={props.onArchiveThread}
635+
settlementSupported={settlementEnvironmentIds.has(thread.environmentId)}
636+
onSettleThread={handleSettleThread}
637+
onUnsettleThread={handleUnsettleThread}
638+
onChangeRequestState={handleChangeRequestState}
639+
projectCwd={
640+
projectCwdByKey.get(scopedProjectKey(thread.environmentId, thread.projectId)) ?? null
641+
}
642+
onSwipeableClose={handleSwipeableClose}
643+
onSwipeableWillOpen={handleSwipeableWillOpen}
644+
/>
645+
);
646+
},
596647
[
597648
handleChangeRequestState,
598649
handleDeleteThread,
@@ -603,17 +654,16 @@ export function HomeScreen(props: HomeScreenProps) {
603654
projectByKey,
604655
projectCwdByKey,
605656
props.onArchiveThread,
657+
props.onDeletePendingTask,
658+
props.onSelectPendingTask,
606659
props.onSelectThread,
607660
props.savedConnectionsById,
608661
serverConfigs,
609662
settlementEnvironmentIds,
610663
v2ProjectTitleByProjectKey,
611664
],
612665
);
613-
const v2KeyExtractor = useCallback(
614-
(item: ThreadListV2Item) => `${item.thread.environmentId}:${item.thread.id}`,
615-
[],
616-
);
666+
const v2KeyExtractor = useCallback((item: ThreadListV2ListItem) => item.key, []);
617667

618668
const extraData = useMemo(
619669
() => ({ savedConnectionsById: props.savedConnectionsById, projectCwdByKey }),
@@ -788,41 +838,9 @@ export function HomeScreen(props: HomeScreenProps) {
788838
</>
789839
);
790840

791-
// v2 renders queued offline tasks above the thread cards — they are not
792-
// thread shells, so the v2 item builder never sees them, but they must
793-
// stay visible and deletable while their environment is offline. They
794-
// respect the same environment scope and search filter as the list.
795-
const v2SearchQuery = props.searchQuery.trim().toLocaleLowerCase();
796-
const v2PendingTasks = props.pendingTasks.filter(
797-
(pendingTask) =>
798-
(props.selectedEnvironmentId === null ||
799-
pendingTask.message.environmentId === props.selectedEnvironmentId) &&
800-
(v2ScopedProjectKeys === null ||
801-
v2ScopedProjectKeys.has(
802-
scopedProjectKey(pendingTask.message.environmentId, pendingTask.creation.projectId),
803-
)) &&
804-
(v2SearchQuery.length === 0 || pendingTask.title.toLocaleLowerCase().includes(v2SearchQuery)),
805-
);
806841
// Project scoping lives in the header filter menu (no inline chip row on
807842
// mobile — the menu is the one filter surface).
808-
const v2ListHeader = (
809-
<>
810-
{listHeader}
811-
{v2PendingTasks.map((pendingTask, index) => (
812-
<PendingTaskListRow
813-
key={pendingTask.message.messageId}
814-
variant="compact"
815-
pendingTask={pendingTask}
816-
environmentLabel={
817-
props.savedConnectionsById[pendingTask.message.environmentId]?.environmentLabel ?? null
818-
}
819-
isLast={index === v2PendingTasks.length - 1}
820-
onSelectPendingTask={props.onSelectPendingTask}
821-
onDeletePendingTask={props.onDeletePendingTask}
822-
/>
823-
))}
824-
</>
825-
);
843+
const v2ListHeader = listHeader;
826844

827845
const listEmpty = !hasResults ? (
828846
hasSearchQuery ? (
@@ -846,37 +864,33 @@ export function HomeScreen(props: HomeScreenProps) {
846864
// is empty. Search outranks the scope — "No results" names the actionable
847865
// fact when a query is active. Snoozed threads outrank the rest: "No
848866
// threads yet" over an inbox that is merely all-snoozed reads as data
849-
// loss. Pending tasks render in the header, so the list showing them
850-
// isn't empty in the user's eyes.
867+
// loss.
851868
const v2SnoozedCount = threadListV2Layout.snoozedCount;
852-
const v2ListEmpty =
853-
v2PendingTasks.length > 0 ? null : hasSearchQuery ? (
854-
v2SnoozedCount > 0 ? (
855-
// The snoozed threads already passed this search filter: "No
856-
// results" would claim nothing matched when matches are merely
857-
// parked.
858-
<EmptyState
859-
title={
860-
v2SnoozedCount === 1 ? "1 matching thread snoozed" : `All matching threads snoozed`
861-
}
862-
detail={`Threads matching "${props.searchQuery}" are snoozed and return when their wake time passes.`}
863-
/>
864-
) : (
865-
<EmptyState title="No results" detail={`No threads matching "${props.searchQuery}".`} />
866-
)
867-
) : v2SnoozedCount > 0 ? (
869+
const v2ListEmpty = hasSearchQuery ? (
870+
v2SnoozedCount > 0 ? (
871+
// The snoozed threads already passed this search filter: "No
872+
// results" would claim nothing matched when matches are merely
873+
// parked.
868874
<EmptyState
869-
title={v2SnoozedCount === 1 ? "1 thread snoozed" : `${v2SnoozedCount} threads snoozed`}
870-
detail="Snoozed threads return when their wake time passes."
871-
/>
872-
) : v2ScopedProjectGroup !== null ? (
873-
<EmptyState
874-
title={`No threads in ${v2ScopedProjectGroup.title}`}
875-
detail="Choose another project or create a new task."
875+
title={v2SnoozedCount === 1 ? "1 matching thread snoozed" : `All matching threads snoozed`}
876+
detail={`Threads matching "${props.searchQuery}" are snoozed and return when their wake time passes.`}
876877
/>
877878
) : (
878-
listEmpty
879-
);
879+
<EmptyState title="No results" detail={`No threads matching "${props.searchQuery}".`} />
880+
)
881+
) : v2SnoozedCount > 0 ? (
882+
<EmptyState
883+
title={v2SnoozedCount === 1 ? "1 thread snoozed" : `${v2SnoozedCount} threads snoozed`}
884+
detail="Snoozed threads return when their wake time passes."
885+
/>
886+
) : v2ScopedProjectGroup !== null ? (
887+
<EmptyState
888+
title={`No threads in ${v2ScopedProjectGroup.title}`}
889+
detail="Choose another project or create a new task."
890+
/>
891+
) : (
892+
listEmpty
893+
);
880894

881895
if (threadListV2Enabled) {
882896
return (
@@ -888,6 +902,8 @@ export function HomeScreen(props: HomeScreenProps) {
888902
keyExtractor={v2KeyExtractor}
889903
extraData={{
890904
projectByKey,
905+
projectCwdByKey,
906+
projectTitleByProjectKey: v2ProjectTitleByProjectKey,
891907
serverConfigs,
892908
savedConnectionsById: props.savedConnectionsById,
893909
}}

0 commit comments

Comments
 (0)