Skip to content
Open
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
10 changes: 8 additions & 2 deletions apps/mobile/src/features/threads/SubagentRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function SubagentRow(props: {
readonly environmentId: EnvironmentId;
readonly subagent: SubagentRowSubagent;
readonly elapsed: ReactNode;
readonly reserveMetadataAction?: boolean;
}) {
const presentation = resolveSubagentRowPresentation(props.subagent);
const detail = subagentCardDetail(presentation.detail);
Expand Down Expand Up @@ -70,7 +71,11 @@ export function SubagentRow(props: {
<SymbolView name="chevron.right" size={12} tintColorClassName="accent-icon-subtle" />
) : null}
</View>
<SubagentMetadata environmentId={props.environmentId} subagent={props.subagent} />
<SubagentMetadata
environmentId={props.environmentId}
subagent={props.subagent}
className={props.reserveMetadataAction ? "pr-9" : undefined}
/>
{detail ? (
<Text
numberOfLines={3}
Expand All @@ -90,6 +95,7 @@ export function SubagentRow(props: {
function SubagentMetadata(props: {
readonly environmentId: EnvironmentId;
readonly subagent: SubagentRowSubagent;
readonly className?: string;
}) {
const { environmentId, subagent } = props;
const config = useEnvironmentServerConfig(environmentId);
Expand All @@ -113,7 +119,7 @@ function SubagentMetadata(props: {
childProject,
});
return (
<View className="min-w-0 flex-row items-center gap-1.5">
<View className={cn("min-w-0 flex-row items-center gap-1.5", props.className)}>
<ProviderIcon
provider={provider?.driver ?? subagent.driver}
iconUrl={provider?.iconUrl}
Expand Down
86 changes: 86 additions & 0 deletions apps/mobile/src/features/threads/SubagentStopButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useAtomValue } from "@effect/atom-react";
import type { EnvironmentId, OrchestrationV2Subagent } from "@t3tools/contracts";
import { useState } from "react";
import { Alert, Pressable, View } from "react-native";

import { AppActivityIndicator } from "../../components/ui/AppActivityIndicator";
import { SymbolView } from "../../components/ui/AppSymbol";
import { cn } from "../../lib/cn";
import { orchestrationEnvironment } from "../../state/orchestration";
import { threadEnvironment } from "../../state/threads";
import { useAtomCommand } from "../../state/use-atom-command";
import {
resolveSubagentRowPresentation,
resolveSubagentStopTarget,
} from "./threadAgentsPresentation";

export function SubagentStopButton(props: {
readonly environmentId: EnvironmentId;
readonly subagent: OrchestrationV2Subagent;
readonly className?: string;
}) {
const target = resolveSubagentStopTarget(props.subagent);
const canStop = useAtomValue(
orchestrationEnvironment.stopWorkflow.permissionAtom(props.environmentId),
);
const stopWorkflow = useAtomCommand(orchestrationEnvironment.stopWorkflow);
const interruptTurn = useAtomCommand(threadEnvironment.interruptTurn);
const [stopping, setStopping] = useState(false);

if (target === null || !canStop) return null;

const workflow = target.kind === "workflow";
const label =
props.subagent.workflow?.name ?? resolveSubagentRowPresentation(props.subagent).title;
const confirmStop = () =>
Alert.alert(
workflow ? "Stop workflow?" : "Stop agent?",
workflow ? `Every running agent in ${label} stops.` : `Stop ${label}?`,
[
{ text: "Cancel", style: "cancel" },
{
text: "Stop",
style: "destructive",
onPress: async () => {
setStopping(true);
try {
const result =
target.kind === "workflow"
? await stopWorkflow({ environmentId: props.environmentId, input: target.input })
: await interruptTurn({
environmentId: props.environmentId,
input: target.input,
});
if (result._tag === "Failure") {
Alert.alert(workflow ? "Could not stop workflow" : "Could not stop agent");
}
} finally {
setStopping(false);
}
},
},
],
);

return (
<Pressable
accessibilityRole="button"
accessibilityLabel={`Stop ${workflow ? "workflow" : "agent"} ${label}`}
accessibilityState={{ disabled: stopping, busy: stopping }}
disabled={stopping}
onPress={confirmStop}
className={cn(
"size-11 items-center justify-center active:opacity-70 disabled:opacity-50",
props.className,
)}
>
<View className="size-6 items-center justify-center rounded-full bg-danger">
{stopping ? (
<AppActivityIndicator size="small" />
) : (
<SymbolView name="stop.fill" size={9} tintColorClassName="accent-danger-foreground" />
)}
</View>
</Pressable>
);
}
82 changes: 60 additions & 22 deletions apps/mobile/src/features/threads/ThreadAgentsSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { useAtomValue } from "@effect/atom-react";
import {
deriveWorkflowGroups,
projectedSubagentsToRuntime,
} from "@t3tools/client-runtime/state/subagentRuntime";
import type { ThreadTurnSubagents } from "@t3tools/client-runtime/state/thread-subagents";
import {
isOrchestrationV2WorkActive,
Expand All @@ -11,7 +15,7 @@ import { StackActions, useNavigation, type StaticScreenProps } from "@react-navi
import * as DateTime from "effect/DateTime";
import { Atom } from "effect/reactivity";
import * as Haptics from "expo-haptics";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Platform, Pressable, ScrollView, View } from "react-native";
import { Screen, ScreenStack, ScreenStackHeaderConfig } from "react-native-screens";
import { useSafeAreaInsets } from "react-native-safe-area-context";
Expand All @@ -22,6 +26,9 @@ import { useUniwindTheme } from "../../lib/useUniwindTheme";
import { environmentThreadDetails } from "../../state/threads";
import { NativeStackScreenOptions, nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";
import { SubagentRow } from "./SubagentRow";
import { SubagentStopButton } from "./SubagentStopButton";
import { ThreadWorkflowGroup } from "./ThreadWorkflowGroup";
import { resolveSubagentStopTarget } from "./threadAgentsPresentation";
import { ScreenContent } from "../../components/ui/ScreenContent";
import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";

Expand Down Expand Up @@ -50,6 +57,16 @@ export function ThreadAgentsSheet({ route }: StaticScreenProps<AgentsTarget>) {
const turn = useThreadTurnSubagents(target);
const subagents = turn?.subagents ?? [];
const hasLiveAgent = (turn?.liveCount ?? 0) > 0;
const workflowGroups = useMemo(
() =>
new Map(
deriveWorkflowGroups(projectedSubagentsToRuntime(subagents)).map((group) => [
group.workflow.id,
group,
]),
),
[subagents],
);

const openChildThread = (childThreadId: ThreadId) => {
void Haptics.selectionAsync();
Expand Down Expand Up @@ -78,15 +95,27 @@ export function ThreadAgentsSheet({ route }: StaticScreenProps<AgentsTarget>) {
No agents in this turn.
</Text>
) : (
subagents.map((subagent) => (
<AgentRow
key={subagent.id}
subagent={subagent}
environmentId={target.environmentId}
tickSeconds={hasLiveAgent}
onOpen={openChildThread}
/>
))
subagents.map((subagent) => {
const group = workflowGroups.get(subagent.id);
return group === undefined ? (
<View key={subagent.id} className="border-b border-border">
<AgentRow
subagent={subagent}
environmentId={target.environmentId}
tickSeconds={hasLiveAgent}
onOpen={openChildThread}
/>
</View>
) : (
<ThreadWorkflowGroup
key={subagent.id}
environmentId={target.environmentId}
subagent={subagent}
group={group}
onOpen={openChildThread}
/>
);
})
)}
</ScrollView>
);
Expand Down Expand Up @@ -142,35 +171,44 @@ function AgentRow(props: {
const childThreadId = subagent.childThreadId;

const row = (
<View className="border-b border-border py-3.5">
<View className="py-3.5">
<SubagentRow
environmentId={props.environmentId}
subagent={subagent}
elapsed={<AgentElapsed subagent={subagent} tickSeconds={props.tickSeconds} />}
reserveMetadataAction={resolveSubagentStopTarget(subagent) !== null}
/>
</View>
);

if (childThreadId === null) {
return (
const content =
childThreadId === null ? (
<View
accessible
accessibilityHint="Provider-managed agent. Its work appears in the transcript."
>
{row}
</View>
) : (
<Pressable
accessibilityRole="link"
accessibilityHint="Opens this agent thread"
onPress={() => props.onOpen(childThreadId)}
className="active:opacity-70"
>
{row}
</Pressable>
);
}

return (
<Pressable
accessibilityRole="link"
accessibilityHint="Opens this agent's thread"
onPress={() => props.onOpen(childThreadId)}
className="active:opacity-70"
>
{row}
</Pressable>
<View className="-mr-2.5 pr-2.5">
{content}
<SubagentStopButton
environmentId={props.environmentId}
subagent={subagent}
className="absolute right-0 top-6"
/>
</View>
);
}

Expand Down
Loading
Loading