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
2 changes: 0 additions & 2 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ const clientSettings: ClientSettings = {
planModeEnabled: false,
showSkillsInSlashMenu: false,
providerModelPreferences: {},
sidebarAutoSettleAfterDays: 3,
sidebarAutoSettleOnMerge: true,
sidebarProjectGroupingMode: "repository_path",
sidebarProjectGroupingOverrides: {
"environment-1:/tmp/project-a": "separate",
Expand Down
46 changes: 3 additions & 43 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ import {
buildThreadListV2ListItems,
THREAD_LIST_V2_SETTLED_INITIAL_COUNT,
THREAD_LIST_V2_SETTLED_PAGE_COUNT,
type ThreadListV2ChangeRequestState,
type ThreadListV2ListItem,
} from "../threads/threadListV2";
import { useThreadListV2ShelfPreferences } from "../threads/use-thread-list-v2-shelf-preferences";
Expand Down Expand Up @@ -209,9 +208,6 @@ export function HomeScreen(props: HomeScreenProps) {
>(() => new Map());
const preferencesResult = useAtomValue(mobilePreferencesAtom);
const threadListV2Enabled = useThreadListV2Enabled();
const autoSettleOnMerge =
!AsyncResult.isSuccess(preferencesResult) ||
preferencesResult.value.autoSettleOnMerge !== false;
const savePreferences = useAtomSet(updateMobilePreferencesAtom);
const openSwipeableRef = useRef<SwipeableMethods | null>(null);
const listRef = useRef<LegendListRef | null>(null);
Expand Down Expand Up @@ -488,33 +484,6 @@ export function HomeScreen(props: HomeScreenProps) {
// Settled threads stay in the live shell stream (settled ≠ archived), so
// the partition works directly off live shells — no snapshot merging or
// optimistic holds.
// PR states stream in per-row. The next partition applies the configured
// merge rule and the always-on close rule, matching web.
const [changeRequestByKey, setChangeRequestByKey] = useState<
ReadonlyMap<string, ThreadListV2ChangeRequestState>
>(() => new Map());
const handleChangeRequestState = useCallback(
(threadKey: string, changeRequest: ThreadListV2ChangeRequestState | null) => {
setChangeRequestByKey((current) => {
const existing = current.get(threadKey) ?? null;
if (
(existing?.state ?? null) === (changeRequest?.state ?? null) &&
(existing?.updatedAt ?? null) === (changeRequest?.updatedAt ?? null) &&
(existing?.linkedPullRequestKey ?? null) === (changeRequest?.linkedPullRequestKey ?? null)
) {
return current;
}
const next = new Map(current);
if (changeRequest === null) {
next.delete(threadKey);
} else {
next.set(threadKey, changeRequest);
}
return next;
});
},
[],
);
const handleSettleThread = useCallback(
(thread: EnvironmentThreadShell) => {
void props.onSettleThread(thread);
Expand Down Expand Up @@ -581,19 +550,16 @@ export function HomeScreen(props: HomeScreenProps) {
toggleSettledShelf,
toggleSnoozedShelf,
} = useThreadListV2ShelfPreferences();
// now is quantized to the minute and ticks so the inactivity auto-settle
// boundary is actually crossed while the app stays open (mirrors web);
// without a clock dependency the partition memoizes a frozen "now".
// A minute clock lets a queued turn leave its short adoption grace period
// while the app stays open.
const [nowMinute, setNowMinute] = useState(() => new Date().toISOString().slice(0, 16));
// Snooze wake times are second-precise; a counter bumped exactly at the
// next wake boundary re-runs the partition with a fresh clock so a woken
// thread reappears immediately instead of on the next minute tick.
const [snoozeWakeTick, bumpSnoozeWakeTick] = useState(0);
useEffect(() => {
if (!threadListV2Enabled) return;
// Refresh immediately on enable: the mount-time value can be hours old
// by the time the beta is switched on, which would misclassify the
// inactivity auto-settle boundary until the first tick.
// Refresh immediately because the mount-time value can be hours old.
setNowMinute(new Date().toISOString().slice(0, 16));
const id = setInterval(() => setNowMinute(new Date().toISOString().slice(0, 16)), 60_000);
return () => clearInterval(id);
Expand Down Expand Up @@ -679,8 +645,6 @@ export function HomeScreen(props: HomeScreenProps) {
projectRefs: v2ScopedProjectGroup === null ? null : v2ScopedProjectGroup.projectRefs,
searchQuery: props.searchQuery,
matchedThreadKeys,
changeRequestByKey,
autoSettleOnMerge,
settlementEnvironmentIds,
snoozeEnvironmentIds,
settledLimit: settledVisibleCount,
Expand All @@ -691,8 +655,6 @@ export function HomeScreen(props: HomeScreenProps) {
selectedThreadKey: null,
});
}, [
changeRequestByKey,
autoSettleOnMerge,
nowMinute,
snoozeWakeTick,
snoozedShelfExpanded,
Expand Down Expand Up @@ -864,7 +826,6 @@ export function HomeScreen(props: HomeScreenProps) {
onPinThread={handlePinThread}
onUnpinThread={handleUnpinThread}
onMovePinnedThread={handleMovePinnedThread}
onChangeRequestState={handleChangeRequestState}
projectCwd={
projectCwdByKey.get(scopedProjectKey(thread.environmentId, thread.projectId)) ?? null
}
Expand All @@ -874,7 +835,6 @@ export function HomeScreen(props: HomeScreenProps) {
);
},
[
handleChangeRequestState,
handleDeleteThread,
arrangedPinnedKeys,
handleMovePinnedThread,
Expand Down
4 changes: 1 addition & 3 deletions apps/mobile/src/features/home/useThreadListActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,7 @@ function useThreadActionExecutor(
}
const result =
action === "unsettle"
? // reason "user" pins the thread active: auto-settle stays
// suppressed until real activity clears the pin server-side.
await unsettleMutation({
? await unsettleMutation({
environmentId: thread.environmentId,
input: { threadId: thread.id, reason: "user" },
})
Expand Down
12 changes: 0 additions & 12 deletions apps/mobile/src/features/settings/SettingsRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,21 +217,9 @@ function EnvironmentPrivacySettingsSection({
}

function GeneralSettingsSection() {
const preferencesResult = useAtomValue(mobilePreferencesAtom);
const savePreferences = useAtomSet(updateMobilePreferencesAtom);
const autoSettleOnMerge =
!AsyncResult.isSuccess(preferencesResult) ||
preferencesResult.value.autoSettleOnMerge !== false;

return (
<SettingsSection title="General">
<SettingsRow icon="folder" label="Project Grouping" target="SettingsProjectGrouping" />
<SettingsSwitchRow
icon="arrow.triangle.branch"
label="Auto-settle merged threads"
value={autoSettleOnMerge}
onValueChange={(value) => savePreferences({ autoSettleOnMerge: value })}
/>
<SettingsRow icon="chart.bar.xaxis" label="Usage" target="SettingsUsage" />
</SettingsSection>
Comment thread
greptile-apps[bot] marked this conversation as resolved.
);
Expand Down
49 changes: 3 additions & 46 deletions apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import { LegendList } from "@legendapp/list/react-native";
import type { MenuAction } from "@react-native-menu/menu";
import { useAtomValue } from "@effect/atom-react";
import { AsyncResult } from "effect/unstable/reactivity";
import type { EnvironmentId } from "@t3tools/contracts";
import { sortPinnedThreadsByOrderKey } from "@t3tools/client-runtime/state/thread-sort";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
Expand All @@ -29,7 +28,6 @@ import { NativeStackScreenOptions } from "../../native/StackHeader";
import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities";
import { useThemeColor } from "../../lib/useThemeColor";
import { useProjects, useThreadShells } from "../../state/entities";
import { mobilePreferencesAtom } from "../../state/preferences";
import { useThreadSearch } from "../../state/queries";
import { useThreadListV2Enabled } from "./use-thread-list-v2-enabled";
import { useThreadListV2ShelfPreferences } from "./use-thread-list-v2-shelf-preferences";
Expand Down Expand Up @@ -83,7 +81,6 @@ import {
buildThreadListV2ListItems,
THREAD_LIST_V2_SETTLED_INITIAL_COUNT,
THREAD_LIST_V2_SETTLED_PAGE_COUNT,
type ThreadListV2ChangeRequestState,
type ThreadListV2ListItem,
} from "./threadListV2";

Expand Down Expand Up @@ -173,10 +170,6 @@ function ThreadNavigationSidebarPane(
regenerateThreadTitle,
} = useThreadListActions();
const threadListV2Enabled = useThreadListV2Enabled();
const preferencesResult = useAtomValue(mobilePreferencesAtom);
const autoSettleOnMerge =
!AsyncResult.isSuccess(preferencesResult) ||
preferencesResult.value.autoSettleOnMerge !== false;
const pendingTasks = usePendingNewTasks();
const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions();
const environments = useMemo(
Expand Down Expand Up @@ -374,33 +367,6 @@ function ThreadNavigationSidebarPane(

// Thread List v2 (beta) support — same model as the compact Home list
// (HomeScreen.tsx): flat creation-order card block + settled recency tail.
// PR states stream in per-row. The next partition applies the configured
// merge rule and the always-on close rule.
const [changeRequestByKey, setChangeRequestByKey] = useState<
ReadonlyMap<string, ThreadListV2ChangeRequestState>
>(() => new Map());
const handleChangeRequestState = useCallback(
(threadKey: string, changeRequest: ThreadListV2ChangeRequestState | null) => {
setChangeRequestByKey((current) => {
const existing = current.get(threadKey) ?? null;
if (
(existing?.state ?? null) === (changeRequest?.state ?? null) &&
(existing?.updatedAt ?? null) === (changeRequest?.updatedAt ?? null) &&
(existing?.linkedPullRequestKey ?? null) === (changeRequest?.linkedPullRequestKey ?? null)
) {
return current;
}
const next = new Map(current);
if (changeRequest === null) {
next.delete(threadKey);
} else {
next.set(threadKey, changeRequest);
}
return next;
});
},
[],
);
// The settled tail renders in pages; expansion resets when the filter
// context changes so environment/search flips never inherit a deep page.
const [settledVisibleCount, setSettledVisibleCount] = useState(
Expand All @@ -423,19 +389,16 @@ function ThreadNavigationSidebarPane(
toggleSettledShelf,
toggleSnoozedShelf,
} = useThreadListV2ShelfPreferences();
// now ticks per minute so the inactivity auto-settle boundary is actually
// crossed while the pane stays open; without a clock dependency the
// partition memoizes a frozen "now".
// A minute clock lets a queued turn leave its short adoption grace period
// while the pane stays open.
const [nowMinute, setNowMinute] = useState(() => new Date().toISOString().slice(0, 16));
// Snooze wake times are second-precise; a counter bumped exactly at the
// next wake boundary re-runs the partition with a fresh clock so a woken
// thread reappears immediately instead of on the next minute tick.
const [snoozeWakeTick, bumpSnoozeWakeTick] = useState(0);
useEffect(() => {
if (!threadListV2Enabled) return;
// Refresh immediately on enable: the mount-time value can be hours old
// by the time the beta is switched on, which would misclassify the
// inactivity auto-settle boundary until the first tick.
// Refresh immediately because the mount-time value can be hours old.
setNowMinute(new Date().toISOString().slice(0, 16));
const id = setInterval(() => setNowMinute(new Date().toISOString().slice(0, 16)), 60_000);
return () => clearInterval(id);
Expand Down Expand Up @@ -518,8 +481,6 @@ function ThreadNavigationSidebarPane(
projectRefs: selectedProjectScope === null ? null : selectedProjectScope.projectRefs,
searchQuery: props.searchQuery,
matchedThreadKeys,
changeRequestByKey,
autoSettleOnMerge,
settlementEnvironmentIds,
snoozeEnvironmentIds,
settledLimit: settledVisibleCount,
Expand All @@ -530,8 +491,6 @@ function ThreadNavigationSidebarPane(
selectedThreadKey: props.selectedThreadKey ?? null,
});
}, [
changeRequestByKey,
autoSettleOnMerge,
nowMinute,
snoozeWakeTick,
snoozedShelfExpanded,
Expand Down Expand Up @@ -944,7 +903,6 @@ function ThreadNavigationSidebarPane(
onPinThread={pinThread}
onUnpinThread={unpinThread}
onMovePinnedThread={movePinnedThread}
onChangeRequestState={handleChangeRequestState}
projectCwd={projectCwdByKey.get(scopeKey) ?? null}
onSwipeableClose={handleSwipeableClose}
onSwipeableWillOpen={handleSwipeableWillOpen}
Expand Down Expand Up @@ -1071,7 +1029,6 @@ function ThreadNavigationSidebarPane(
arrangedPinnedKeys,
confirmDeletePendingTask,
confirmDeleteThread,
handleChangeRequestState,
handleSelectThread,
handleSwipeableClose,
handleSwipeableWillOpen,
Expand Down
26 changes: 1 addition & 25 deletions apps/mobile/src/features/threads/thread-list-v2-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,10 @@ import { ThreadSwipeable } from "../home/thread-swipe-actions";
import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
import { buildThreadTitleRegenerationMenuItems } from "./thread-title-regeneration-menu";
import {
resolveThreadListV2ChangeRequestState,
resolveThreadListV2SnoozeMenuSelection,
resolveThreadListV2SnoozeGateExpiryMs,
resolveThreadListV2Status,
resolveThreadListV2SwipeActions,
type ThreadListV2ChangeRequestState,
type ThreadListV2Status,
} from "./threadListV2";
import { ThreadSearchMatchExcerpt } from "./thread-search-match";
Expand Down Expand Up @@ -371,12 +369,6 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
readonly canMovePinnedDown?: boolean;
readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void;
readonly onSwipeableClose: (methods: SwipeableMethods) => void;
/** Reports this row's live PR (state + last activity) for the partition's
merge and close rules. Mirrors web's onChangeRequestState. */
readonly onChangeRequestState?: (
threadKey: string,
changeRequest: ThreadListV2ChangeRequestState | null,
) => void;
readonly projectCwd?: string | null;
readonly searchMatch?: EnvironmentThreadSearchMatch;
readonly searchQuery?: string;
Expand All @@ -399,25 +391,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
onPinThread,
onUnpinThread,
onMovePinnedThread,
onChangeRequestState,
} = props;
const snoozedRow = props.snoozed === true;
const pinnedRow = props.pinned === true;

const pr = useThreadPr(thread, props.projectCwd ?? props.project?.workspaceRoot ?? null);
const prState = pr?.state ?? null;
const prUpdatedAt = pr?.updatedAt ?? null;
const threadKey = `${thread.environmentId}:${thread.id}`;
useEffect(() => {
const changeRequest = resolveThreadListV2ChangeRequestState({
linkedPullRequest: thread.linkedPullRequest,
state: prState,
updatedAt: prUpdatedAt,
});
if (changeRequest === undefined) return;
onChangeRequestState?.(threadKey, changeRequest);
}, [onChangeRequestState, prState, prUpdatedAt, thread.linkedPullRequest, threadKey]);

const screenColor = useThemeColor("--color-screen");
const drawerColor = useThemeColor("--color-drawer");
const pressedBackgroundColor = useThemeColor("--color-subtle");
Expand Down Expand Up @@ -454,9 +432,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
);
const handleArchive = useCallback(() => onArchiveThread(thread), [onArchiveThread, thread]);

// Swipe: the v2 primary action is the lifecycle transition. Every settled
// row can un-settle — explicit settles clear the override, auto-settled
// rows get pinned active until real activity clears the pin.
// Swipe uses the primary lifecycle transition for the row.
const canUnsettle = variant === "slim";
const [snoozeGateTick, bumpSnoozeGateTick] = useState(0);
const snoozeGateExpiryMs = props.snoozeSupported
Expand Down
Loading
Loading