| ... | ... | @@ -25,7 +25,7 @@ import { Alert, Pressable, useWindowDimensions, View } from "react-native"; |
| 25 | 25 | import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; |
| 26 | 26 | |
| 27 | 27 | import type { ThreadListProvider } from "../../state/thread-list-environments"; |
| 28 | | import { SymbolView } from "../../components/AppSymbol"; |
| 28 | import { SymbolView, type AppSymbolName } from "../../components/AppSymbol"; |
| 29 | 29 | import { AppText as Text } from "../../components/AppText"; |
| 30 | 30 | import { ControlPillMenu } from "../../components/ControlPill"; |
| 31 | 31 | import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol"; |
| ... | ... | @@ -51,6 +51,7 @@ import { |
| 51 | 51 | type ThreadListV2Status, |
| 52 | 52 | } from "./threadListV2"; |
| 53 | 53 | import { QueuedMessageIcon } from "./queued-message-icon"; |
| 54 | import { shouldRecedeThreadRow } from "./thread-row-emphasis"; |
| 54 | 55 | import { ThreadSearchMatchExcerpt } from "./thread-search-match"; |
| 55 | 56 | |
| 56 | 57 | /** |
| ... | ... | @@ -63,14 +64,50 @@ import { ThreadSearchMatchExcerpt } from "./thread-search-match"; |
| 63 | 64 | // Status hues follow the system-wide convention set by sidebar v1 and the |
| 64 | 65 | // Live Activity/widgets (amber approval, indigo input, sky working) so a |
| 65 | 66 | // thread reads the same color everywhere it surfaces. |
| 66 | | const STATUS_LABEL_BY_STATUS: Partial< |
| 67 | | Record<ThreadListV2Status, { label: string; className: string }> |
| 68 | | > = { |
| 69 | | approval: { label: "Approval", className: "text-warning-foreground" }, |
| 70 | | input: { label: "Input", className: "text-adaptive-indigo-600-300" }, |
| 71 | | working: { label: "Working", className: "text-adaptive-sky-600-400" }, |
| 72 | | failed: { label: "Failed", className: "text-danger-foreground" }, |
| 73 | | limited: { label: "Limited", className: "text-warning-foreground" }, |
| 67 | // Icons match the web sidebar's status glyphs. |
| 68 | interface StatusLabel { |
| 69 | readonly label: string; |
| 70 | readonly icon: AppSymbolName; |
| 71 | readonly className: string; |
| 72 | readonly iconTintClassName: string; |
| 73 | } |
| 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 | 113 | // Menus keep lifecycle and title regeneration together. Archive keeps its |
| ... | ... | @@ -593,8 +630,8 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { |
| 593 | 630 | // A native /goal keeps the agent going across turns until it is met. |
| 594 | 631 | (status === "working" && workingLabel !== undefined && thread.goal?.status === "active" |
| 595 | 632 | ? { ...workingLabel, label: "Goal" } |
| 596 | | : workingLabel) ?? |
| 597 | | (isUnread ? { label: "Done", className: "text-adaptive-emerald-700-300" } : undefined); |
| 633 | : workingLabel) ?? (isUnread ? DONE_STATUS_LABEL : undefined); |
| 634 | const recede = shouldRecedeThreadRow({ status, selected }); |
| 598 | 635 | // The timestamp is precomputed on the list item (same stamps the settled |
| 599 | 636 | // tail sorts by) so a minute tick only re-renders rows that draw it. |
| 600 | 637 | const timeLabel = props.timeLabel; |
| ... | ... | @@ -959,21 +996,44 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { |
| 959 | 996 | type="monochrome" |
| 960 | 997 | /> |
| 961 | 998 | ) : null} |
| 962 | | <Text |
| 963 | | className={cn( |
| 964 | | "text-xs tabular-nums", |
| 965 | | statusLabel?.className ?? |
| 966 | | (selected |
| 999 | {statusLabel ? ( |
| 1000 | <View className="flex-row items-center gap-1"> |
| 1001 | <SymbolView |
| 1002 | name={statusLabel.icon} |
| 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 | 1024 | ? selectedThreadRowColors.foregroundClassName |
| 968 | | : rowAppearance.tertiaryForegroundClassName), |
| 969 | | )} |
| 970 | | > |
| 971 | | {statusLabel?.label ?? timeLabel} |
| 972 | | </Text> |
| 1025 | : rowAppearance.tertiaryForegroundClassName, |
| 1026 | )} |
| 1027 | > |
| 1028 | {timeLabel} |
| 1029 | </Text> |
| 1030 | )} |
| 973 | 1031 | </View> |
| 974 | 1032 | <Text |
| 975 | 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 | 1037 | selected |
| 978 | 1038 | ? selectedThreadRowColors.foregroundClassName |
| 979 | 1039 | : rowAppearance.foregroundClassName, |
| ... | ... | @@ -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 | 1186 | const rowContent = (close: () => void) => |
| 1119 | 1187 | variant === "card" ? ( |
| 1120 | 1188 | <RowPressable |
| ... | ... | @@ -1135,14 +1203,14 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { |
| 1135 | 1203 | style={rowAppearance.cardStyle} |
| 1136 | 1204 | > |
| 1137 | 1205 | {sidebarPane ? ( |
| 1138 | | cardContent |
| 1206 | fadedCardContent |
| 1139 | 1207 | ) : ( |
| 1140 | 1208 | /* Flat native list rows: no tonal containers — colored status |
| 1141 | 1209 | labels and text hierarchy carry state, an inset hairline |
| 1142 | 1210 | separates rows. The opaque screen background stays so swipe |
| 1143 | 1211 | actions reveal behind the row. */ |
| 1144 | 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 | 1214 | {THREAD_LIST_V2_ROW_DIVIDERS && props.showTrailingDivider !== false ? ( |
| 1147 | 1215 | <View className="ml-5 h-px bg-border-subtle" /> |
| 1148 | 1216 | ) : null} |