Fix History restore and diff truncation edge cases

This commit is contained in:
Mohamed Boudra
2026-06-18 20:44:57 +07:00
parent b3f81c0bf4
commit 00c57617d5
25 changed files with 717 additions and 127 deletions
+91 -110
View File
@@ -15,14 +15,14 @@ import type { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
import { useIsCompactFormFactor } from "@/constants/layout";
import { formatTimeAgo } from "@/utils/time";
import { shortenPath } from "@/utils/shorten-path";
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
import { useSessionStore } from "@/stores/session-store";
import { Archive } from "lucide-react-native";
import { getProviderIcon } from "@/components/provider-icons";
import { navigateToAgent } from "@/utils/navigate-to-agent";
import type { Agent } from "@/stores/session-store";
import { useArchiveAgent } from "@/hooks/use-archive-agent";
import { useQueryClient } from "@tanstack/react-query";
import { agentHistoryQueryKey } from "@/hooks/agent-history-query-key";
interface AgentListProps {
agents: AggregatedAgent[];
@@ -49,65 +49,6 @@ type FlatListItem =
| { type: "header"; key: string; section: DateSectionKey }
| { type: "agent"; key: string; agent: AggregatedAgent };
function buildHistoricalAgentDetail(agent: AggregatedAgent): Agent {
return {
serverId: agent.serverId,
id: agent.id,
provider: agent.provider,
status: agent.status,
createdAt: agent.createdAt,
updatedAt: agent.lastActivityAt,
lastUserMessageAt: null,
lastActivityAt: agent.lastActivityAt,
capabilities: {
supportsStreaming: false,
supportsSessionPersistence: false,
supportsDynamicModes: false,
supportsMcpServers: false,
supportsReasoningStream: false,
supportsToolInvocations: false,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
runtimeInfo: {
provider: agent.provider,
sessionId: null,
},
title: agent.title,
cwd: agent.cwd,
workspaceId: agent.workspaceId,
model: null,
thinkingOptionId: null,
requiresAttention: agent.requiresAttention,
attentionReason: agent.attentionReason,
attentionTimestamp: agent.attentionTimestamp,
archivedAt: agent.archivedAt,
labels: agent.labels,
parentAgentId: null,
};
}
function rememberArchivedAgentDetail(agent: AggregatedAgent) {
if (!agent.archivedAt) {
return;
}
useSessionStore.getState().setAgentDetails(agent.serverId, (previous) => {
const existing = previous.get(agent.id);
const next = new Map(previous);
next.set(agent.id, {
...buildHistoricalAgentDetail(agent),
...existing,
archivedAt: existing?.archivedAt ?? agent.archivedAt,
cwd: existing?.cwd ?? agent.cwd,
workspaceId: existing?.workspaceId ?? agent.workspaceId,
});
return next;
});
}
function deriveDateSectionKey(lastActivityAt: Date): DateSectionKey {
const now = new Date();
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
@@ -151,23 +92,6 @@ function formatDateSectionLabel(t: TFunction, section: DateSectionKey): string {
}
}
function formatStatusLabel(t: TFunction, status: AggregatedAgent["status"]): string {
switch (status) {
case "initializing":
return t("agentList.status.initializing");
case "idle":
return t("agentList.status.idle");
case "running":
return t("agentList.status.running");
case "error":
return t("agentList.status.error");
case "closed":
return t("agentList.status.closed");
default:
return status;
}
}
function SessionBadge({
label,
icon,
@@ -221,8 +145,9 @@ function SessionRow({
const timeAgo = formatTimeAgo(agent.lastActivityAt);
const agentKey = `${agent.serverId}:${agent.id}`;
const isSelected = selectedAgentId === agentKey;
const statusLabel = formatStatusLabel(t, agent.status);
const projectPath = shortenPath(agent.cwd);
const projectName = agent.projectPlacement?.projectName ?? "";
const branch = agent.projectPlacement?.checkout.currentBranch ?? "";
const workspaceName = agent.projectPlacement?.workspaceName ?? "";
const ProviderIcon = getProviderIcon(agent.provider);
const pendingPermissionCount = agent.pendingPermissionCount ?? 0;
@@ -279,32 +204,61 @@ function SessionRow({
</View>
{isMobile && (
<View style={styles.rowMetaRow}>
<Text style={styles.sessionMetaText} numberOfLines={1}>
{projectPath}
<Text
style={styles.sessionMetaText}
numberOfLines={1}
testID={`agent-row-project-${agent.serverId}-${agent.id}`}
>
{projectName}
</Text>
<Text style={styles.sessionMetaSeparator}>·</Text>
<Text style={styles.sessionMetaText}>{statusLabel}</Text>
<Text
style={styles.sessionMetaText}
numberOfLines={1}
testID={`agent-row-branch-${agent.serverId}-${agent.id}`}
>
{branch}
</Text>
<Text style={styles.sessionMetaSeparator}>·</Text>
<Text
style={styles.sessionMetaText}
numberOfLines={1}
testID={`agent-row-workspace-${agent.serverId}-${agent.id}`}
>
{workspaceName}
</Text>
<Text style={styles.sessionMetaSeparator}>·</Text>
<Text style={styles.sessionMetaText}>{timeAgo}</Text>
{agent.serverLabel ? (
<>
<Text style={styles.sessionMetaSeparator}>·</Text>
<Text style={styles.sessionMetaText} numberOfLines={1}>
{agent.serverLabel}
</Text>
</>
) : null}
</View>
)}
</View>
{!isMobile && (
<>
<Text style={styles.columnMeta} numberOfLines={1}>
{projectPath}
<View style={styles.rowColumns}>
<Text
style={styles.columnMeta}
numberOfLines={1}
testID={`agent-row-project-${agent.serverId}-${agent.id}`}
>
{projectName}
</Text>
<Text style={styles.columnMetaFixed}>{statusLabel}</Text>
<Text style={styles.columnMetaFixed}>{timeAgo}</Text>
</>
<Text
style={styles.columnMeta}
numberOfLines={1}
testID={`agent-row-branch-${agent.serverId}-${agent.id}`}
>
{branch}
</Text>
<Text
style={styles.columnMeta}
numberOfLines={1}
testID={`agent-row-workspace-${agent.serverId}-${agent.id}`}
>
{workspaceName}
</Text>
<Text style={styles.columnMetaFixed} numberOfLines={1}>
{timeAgo}
</Text>
</View>
)}
{isMobile && showAttentionIndicator && agent.requiresAttention ? (
<View style={styles.rowTrailing}>
@@ -330,6 +284,7 @@ export function AgentList({
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
const isMobile = useIsCompactFormFactor();
const { archiveAgent } = useArchiveAgent();
const queryClient = useQueryClient();
const actionClient = useSessionStore((state) =>
actionAgent?.serverId ? (state.sessions[actionAgent.serverId]?.client ?? null) : null,
@@ -346,17 +301,35 @@ export function AgentList({
const serverId = agent.serverId;
const agentId = agent.id;
const openAgent = () => {
onAgentSelect?.();
navigateToAgent({
serverId,
agentId,
workspaceId: agent.workspaceId,
pin: false,
});
};
onAgentSelect?.();
if (agent.archivedAt) {
const client = useSessionStore.getState().sessions[serverId]?.client ?? null;
if (client) {
void client
.refreshAgent(agentId)
.then(() => {
openAgent();
return queryClient.invalidateQueries({
queryKey: agentHistoryQueryKey(serverId),
});
})
.catch(() => {});
}
return;
}
rememberArchivedAgentDetail(agent);
navigateToAgent({
serverId,
agentId,
pin: Boolean(agent.archivedAt),
});
openAgent();
},
[isActionSheetVisible, onAgentSelect],
[isActionSheetVisible, onAgentSelect, queryClient],
);
const handleAgentLongPress = useCallback(
@@ -561,12 +534,14 @@ const styles = StyleSheet.create((theme) => ({
rowContent: {
flex: 1,
minWidth: 0,
overflow: "hidden",
},
rowTitleRow: {
flexDirection: "row",
alignItems: "center",
flexWrap: "wrap",
flexWrap: "nowrap",
gap: theme.spacing[2],
overflow: "hidden",
},
providerIconWrap: {
width: theme.iconSize.md,
@@ -594,6 +569,7 @@ const styles = StyleSheet.create((theme) => ({
},
sessionTitle: {
flexShrink: 1,
minWidth: 0,
fontSize: theme.fontSize.sm,
fontWeight: "400",
color: theme.colors.foreground,
@@ -612,13 +588,17 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted,
opacity: 0.7,
},
rowColumns: {
flexDirection: "row",
alignItems: "center",
flexShrink: 0,
gap: theme.spacing[3],
},
columnMeta: {
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
flexShrink: 1,
minWidth: 60,
maxWidth: 200,
marginLeft: theme.spacing[4],
flexShrink: 0,
width: 132,
},
columnMetaFixed: {
fontSize: theme.fontSize.sm,
@@ -630,6 +610,7 @@ const styles = StyleSheet.create((theme) => ({
badge: {
flexDirection: "row",
alignItems: "center",
flexShrink: 0,
gap: theme.spacing[1],
paddingHorizontal: theme.spacing[2],
paddingVertical: theme.spacing[1],