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
13 changes: 13 additions & 0 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,15 @@ export function HomeScreen(props: HomeScreenProps) {
}
return supported;
}, [serverConfigs]);
const persistenceEnvironmentIds = useMemo(() => {
const supported = new Set<EnvironmentId>();
for (const [environmentId, config] of serverConfigs) {
if (config.environment.capabilities.threadPersistence === true) {
supported.add(environmentId);
}
}
return supported;
}, [serverConfigs]);
const pinReorderEnvironmentIds = useMemo(() => {
const supported = new Set<EnvironmentId>();
for (const [environmentId, config] of serverConfigs) {
Expand Down Expand Up @@ -848,6 +857,7 @@ export function HomeScreen(props: HomeScreenProps) {
onArchiveThread={props.onArchiveThread}
onRegenerateThreadTitle={handleRegenerateThreadTitle}
titleRegenerationSupported={titleRegenerationEnvironmentIds.has(thread.environmentId)}
persistenceSupported={persistenceEnvironmentIds.has(thread.environmentId)}
settlementSupported={settlementEnvironmentIds.has(thread.environmentId)}
onSettleThread={handleSettleThread}
snoozeSupported={snoozeEnvironmentIds.has(thread.environmentId)}
Expand Down Expand Up @@ -888,6 +898,7 @@ export function HomeScreen(props: HomeScreenProps) {
handleSwipeableWillOpen,
handleUnsettleThread,
pinningEnvironmentIds,
persistenceEnvironmentIds,
pinReorderEnvironmentIds,
projectByKey,
projectCwdByKey,
Expand Down Expand Up @@ -1009,6 +1020,7 @@ export function HomeScreen(props: HomeScreenProps) {
onDeleteThread={props.onDeleteThread}
onRegenerateThreadTitle={handleRegenerateThreadTitle}
titleRegenerationSupported={titleRegenerationEnvironmentIds.has(thread.environmentId)}
persistenceSupported={persistenceEnvironmentIds.has(thread.environmentId)}
onSelectThread={props.onSelectThread}
onSwipeableClose={handleSwipeableClose}
onSwipeableWillOpen={handleSwipeableWillOpen}
Expand Down Expand Up @@ -1040,6 +1052,7 @@ export function HomeScreen(props: HomeScreenProps) {
props.onSelectThread,
props.searchQuery,
props.savedConnectionsById,
persistenceEnvironmentIds,
threadSearchMatchByKey,
titleRegenerationEnvironmentIds,
updateGroupDisplay,
Expand Down
23 changes: 23 additions & 0 deletions apps/mobile/src/features/threads/PersistentThreadIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Svg, { Path, Rect } from "react-native-svg";

export function PersistentThreadIcon(props: { readonly color: string; readonly size?: number }) {
const size = props.size ?? 14;
return (
<Svg
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={props.color}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<Path d="M22 17a2 2 0 0 1-2 2H6.83a2 2 0 0 0-1.42.59l-2.2 2.2A.71.71 0 0 1 2 21.29V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" />
<Rect width={8} height={5} x={8} y={10} rx={1} />
<Path d="M10 10V8a2 2 0 0 1 4 0v2" />
</Svg>
);
}
12 changes: 12 additions & 0 deletions apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,15 @@ function ThreadNavigationSidebarPane(
}
return supported;
}, [serverConfigs]);
const persistenceEnvironmentIds = useMemo(() => {
const supported = new Set<EnvironmentId>();
for (const [environmentId, config] of serverConfigs) {
if (config.environment.capabilities.threadPersistence === true) {
supported.add(environmentId);
}
}
return supported;
}, [serverConfigs]);
const pinReorderEnvironmentIds = useMemo(() => {
const supported = new Set<EnvironmentId>();
for (const [environmentId, config] of serverConfigs) {
Expand Down Expand Up @@ -913,6 +922,7 @@ function ThreadNavigationSidebarPane(
onArchiveThread={archiveThread}
onRegenerateThreadTitle={regenerateThreadTitle}
titleRegenerationSupported={titleRegenerationEnvironmentIds.has(thread.environmentId)}
persistenceSupported={persistenceEnvironmentIds.has(thread.environmentId)}
settlementSupported={settlementEnvironmentIds.has(thread.environmentId)}
onSettleThread={settleThread}
snoozeSupported={snoozeEnvironmentIds.has(thread.environmentId)}
Expand Down Expand Up @@ -1034,6 +1044,7 @@ function ThreadNavigationSidebarPane(
onDeleteThread={confirmDeleteThread}
onRegenerateThreadTitle={regenerateThreadTitle}
titleRegenerationSupported={titleRegenerationEnvironmentIds.has(thread.environmentId)}
persistenceSupported={persistenceEnvironmentIds.has(thread.environmentId)}
onSelectThread={handleSelectThread}
onSwipeableClose={handleSwipeableClose}
onSwipeableWillOpen={handleSwipeableWillOpen}
Expand Down Expand Up @@ -1067,6 +1078,7 @@ function ThreadNavigationSidebarPane(
pinReorderEnvironmentIds,
pinThread,
pinningEnvironmentIds,
persistenceEnvironmentIds,
projectByKey,
projectCwdByKey,
projectTitleByProjectKey,
Expand Down
122 changes: 91 additions & 31 deletions apps/mobile/src/features/threads/thread-list-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ import { buildThreadTitleRegenerationMenuItems } from "./thread-title-regenerati
import { resolveThreadStatus, shouldShowActionWaitingIndicator } from "./threadPresentation";
import { actionRunningPresentation } from "@t3tools/shared/actionResume";
import { ThreadSearchMatchExcerpt } from "./thread-search-match";
import { PersistentThreadIcon } from "./PersistentThreadIcon";
import {
buildThreadPersistenceMenuItems,
persistenceIntentForMenuEvent,
} from "./thread-persistence-menu";

/**
* Shared presentation for the thread lists: the compact (phone) Home list and
Expand Down Expand Up @@ -439,6 +444,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
readonly onDeleteThread: (thread: EnvironmentThreadShell) => void;
readonly onRegenerateThreadTitle: (thread: EnvironmentThreadShell) => void;
readonly titleRegenerationSupported: boolean;
readonly persistenceSupported: boolean;
readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void;
readonly onSwipeableClose: (methods: SwipeableMethods) => void;
readonly simultaneousSwipeGesture?: ComponentProps<
Expand Down Expand Up @@ -473,6 +479,9 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
const abandonWorktreeCleanup = useAtomCommand(threadEnvironment.abandonWorktreeCleanup, {
reportFailure: false,
});
const setThreadPersistence = useAtomCommand(threadEnvironment.setPersistence, {
reportFailure: false,
});
const status = resolveThreadStatus(thread);
const pr = useThreadPr(thread, props.projectCwd);
const timestamp = relativeTime(
Expand Down Expand Up @@ -511,6 +520,22 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
() => onRegenerateThreadTitle(thread),
[onRegenerateThreadTitle, thread],
);
const handlePersistence = useCallback(
async (persistent: boolean) => {
const result = await setThreadPersistence({
environmentId: thread.environmentId,
input: { threadId: thread.id, persistent },
});
if (result._tag === "Failure") {
const error = Cause.squash(result.cause);
Alert.alert(
"Could not update persistent thread",
error instanceof Error ? error.message : "The persistent thread could not be updated.",
);
}
},
[setThreadPersistence, thread.environmentId, thread.id],
);
const handleCancelAction = useCallback(async () => {
if (runningAction === null) return;
const result = await closeTerminal({
Expand Down Expand Up @@ -568,25 +593,36 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
);
}, [abandonWorktreeCleanup, thread.environmentId, thread.id]);
const menuActions = useMemo<MenuAction[]>(
() => [
THREAD_ROW_MENU_ACTIONS[0]!,
...buildThreadTitleRegenerationMenuItems({
supported: props.titleRegenerationSupported,
isRegenerating: thread.titleRegeneration != null,
() =>
buildThreadPersistenceMenuItems({
persistent: thread.persistent === true,
supported: props.persistenceSupported,
actions: [
THREAD_ROW_MENU_ACTIONS[0]!,
...buildThreadTitleRegenerationMenuItems({
supported: props.titleRegenerationSupported,
isRegenerating: thread.titleRegeneration != null,
}),
...(runningAction === null
? []
: [
{
id: "cancel-action",
title: `Cancel ${runningAction.actionName}`,
image: "stop.fill",
attributes: { destructive: true },
} satisfies MenuAction,
]),
THREAD_ROW_MENU_ACTIONS[1]!,
],
}),
...(runningAction === null
? []
: [
{
id: "cancel-action",
title: `Cancel ${runningAction.actionName}`,
image: "stop.fill",
attributes: { destructive: true },
} satisfies MenuAction,
]),
THREAD_ROW_MENU_ACTIONS[1]!,
[
props.persistenceSupported,
props.titleRegenerationSupported,
runningAction,
thread.persistent,
thread.titleRegeneration,
],
[props.titleRegenerationSupported, runningAction, thread.titleRegeneration],
);
const primaryAction = useMemo(
() => ({
Expand All @@ -605,12 +641,15 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
if (nativeEvent.event === "retry-worktree-cleanup") void handleRetryWorktreeCleanup();
if (nativeEvent.event === "keep-worktree") handleKeepWorktree();
if (nativeEvent.event === "delete") handleDelete();
const persistenceIntent = persistenceIntentForMenuEvent(nativeEvent.event);
if (persistenceIntent !== null) void handlePersistence(persistenceIntent);
},
[
handleArchive,
handleCancelAction,
handleDelete,
handleKeepWorktree,
handlePersistence,
handleRegenerateTitle,
handleRetryWorktreeCleanup,
],
Expand Down Expand Up @@ -679,7 +718,9 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
accessibilityHint={
cleanupFailed
? "Thread unavailable. Long-press for worktree recovery actions"
: "Swipe left for archive and delete actions"
: thread.persistent === true
? "Persistent thread. Long-press to disable persistence"
: "Swipe left for archive and delete actions"
}
accessibilityLabel={threadAccessibilityLabel}
accessibilityRole="button"
Expand Down Expand Up @@ -709,9 +750,20 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
}}
>
<View className="flex-row items-center justify-between gap-2">
<Text className="flex-1 text-lg font-t3-bold text-foreground" numberOfLines={1}>
{thread.title}
</Text>
<View className="flex-1 flex-row items-center gap-1.5">
{thread.persistent === true ? (
<PersistentThreadIcon color={String(theme["--color-foreground"])} size={15} />
) : null}
Comment thread
lastobelus marked this conversation as resolved.
<Text
className={cn(
"flex-1 text-lg font-t3-bold text-foreground",
thread.persistent === true && "italic",
)}
numberOfLines={1}
>
{thread.title}
</Text>
</View>
<View className="flex-row items-center gap-2">
{actionStatusIndicator}
{statusPill}
Expand Down Expand Up @@ -769,15 +821,23 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
>
<View className="gap-[3px]">
<View className="flex-row items-center justify-between gap-2">
<Text
className={cn(
"flex-1 text-base font-t3-medium",
selected ? "text-user-bubble-foreground" : "text-foreground",
)}
numberOfLines={1}
>
{thread.title}
</Text>
<View className="flex-1 flex-row items-center gap-1.5">
{thread.persistent === true ? (
<PersistentThreadIcon
color={String(selected ? selectedForegroundColor : theme["--color-foreground"])}
/>
) : null}
<Text
className={cn(
"flex-1 text-base font-t3-medium",
selected ? "text-user-bubble-foreground" : "text-foreground",
thread.persistent === true && "italic",
)}
numberOfLines={1}
>
{thread.title}
</Text>
</View>
<View className="flex-row items-center gap-2">
{actionStatusIndicator}
{statusPill}
Expand Down Expand Up @@ -807,7 +867,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
return (
<ThreadSwipeable
backgroundColor={backgroundColor}
enabled={!cleanupPending && !cleanupFailed}
enabled={!cleanupPending && !cleanupFailed && thread.persistent !== true}
containerStyle={
compact ? undefined : { borderRadius: SIDEBAR_ROW_RADIUS, overflow: "hidden" }
}
Expand Down
Loading