Open History to see the last commit
1import { useAtomValue } from "@effect/atom-react";
2import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native";
3import { scopeThreadRef } from "@t3tools/client-runtime/environment";
4import {
5 presentPendingBackgroundWork,
6 type PendingBackgroundWorkItem,
7} from "@t3tools/client-runtime/state/thread-execution";
8import type { EnvironmentId, ThreadId } from "@t3tools/contracts";
9import * as Haptics from "expo-haptics";
10import { Pressable, ScrollView, View } from "react-native";
11import { useSafeAreaInsets } from "react-native-safe-area-context";
12
13import { SheetHeader } from "../../components/ui/SheetHeader";
14import { SymbolView } from "../../components/ui/AppSymbol";
15import { AppText as Text } from "../../components/ui/AppText";
16import { ScreenContent } from "../../components/ui/ScreenContent";
17import { environmentThreadShells } from "../../state/threads";
18
19const KIND_LABELS = {
20 subagent: "Subagent",
21 command: "Command",
22 monitor: "Monitor",
23 background_task: "Background task",
24} as const;
25
26type BackgroundWorkTarget = { readonly environmentId: EnvironmentId; readonly threadId: ThreadId };
27
28export function ThreadBackgroundWorkSheet({ route }: StaticScreenProps<BackgroundWorkTarget>) {
29 const navigation = useNavigation();
30 const insets = useSafeAreaInsets();
31 const target = route.params;
32 const thread = useAtomValue(
33 environmentThreadShells.threadShellAtom(scopeThreadRef(target.environmentId, target.threadId)),
34 );
35 const presentation = presentPendingBackgroundWork(thread?.pendingBackgroundTasks ?? []);
36 const openThread = (threadId: ThreadId) => {
37 void Haptics.selectionAsync();
38 // A thread replaces the leaf sheet so workspace back navigation still works.
39 navigation.dispatch(
40 StackActions.replace("Thread", { environmentId: target.environmentId, threadId }),
41 );
42 };
43 const content = (
44 <ScrollView
45 nestedScrollEnabled
46 className="flex-1"
47 contentInsetAdjustmentBehavior="automatic"
48 contentContainerClassName="px-5 pb-6"
49 contentContainerStyle={{ paddingBottom: Math.max(insets.bottom, 16) + 8 }}
50 >
51 {presentation === null ? (
52 <Text className="pt-6 text-center text-sm text-foreground-muted">
53 Nothing running in the background.
54 </Text>
55 ) : (
56 presentation.items.map((item) => (
57 <BackgroundWorkRow key={item.taskId} item={item} onOpen={openThread} />
58 ))
59 )}
60 </ScrollView>
61 );
62 return (
63 <View collapsable={false} className="flex-1 bg-sheet">
64 <SheetHeader title="Background work" onClose={() => navigation.goBack()} />
65 <ScreenContent>{content}</ScreenContent>
66 </View>
67 );
68}
69
70function BackgroundWorkRow(props: {
71 readonly item: PendingBackgroundWorkItem;
72 readonly onOpen: (threadId: ThreadId) => void;
73}) {
74 const { item } = props;
75 const childThreadId = item.childThreadId;
76 const row = (
77 <View className="flex-row items-center gap-3 border-b border-border py-3.5">
78 <SymbolView
79 name={
80 item.kind === "subagent" ? "person.2" : item.kind === "monitor" ? "clock" : "terminal"
81 }
82 size={15}
83 tintColorClassName="accent-foreground-muted"
84 type="monochrome"
85 />
86 <View className="min-w-0 flex-1">
87 <Text className="font-t3-medium text-sm text-foreground">{item.label}</Text>
88 <Text className="text-xs text-foreground-muted">{KIND_LABELS[item.kind]}</Text>
89 </View>
90 {childThreadId === undefined ? null : (
91 <SymbolView name="chevron.right" size={12} tintColorClassName="accent-icon-subtle" />
92 )}
93 </View>
94 );
95 return childThreadId === undefined ? (
96 <View accessible>{row}</View>
97 ) : (
98 <Pressable
99 accessibilityRole="link"
100 accessibilityLabel={`Open subagent ${item.label}`}
101 onPress={() => props.onOpen(childThreadId)}
102 className="active:opacity-70"
103 >
104 {row}
105 </Pressable>
106 );
107}