| ... | @@ -25,7 +25,7 @@ import { Alert, Pressable, useWindowDimensions, View } from "react-native"; | ... | @@ -25,7 +25,7 @@ import { Alert, Pressable, useWindowDimensions, View } from "react-native"; |
| 25 | import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; | 25 | import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; |
| 26 | | 26 | |
| 27 | import type { ThreadListProvider } from "../../state/thread-list-environments"; | 27 | import type { ThreadListProvider } from "../../state/thread-list-environments"; |
| 28 | import { SymbolView } from "../../components/AppSymbol"; | 28 | import { SymbolView, type AppSymbolName } from "../../components/AppSymbol"; |
| 29 | import { AppText as Text } from "../../components/AppText"; | 29 | import { AppText as Text } from "../../components/AppText"; |
| 30 | import { ControlPillMenu } from "../../components/ControlPill"; | 30 | import { ControlPillMenu } from "../../components/ControlPill"; |
| 31 | import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol"; | 31 | import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol"; |
| ... | @@ -51,6 +51,7 @@ import { | ... | @@ -51,6 +51,7 @@ import { |
| 51 | type ThreadListV2Status, | 51 | type ThreadListV2Status, |
| 52 | } from "./threadListV2"; | 52 | } from "./threadListV2"; |
| 53 | import { QueuedMessageIcon } from "./queued-message-icon"; | 53 | import { QueuedMessageIcon } from "./queued-message-icon"; |
| | 54 | import { shouldRecedeThreadRow } from "./thread-row-emphasis"; |
| 54 | import { ThreadSearchMatchExcerpt } from "./thread-search-match"; | 55 | import { ThreadSearchMatchExcerpt } from "./thread-search-match"; |
| 55 | | 56 | |
| 56 | /** | 57 | /** |
| ... | @@ -63,14 +64,50 @@ import { ThreadSearchMatchExcerpt } from "./thread-search-match"; | ... | @@ -63,14 +64,50 @@ import { ThreadSearchMatchExcerpt } from "./thread-search-match"; |
| 63 | // Status hues follow the system-wide convention set by sidebar v1 and the | 64 | // Status hues follow the system-wide convention set by sidebar v1 and the |
| 64 | // Live Activity/widgets (amber approval, indigo input, sky working) so a | 65 | // Live Activity/widgets (amber approval, indigo input, sky working) so a |
| 65 | // thread reads the same color everywhere it surfaces. | 66 | // thread reads the same color everywhere it surfaces. |
| 66 | const STATUS_LABEL_BY_STATUS: Partial< | 67 | // Icons match the web sidebar's status glyphs. |
| 67 | Record<ThreadListV2Status, { label: string; className: string }> | 68 | interface StatusLabel { |
| 68 | > = { | 69 | readonly label: string; |
| 69 | approval: { label: "Approval", className: "text-warning-foreground" }, | 70 | readonly icon: AppSymbolName; |
| 70 | input: { label: "Input", className: "text-adaptive-indigo-600-300" }, | 71 | readonly className: string; |
| 71 | working: { label: "Working", className: "text-adaptive-sky-600-400" }, | 72 | readonly iconTintClassName: string; |
| 72 | failed: { label: "Failed", className: "text-danger-foreground" }, | 73 | } |
| 73 | limited: { label: "Limited", className: "text-warning-foreground" }, | 74 | const STATUS_LABEL_BY_STATUS: Partial<Record<ThreadListV2Status, StatusLabel>> = { |
| | 75 | approval: { |
| | 76 | label: "Approval", |
| | 77 | icon: "exclamationmark.shield", |
| | 78 | className: "text-warning-foreground", |
| | 79 | iconTintClassName: "accent-warning-foreground", |
| | 80 | }, |
| | 81 | input: { |
| | 82 | label: "Input", |
| | 83 | icon: "questionmark.bubble", |
| | 84 | className: "text-adaptive-indigo-600-300", |
| | 85 | iconTintClassName: "accent-adaptive-indigo-600-300", |
| | 86 | }, |
| | 87 | working: { |
| | 88 | label: "Working", |
| | 89 | icon: "circle.dashed", |
| | 90 | className: "text-adaptive-sky-600-400", |
| | 91 | iconTintClassName: "accent-adaptive-sky-600-400", |
| | 92 | }, |
| | 93 | failed: { |
| | 94 | label: "Failed", |
| | 95 | icon: "exclamationmark.circle", |
| | 96 | className: "text-danger-foreground", |
| | 97 | iconTintClassName: "accent-danger-foreground", |
| | 98 | }, |
| | 99 | limited: { |
| | 100 | label: "Limited", |
| | 101 | icon: "exclamationmark.circle", |
| | 102 | className: "text-warning-foreground", |
| | 103 | iconTintClassName: "accent-warning-foreground", |
| | 104 | }, |
| | 105 | }; |
| | 106 | const DONE_STATUS_LABEL: StatusLabel = { |
| | 107 | label: "Done", |
| | 108 | icon: "checkmark.circle", |
| | 109 | className: "text-adaptive-emerald-700-300", |
| | 110 | iconTintClassName: "accent-adaptive-emerald-700-300", |
| 74 | }; | 111 | }; |
| 75 | | 112 | |
| 76 | // Menus keep lifecycle and title regeneration together. Archive keeps its | 113 | // Menus keep lifecycle and title regeneration together. Archive keeps its |
| ... | @@ -593,8 +630,8 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { | ... | @@ -593,8 +630,8 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { |
| 593 | // A native /goal keeps the agent going across turns until it is met. | 630 | // A native /goal keeps the agent going across turns until it is met. |
| 594 | (status === "working" && workingLabel !== undefined && thread.goal?.status === "active" | 631 | (status === "working" && workingLabel !== undefined && thread.goal?.status === "active" |
| 595 | ? { ...workingLabel, label: "Goal" } | 632 | ? { ...workingLabel, label: "Goal" } |
| 596 | : workingLabel) ?? | 633 | : workingLabel) ?? (isUnread ? DONE_STATUS_LABEL : undefined); |
| 597 | (isUnread ? { label: "Done", className: "text-adaptive-emerald-700-300" } : undefined); | 634 | const recede = shouldRecedeThreadRow({ status, selected }); |
| 598 | // The timestamp is precomputed on the list item (same stamps the settled | 635 | // The timestamp is precomputed on the list item (same stamps the settled |
| 599 | // tail sorts by) so a minute tick only re-renders rows that draw it. | 636 | // tail sorts by) so a minute tick only re-renders rows that draw it. |
| 600 | const timeLabel = props.timeLabel; | 637 | const timeLabel = props.timeLabel; |
| ... | @@ -959,21 +996,44 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { | ... | @@ -959,21 +996,44 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { |
| 959 | type="monochrome" | 996 | type="monochrome" |
| 960 | /> | 997 | /> |
| 961 | ) : null} | 998 | ) : null} |
| 962 | <Text | 999 | {statusLabel ? ( |
| 963 | className={cn( | 1000 | <View className="flex-row items-center gap-1"> |
| 964 | "text-xs tabular-nums", | 1001 | <SymbolView |
| 965 | statusLabel?.className ?? | 1002 | name={statusLabel.icon} |
| 966 | (selected | 1003 | size={13} |
| | 1004 | tintColorClassName={ |
| | 1005 | selected ? selectedThreadRowColors.iconTintClassName : statusLabel.iconTintClassName |
| | 1006 | } |
| | 1007 | type="monochrome" |
| | 1008 | weight="semibold" |
| | 1009 | /> |
| | 1010 | <Text |
| | 1011 | className={cn( |
| | 1012 | "text-xs font-t3-bold", |
| | 1013 | selected ? selectedThreadRowColors.foregroundClassName : statusLabel.className, |
| | 1014 | )} |
| | 1015 | > |
| | 1016 | {statusLabel.label} |
| | 1017 | </Text> |
| | 1018 | </View> |
| | 1019 | ) : ( |
| | 1020 | <Text |
| | 1021 | className={cn( |
| | 1022 | "text-xs tabular-nums", |
| | 1023 | selected |
| 967 | ? selectedThreadRowColors.foregroundClassName | 1024 | ? selectedThreadRowColors.foregroundClassName |
| 968 | : rowAppearance.tertiaryForegroundClassName), | 1025 | : rowAppearance.tertiaryForegroundClassName, |
| 969 | )} | 1026 | )} |
| 970 | > | 1027 | > |
| 971 | {statusLabel?.label ?? timeLabel} | 1028 | {timeLabel} |
| 972 | </Text> | 1029 | </Text> |
| | 1030 | )} |
| 973 | </View> | 1031 | </View> |
| 974 | <Text | 1032 | <Text |
| 975 | className={cn( | 1033 | className={cn( |
| 976 | "mt-1 text-base font-t3-medium", | 1034 | "mt-1 text-base", |
| | 1035 | // Background work recedes to regular weight, matching web. |
| | 1036 | !recede && "font-t3-medium", |
| 977 | selected | 1037 | selected |
| 978 | ? selectedThreadRowColors.foregroundClassName | 1038 | ? selectedThreadRowColors.foregroundClassName |
| 979 | : rowAppearance.foregroundClassName, | 1039 | : rowAppearance.foregroundClassName, |
| ... | @@ -1115,6 +1175,14 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { | ... | @@ -1115,6 +1175,14 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { |
| 1115 | </> | 1175 | </> |
| 1116 | ); | 1176 | ); |
| 1117 | | 1177 | |
| | 1178 | // Background work fades as a whole, status label included, so it takes |
| | 1179 | // less attention than rows that need a human (input, approval, done). |
| | 1180 | const fadedCardContent = recede ? ( |
| | 1181 | <View style={{ opacity: 0.55 }}>{cardContent}</View> |
| | 1182 | ) : ( |
| | 1183 | cardContent |
| | 1184 | ); |
| | 1185 | |
| 1118 | const rowContent = (close: () => void) => | 1186 | const rowContent = (close: () => void) => |
| 1119 | variant === "card" ? ( | 1187 | variant === "card" ? ( |
| 1120 | <RowPressable | 1188 | <RowPressable |
| ... | @@ -1135,14 +1203,14 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { | ... | @@ -1135,14 +1203,14 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { |
| 1135 | style={rowAppearance.cardStyle} | 1203 | style={rowAppearance.cardStyle} |
| 1136 | > | 1204 | > |
| 1137 | {sidebarPane ? ( | 1205 | {sidebarPane ? ( |
| 1138 | cardContent | 1206 | fadedCardContent |
| 1139 | ) : ( | 1207 | ) : ( |
| 1140 | /* Flat native list rows: no tonal containers — colored status | 1208 | /* Flat native list rows: no tonal containers — colored status |
| 1141 | labels and text hierarchy carry state, an inset hairline | 1209 | labels and text hierarchy carry state, an inset hairline |
| 1142 | separates rows. The opaque screen background stays so swipe | 1210 | separates rows. The opaque screen background stays so swipe |
| 1143 | actions reveal behind the row. */ | 1211 | actions reveal behind the row. */ |
| 1144 | <View> | 1212 | <View> |
| 1145 | <View className={THREAD_LIST_V2_ROW_CONTENT_CLASS_NAME}>{cardContent}</View> | 1213 | <View className={THREAD_LIST_V2_ROW_CONTENT_CLASS_NAME}>{fadedCardContent}</View> |
| 1146 | {THREAD_LIST_V2_ROW_DIVIDERS && props.showTrailingDivider !== false ? ( | 1214 | {THREAD_LIST_V2_ROW_DIVIDERS && props.showTrailingDivider !== false ? ( |
| 1147 | <View className="ml-5 h-px bg-border-subtle" /> | 1215 | <View className="ml-5 h-px bg-border-subtle" /> |
| 1148 | ) : null} | 1216 | ) : null} |