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
38 changes: 32 additions & 6 deletions apps/mobile/src/features/threads/ActionResumeNotice.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { EnvironmentId, OrchestrationThreadShell } from "@t3tools/contracts";
import { actionRunningPresentation } from "@t3tools/shared/actionResume";
import * as Cause from "effect/Cause";
import { useCallback, useEffect, useState } from "react";
import { Alert, View } from "react-native";
Expand Down Expand Up @@ -71,16 +72,41 @@ export function ActionResumeNotice(props: {
);

if (action?.outcome === "running") {
const presentation = actionRunningPresentation(action);
const working = presentation.state === "working";
return (
<View className="rounded-2xl border border-adaptive-yellow-400-a35-300-a20 bg-yellow-500/10 px-3.5 py-3">
<View
className={
working
? "rounded-2xl border border-sky-500/25 bg-sky-500/10 px-3.5 py-3"
: "rounded-2xl border border-adaptive-yellow-400-a35-300-a20 bg-yellow-500/10 px-3.5 py-3"
}
>
<View className="flex-row items-center gap-2.5">
<SymbolView name="clock" size={18} tintColor="#eab308" type="monochrome" />
<SymbolView
name={working ? "gearshape" : "clock"}
size={18}
tintColor={working ? "#0a84ff" : "#eab308"}
type="monochrome"
/>
<View className="min-w-0 flex-1">
<Text className="text-sm font-t3-bold text-adaptive-yellow-800-200">
Waiting for {action.actionName}
<Text
className={
working
? "text-sm font-t3-bold text-adaptive-sky-700-300"
: "text-sm font-t3-bold text-adaptive-yellow-800-200"
}
>
{presentation.label}: {action.actionName}
</Text>
<Text className="mt-0.5 text-xs text-adaptive-yellow-800-a75-200-a70">
The agent will resume once this Action finishes and the thread is idle.
<Text
className={
working
? "mt-0.5 text-xs text-adaptive-sky-700-300 opacity-75"
: "mt-0.5 text-xs text-adaptive-yellow-800-a75-200-a70"
}
>
{presentation.summary}
</Text>
</View>
<ControlPill
Expand Down
72 changes: 54 additions & 18 deletions apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { type LegendListRef } from "@legendapp/list/react-native";
import type { EnvironmentId, MessageId, ThreadId, TurnId } from "@t3tools/contracts";
import { classifyMarkdownImageSource } from "@t3tools/client-runtime/markdown-images";
import { CHAT_LIST_ANCHOR_OFFSET, resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList";
import { parseActionResumeFollowUp } from "@t3tools/shared/actionResume";
import {
actionResultPresentation,
type ActionResultPresentationOutcome,
parseActionResumeFollowUp,
} from "@t3tools/shared/actionResume";
import { formatElapsed } from "@t3tools/shared/orchestrationTiming";
import { SymbolView } from "../../components/AppSymbol";
import { HeaderHeightContext } from "@react-navigation/elements";
Expand Down Expand Up @@ -1031,12 +1035,13 @@ function renderFeedEntry(
const actionFollowUp =
message.role === "system" ? parseActionResumeFollowUp(message.text) : null;
if (actionFollowUp) {
const presentation = actionResultPresentation(actionFollowUp);
return (
<ActionFollowUpCard
actionName={actionFollowUp.actionName}
exitCode={actionFollowUp.exitCode}
validatedStatus={actionFollowUp.validatedStatus}
lastOutputLine={actionFollowUp.lastOutputLine}
outcome={presentation.outcome}
outcomeLabel={presentation.label}
summary={presentation.summary}
output={actionFollowUp.output}
detailedOutputAvailable={actionFollowUp.detailedOutputAvailable}
iconColor={iconSubtleColor}
Expand Down Expand Up @@ -1203,33 +1208,64 @@ function renderFeedEntry(

const ActionFollowUpCard = memo(function ActionFollowUpCard(props: {
readonly actionName: string;
readonly exitCode: number | null;
readonly validatedStatus: string;
readonly lastOutputLine: string;
readonly outcome: ActionResultPresentationOutcome;
readonly outcomeLabel: string;
readonly summary: string;
readonly output: string;
readonly detailedOutputAvailable: boolean;
readonly iconColor: string | ColorValue;
readonly expanded: boolean;
readonly onToggle: () => void;
}) {
const status = props.exitCode ?? props.validatedStatus;
const tone =
props.outcome === "success"
? {
container: "border-emerald-500/25 bg-emerald-500/[0.06]",
text: "text-adaptive-emerald-700-300",
color: "#30d158",
icon: "checkmark.circle" as const,
}
: props.outcome === "error"
? {
container: "border-rose-500/25 bg-rose-500/[0.06]",
text: "text-adaptive-rose-700-300",
color: "#ff453a",
icon: "xmark.circle.fill" as const,
}
: props.outcome === "cancelled"
? {
container: "border-adaptive-black-a10-white-a10 bg-neutral-500/[0.06]",
text: "text-foreground-muted",
color: props.iconColor,
icon: "xmark.circle.fill" as const,
}
: props.outcome === "blocked"
? {
container: "border-violet-500/25 bg-violet-500/[0.06]",
text: "text-adaptive-violet-700-300",
color: "#bf5af2",
icon: "exclamationmark.triangle" as const,
}
: {
container: "border-amber-500/25 bg-amber-500/[0.06]",
text: "text-adaptive-amber-700-300",
color: "#eab308",
icon: "exclamationmark.triangle" as const,
};
const heading = (
<>
<SymbolView name="cpu" size={14} tintColor={props.iconColor} type="monochrome" />
<Text
className="min-w-0 flex-1 font-t3-medium text-xs text-adaptive-amber-800-200"
numberOfLines={1}
>
Action completed: {props.actionName} Status: {status}
<SymbolView name={tone.icon} size={14} tintColor={tone.color} type="monochrome" />
<Text className={cn("min-w-0 flex-1 font-t3-medium text-xs", tone.text)} numberOfLines={1}>
{props.outcomeLabel}: {props.actionName}
</Text>
</>
);

return (
<View className="mb-5 overflow-hidden rounded-xl border border-amber-500/25 bg-amber-500/[0.06]">
<View className={cn("mb-5 overflow-hidden rounded-xl border", tone.container)}>
{props.detailedOutputAvailable ? (
<View
accessibilityLabel={`Action completed: ${props.actionName}. Status: ${status}`}
accessibilityLabel={`${props.outcomeLabel}: ${props.actionName}. ${props.summary}`}
className="min-h-10 flex-row items-center gap-1.5 px-3 pt-2.5"
>
{heading}
Expand All @@ -1238,7 +1274,7 @@ const ActionFollowUpCard = memo(function ActionFollowUpCard(props: {
<Pressable
accessibilityRole="button"
accessibilityState={{ expanded: props.expanded }}
accessibilityLabel={`Action completed: ${props.actionName}. Status: ${status}`}
accessibilityLabel={`${props.outcomeLabel}: ${props.actionName}. ${props.summary}`}
className="min-h-10 flex-row items-center gap-1.5 px-3 pt-2.5"
onPress={props.onToggle}
>
Expand All @@ -1263,7 +1299,7 @@ const ActionFollowUpCard = memo(function ActionFollowUpCard(props: {
) : (
<View className="px-3 pb-2.5 pt-1">
<Text className="text-sm text-foreground" numberOfLines={1}>
{props.lastOutputLine}
{props.summary}
</Text>
{props.detailedOutputAvailable ? (
<Text className="mt-0.5 text-xs text-foreground-muted">
Expand Down
7 changes: 5 additions & 2 deletions apps/mobile/src/features/threads/thread-list-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type { HomeGroupDisplayAction } from "../home/homeListItems";
import { ThreadSwipeable } from "../home/thread-swipe-actions";
import { buildThreadTitleRegenerationMenuItems } from "./thread-title-regeneration-menu";
import { resolveThreadStatus, shouldShowActionWaitingIndicator } from "./threadPresentation";
import { actionRunningPresentation } from "@t3tools/shared/actionResume";
import { ThreadSearchMatchExcerpt } from "./thread-search-match";

/**
Expand Down Expand Up @@ -480,7 +481,9 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
const showActionWaitingIndicator = shouldShowActionWaitingIndicator(thread, status?.kind ?? null);
const threadAccessibilityLabel = [
thread.title,
showActionWaitingIndicator && runningAction ? `Waiting for ${runningAction.actionName}` : null,
showActionWaitingIndicator && runningAction
? `Waiting for ${runningAction.actionName}. ${actionRunningPresentation(runningAction).summary}`
: null,
pr?.accessibilityLabel ?? null,
]
.filter((part): part is string => part !== null)
Expand Down Expand Up @@ -623,7 +626,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
const actionStatusIndicator =
showActionWaitingIndicator && runningAction ? (
<View
accessibilityLabel={`Waiting for ${runningAction.actionName}`}
accessibilityLabel={`Waiting for ${runningAction.actionName}. ${actionRunningPresentation(runningAction).summary}`}
className="size-3 items-center justify-center"
>
<View className="size-1.5 rounded-full bg-adaptive-yellow-500-300" />
Expand Down
7 changes: 5 additions & 2 deletions apps/mobile/src/features/threads/thread-list-v2-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
resolveWorktreeCleanupStatus,
shouldShowActionWaitingIndicator,
} from "./threadPresentation";
import { actionRunningPresentation } from "@t3tools/shared/actionResume";
import {
resolveThreadListV2ChangeRequestState,
resolveThreadListV2CleanupActions,
Expand Down Expand Up @@ -460,7 +461,9 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
const timeLabel = threadTimeLabel(thread);
const threadAccessibilityLabel = [
thread.title,
showActionWaitingIndicator && runningAction ? `Waiting for ${runningAction.actionName}` : null,
showActionWaitingIndicator && runningAction
? `Waiting for ${runningAction.actionName}. ${actionRunningPresentation(runningAction).summary}`
: null,
]
.filter((part): part is string => part !== null)
.join(", ");
Expand Down Expand Up @@ -833,7 +836,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
<View className="flex-row items-center gap-1">
{showActionWaitingIndicator && runningAction ? (
<SymbolView
accessibilityLabel={`Waiting for ${runningAction.actionName}`}
accessibilityLabel={`Waiting for ${runningAction.actionName}. ${actionRunningPresentation(runningAction).summary}`}
name="clock.arrow.circlepath"
size={12}
tintColor={selected ? String(selectedForegroundColor) : "#eab308"}
Expand Down
22 changes: 22 additions & 0 deletions apps/mobile/src/features/threads/threadListV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,28 @@ describe("resolveThreadListV2Status", () => {
expect(shouldShowActionWaitingIndicator(working, "working")).toBe(true);
});

it("presents Action working progress without a redundant secondary indicator", () => {
const actionResume = {
outcome: "running",
actionName: "QA",
progress: {
version: 1,
state: "working",
summary: "Running checks",
updatedAt: NOW,
},
} as NonNullable<EnvironmentThreadShell["actionResume"]>;
const working = makeThread({
id: ThreadId.make("working-progress"),
title: "Working Action",
actionResume,
});

expect(resolveThreadListV2Status(working)).toBe("working");
expect(resolveThreadStatus(working)).toMatchObject({ kind: "working", label: "Working" });
expect(shouldShowActionWaitingIndicator(working, "working")).toBe(false);
});

it("resolves ready for quiescent threads", () => {
expect(resolveThreadListV2Status(makeThread({ id: ThreadId.make("t"), title: "t" }))).toBe(
"ready",
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/src/features/threads/threadListV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
sortPinnedThreadsByOrderKey,
} from "@t3tools/client-runtime/state/thread-sort";
import type { EnvironmentId, ProjectId, ThreadLinkedPullRequest } from "@t3tools/contracts";
import { actionRunningPresentation } from "@t3tools/shared/actionResume";

import type { PendingNewTask } from "../../state/use-pending-new-tasks";

Expand Down Expand Up @@ -185,7 +186,7 @@ export function resolveThreadListV2Status(
return "failed";
}
if (thread.actionResume?.outcome === "running") {
return "waiting";
return actionRunningPresentation(thread.actionResume).state;
}
return "ready";
}
Expand Down
24 changes: 17 additions & 7 deletions apps/mobile/src/features/threads/threadPresentation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { StatusTone } from "../../components/StatusPill";
import type { OrchestrationLatestTurn, OrchestrationSession } from "@t3tools/contracts";
import { actionRunningPresentation } from "@t3tools/shared/actionResume";
import { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";

export function threadSortValue(thread: EnvironmentThreadShell): number {
Expand Down Expand Up @@ -39,7 +40,11 @@ export function shouldShowActionWaitingIndicator(
thread: Pick<EnvironmentThreadShell, "actionResume">,
primaryStatus: string | null,
): boolean {
return thread.actionResume?.outcome === "running" && primaryStatus !== "waiting";
return (
thread.actionResume?.outcome === "running" &&
actionRunningPresentation(thread.actionResume).state === "waiting" &&
primaryStatus !== "waiting"
);
}

function isLatestTurnSettled(
Expand Down Expand Up @@ -157,13 +162,18 @@ export function resolveThreadStatus(
}

if (thread.actionResume?.outcome === "running") {
const action = actionRunningPresentation(thread.actionResume);
return {
kind: "waiting",
label: "Waiting",
pillClassName: "bg-adaptive-yellow-500-a12-a16",
textClassName: "text-adaptive-yellow-700-300",
iconColor: "#eab308",
iconBackground: "rgba(234,179,8,0.22)",
kind: action.state,
label: action.label,
pillClassName:
action.state === "working"
? "bg-adaptive-sky-500-a12-a16"
: "bg-adaptive-yellow-500-a12-a16",
textClassName:
action.state === "working" ? "text-adaptive-sky-700-300" : "text-adaptive-yellow-700-300",
iconColor: action.state === "working" ? "#0a84ff" : "#eab308",
iconBackground: action.state === "working" ? "rgba(10,132,255,0.22)" : "rgba(234,179,8,0.22)",
pulse: false,
};
}
Expand Down
Loading