@@ -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" ;
4040import {
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" ;
4647import type { HomeListFilterMenuEnvironment } from "./home-list-filter-menu" ;
4748import {
@@ -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