sidebar history: status indicator on session rows

Sidebar Session rows now mirror the workspace tab descriptor: same
provider icon, same loader spinner, same status dot, same attention
badge. Reuses WorkspaceTabPresentationResolver + WorkspaceTabIcon
directly — no parallel implementation. Status reactivity stays inside
the tab descriptor's own per-agent selector, so selectSidebarSessionSlice
is unchanged and the parent ids hook keeps its reference stability.

Navigation test mocks tab presentation so it doesn't pull the full
workspace stack just to assert click behavior; production uses the
real shared component.
This commit is contained in:
Mohamed Boudra
2026-05-08 19:25:20 +07:00
parent 670e6a6fdd
commit b252cd98d2
2 changed files with 61 additions and 5 deletions
@@ -29,13 +29,21 @@ const {
navigateToPreparedWorkspaceTabMock: vi.fn(),
prepareWorkspaceTabMock: vi.fn(),
testTheme: {
borderRadius: { lg: 8 },
borderRadius: { full: 999, lg: 8 },
colors: {
foreground: "#111111",
foregroundMuted: "#666666",
palette: {
amber: { 500: "#f59e0b", 700: "#b45309" },
blue: { 500: "#3b82f6" },
green: { 500: "#22c55e" },
red: { 500: "#ef4444" },
},
surface0: "#ffffff",
surface2: "#eeeeee",
surfaceSidebarHover: "#f5f5f5",
},
colorScheme: "light",
fontSize: { sm: 14, xs: 12 },
fontWeight: { medium: "500" },
iconSize: { md: 20, sm: 16 },
@@ -61,10 +69,34 @@ vi.mock("@/components/provider-icons", async () => {
};
});
vi.mock("@/screens/workspace/workspace-tab-presentation", async () => {
const ReactModule = await import("react");
return {
WorkspaceTabPresentationResolver: ({ children }: { children: (presentation: {}) => unknown }) =>
children({}),
WorkspaceTabIcon: () =>
ReactModule.createElement("span", { "data-testid": "workspace-tab-icon" }),
};
});
vi.mock("react-native-reanimated", () => ({
default: {
View: "div",
},
Easing: {
linear: vi.fn(),
},
makeMutable: (value: unknown) => ({ value }),
useAnimatedStyle: (factory: () => unknown) => factory(),
withRepeat: (value: unknown) => value,
withTiming: (value: unknown) => value,
}));
vi.mock("react-native-unistyles", () => ({
StyleSheet: {
create: (styles: unknown) => (typeof styles === "function" ? styles(testTheme) : styles),
},
withUnistyles: (Component: unknown) => Component,
useUnistyles: () => ({
theme: testTheme,
rt: {},
+28 -4
View File
@@ -10,12 +10,16 @@ import {
import { useShallow } from "zustand/shallow";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { SidebarAgentListSkeleton } from "@/components/sidebar-agent-list-skeleton";
import { getProviderIcon } from "@/components/provider-icons";
import {
type AggregatedAgentIdEntry,
useAggregatedAgentIds,
useAggregatedAgentsInitialLoad,
} from "@/hooks/use-aggregated-agents";
import {
WorkspaceTabIcon,
WorkspaceTabPresentationResolver,
} from "@/screens/workspace/workspace-tab-presentation";
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
import type { SidebarProjectEntry } from "@/hooks/use-sidebar-workspaces-list";
import { useSessionStore } from "@/stores/session-store";
import { navigateToPreparedWorkspaceTab, prepareWorkspaceTab } from "@/utils/workspace-navigation";
@@ -149,13 +153,20 @@ const SidebarSessionRow = memo(function SidebarSessionRow({
);
const titleStyle = useMemo(() => [styles.title, isHovered && styles.titleHovered], [isHovered]);
const tabDescriptor = useMemo<WorkspaceTabDescriptor>(
() => ({
key: `agent_${id}`,
tabId: `agent_${id}`,
kind: "agent",
target: { kind: "agent", agentId: id },
}),
[id],
);
if (!agent || agent.archivedAt) {
return null;
}
const ProviderIcon = getProviderIcon(agent.provider);
return (
<View onPointerEnter={handlePointerEnter} onPointerLeave={handlePointerLeave}>
<Pressable
@@ -166,7 +177,20 @@ const SidebarSessionRow = memo(function SidebarSessionRow({
testID={`sidebar-session-row-${serverId}-${id}`}
>
<View style={styles.providerIconWrap}>
<ProviderIcon size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
<WorkspaceTabPresentationResolver
tab={tabDescriptor}
serverId={serverId}
workspaceId="sidebar-sessions"
>
{(presentation) => (
<WorkspaceTabIcon
presentation={presentation}
active={isHovered}
size={theme.iconSize.sm}
statusDotBorderColor={isHovered ? theme.colors.surfaceSidebarHover : undefined}
/>
)}
</WorkspaceTabPresentationResolver>
</View>
<Text style={titleStyle} numberOfLines={1}>
{agent.title || "New session"}