Skip to content

Commit 8d84666

Browse files
t3dotggclaude
andauthored
feat(mobile): beta Working section hides busy threads until they need you (#15346)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent a78760e commit 8d84666

16 files changed

Lines changed: 587 additions & 103 deletions

‎apps/mobile/src/features/home/HomeScreen.tsx‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@ import {
4444
ThreadListV2SettledShelfHeader,
4545
ThreadListV2ShowMoreRow,
4646
ThreadListV2SnoozedShelfHeader,
47+
ThreadListV2WorkingShelfHeader,
4748
} from "../threads/thread-list-v2-items";
4849
import { useThreadRowProviderInstanceResolver } from "../threads/thread-provider-instance";
4950
import {
5051
buildThreadListV2Items,
5152
getThreadListV2OrderedSection,
5253
buildThreadListV2ListItems,
5354
threadListV2ListItemsAreEqual,
55+
threadListInboxReturns,
5456
THREAD_LIST_V2_SETTLED_INITIAL_COUNT,
5557
THREAD_LIST_V2_SETTLED_PAGE_COUNT,
5658
type ThreadListV2ListItem,
@@ -466,8 +468,11 @@ export function HomeScreen(props: HomeScreenProps) {
466468
loaded: shelfPreferencesLoaded,
467469
settledShelfExpanded,
468470
snoozedShelfExpanded,
471+
workingShelfEnabled,
472+
workingShelfExpanded,
469473
toggleSettledShelf,
470474
toggleSnoozedShelf,
475+
toggleWorkingShelf,
471476
} = useThreadListV2ShelfPreferences();
472477
// The queued-start and snooze helpers need a clock while the list stays open.
473478
const [nowMinute, setNowMinute] = useState(() => new Date().toISOString().slice(0, 16));
@@ -520,8 +525,13 @@ export function HomeScreen(props: HomeScreenProps) {
520525
queuedThreadKeys,
521526
}),
522527
});
523-
return new Map([...sectionAvailability("pinned"), ...sectionAvailability("active")]);
528+
// The Working beta orders the inbox by time, so only pins can move.
529+
return new Map([
530+
...sectionAvailability("pinned"),
531+
...(workingShelfEnabled ? [] : sectionAvailability("active")),
532+
]);
524533
}, [
534+
workingShelfEnabled,
525535
pinReorderEnvironmentIds,
526536
activeReorderEnvironmentIds,
527537
props.threads,
@@ -533,6 +543,7 @@ export function HomeScreen(props: HomeScreenProps) {
533543
snoozeWakeTick,
534544
]);
535545
const threadListV2Layout = useMemo(() => {
546+
threadListInboxReturns.observe(workingShelfEnabled ? props.threads : null);
536547
// Settled threads are live shells; archived threads keep their original
537548
// "hidden from lists" meaning.
538549
return buildThreadListV2Items({
@@ -547,11 +558,16 @@ export function HomeScreen(props: HomeScreenProps) {
547558
queuedThreadKeys,
548559
settledLimit: settledVisibleCount,
549560
now: new Date().toISOString(),
561+
workingShelfEnabled,
562+
workingShelfExpanded,
563+
inboxReturnAt: threadListInboxReturns.returnedAt,
550564
snoozedShelfExpanded,
551565
settledShelfExpanded,
552566
selectedThreadKey: null,
553567
});
554568
}, [
569+
workingShelfEnabled,
570+
workingShelfExpanded,
555571
pendingOrder,
556572
queuedThreadKeys,
557573
nowMinute,
@@ -606,6 +622,9 @@ export function HomeScreen(props: HomeScreenProps) {
606622
buildThreadListV2ListItems({
607623
items: threadListV2Layout.items,
608624
pendingTasks: v2PendingTasks,
625+
workingCount: threadListV2Layout.workingCount,
626+
workingShelfExpanded,
627+
workingShelfHeaderIndex: threadListV2Layout.workingShelfHeaderIndex,
609628
snoozedCount: threadListV2Layout.snoozedCount,
610629
snoozedShelfExpanded,
611630
snoozedShelfHeaderIndex: threadListV2Layout.snoozedShelfHeaderIndex,
@@ -628,6 +647,7 @@ export function HomeScreen(props: HomeScreenProps) {
628647
snoozeEnvironmentIds,
629648
threadListV2Layout,
630649
v2PendingTasks,
650+
workingShelfExpanded,
631651
],
632652
);
633653

@@ -662,6 +682,16 @@ export function HomeScreen(props: HomeScreenProps) {
662682
/>
663683
);
664684
}
685+
if (item.type === "v2-working-shelf") {
686+
return (
687+
<ThreadListV2WorkingShelfHeader
688+
count={item.count}
689+
disabled={item.disabled}
690+
expanded={item.expanded}
691+
onToggle={toggleWorkingShelf}
692+
/>
693+
);
694+
}
665695
if (item.type === "v2-snoozed-shelf") {
666696
return (
667697
<ThreadListV2SnoozedShelfHeader
@@ -730,7 +760,7 @@ export function HomeScreen(props: HomeScreenProps) {
730760
reorderSupported={
731761
item.item.pinned
732762
? pinReorderEnvironmentIds.has(thread.environmentId)
733-
: activeReorderEnvironmentIds.has(thread.environmentId)
763+
: !workingShelfEnabled && activeReorderEnvironmentIds.has(thread.environmentId)
734764
}
735765
canMoveUp={item.canMoveUp}
736766
canMoveDown={item.canMoveDown}
@@ -782,8 +812,10 @@ export function HomeScreen(props: HomeScreenProps) {
782812
titleRegenerationEnvironmentIds,
783813
toggleSettledShelf,
784814
toggleSnoozedShelf,
815+
toggleWorkingShelf,
785816
v2ProjectTitleByProjectKey,
786817
props.searchQuery,
818+
workingShelfEnabled,
787819
],
788820
);
789821
const v2KeyExtractor = useCallback((item: ThreadListV2ListItem) => item.key, []);
@@ -800,6 +832,8 @@ export function HomeScreen(props: HomeScreenProps) {
800832
savedConnectionsById: props.savedConnectionsById,
801833
searchQuery: props.searchQuery,
802834
threadSearchMatchByKey,
835+
// Rows read it for their reorder menu items.
836+
workingShelfEnabled,
803837
}),
804838
[
805839
projectByKey,
@@ -808,6 +842,7 @@ export function HomeScreen(props: HomeScreenProps) {
808842
listEnvironments,
809843
threadSearchMatchByKey,
810844
v2ProjectTitleByProjectKey,
845+
workingShelfEnabled,
811846
],
812847
);
813848

‎apps/mobile/src/features/settings/SettingsThreadsRouteScreen.tsx‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export function SettingsThreadsRouteScreen() {
4545
contentContainerStyle={{ paddingBottom: Math.max(insets.bottom, 18) + 18 }}
4646
>
4747
<AutoSettleSettingsRows />
48+
<BetaSettingsSection />
4849
<LegacySettingsSection />
4950
</ScrollView>
5051
</SettingsScreen>
@@ -239,6 +240,35 @@ function AutoSettleSettingsRows() {
239240
);
240241
}
241242

243+
/**
244+
* Device-local beta toggles, the counterpart of web's Working section (beta)
245+
* in Settings → General.
246+
*/
247+
function BetaSettingsSection() {
248+
const savePreferences = useAtomSet(updateMobilePreferencesAtom);
249+
const preferences = useAtomValue(mobilePreferencesAtom);
250+
const workingShelfEnabled =
251+
AsyncResult.isSuccess(preferences) && preferences.value.workingShelfEnabled === true;
252+
253+
return (
254+
<View className="gap-3">
255+
<SettingsSection title="Beta">
256+
<SettingsSwitchRow
257+
icon="bolt.circle"
258+
label="Working section"
259+
value={workingShelfEnabled}
260+
onValueChange={(value) => savePreferences({ workingShelfEnabled: value })}
261+
/>
262+
</SettingsSection>
263+
<Text className="px-2 text-sm text-foreground-muted">
264+
Fold working and monitoring threads into a Working section. They return to the top of the
265+
list when they need you. While this is on, active threads are ordered by time and cannot be
266+
moved.
267+
</Text>
268+
</View>
269+
);
270+
}
271+
242272
/**
243273
* Device-local legacy toggles. Mobile has no client-settings sync, so this is
244274
* the counterpart of web's Settings → General → Legacy features backed by

‎apps/mobile/src/features/threads/ThreadArrangementSheet.tsx‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { appAtomRegistry } from "../../state/atom-registry";
22
import { useAtomValue } from "@effect/atom-react";
33
import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
44
import { effectiveSnoozed } from "@t3tools/client-runtime/state/thread-settled";
5+
import { sortInboxThreadsByReturn } from "@t3tools/client-runtime/state/thread-inbox";
56
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
67
import { Animated, FlatList, Modal, Pressable, View } from "react-native";
78
import { Gesture, GestureDetector, GestureHandlerRootView } from "react-native-gesture-handler";
@@ -26,7 +27,8 @@ import {
2627
threadDragAction,
2728
type ThreadMoveDestination,
2829
} from "./threadOrder";
29-
import { getThreadListV2OrderedSection } from "./threadListV2";
30+
import { getThreadListV2OrderedSection, threadListInboxReturns } from "./threadListV2";
31+
import { useThreadListV2ShelfPreferences } from "./use-thread-list-v2-shelf-preferences";
3032

3133
const ROW_HEIGHT = 56;
3234
const HEADER_HEIGHT = 48;
@@ -154,6 +156,7 @@ export function ThreadArrangementSheet(props: { onClose: () => void }) {
154156
const pendingOrder = useAtomValue(pendingThreadOrderAtom);
155157
const dropBusy = useAtomValue(threadDropBusyAtom);
156158
const { moveThread } = useThreadListActions();
159+
const { workingShelfEnabled } = useThreadListV2ShelfPreferences();
157160
const [now, setNow] = useState(() => new Date().toISOString());
158161
const [expanded, setExpanded] = useState({ snoozed: false, settled: false });
159162
useEffect(() => {
@@ -195,31 +198,36 @@ export function ThreadArrangementSheet(props: { onClose: () => void }) {
195198
);
196199
return {
197200
pinned,
198-
active,
201+
// The Working beta orders the inbox by time; show that order here too.
202+
active: workingShelfEnabled
203+
? sortInboxThreadsByReturn(active, threadListInboxReturns.returnedAt)
204+
: active,
199205
snoozed: parked.filter((thread) => effectiveSnoozed(thread, { now })),
200206
settled: parked.filter((thread) => !effectiveSnoozed(thread, { now })),
201207
};
202-
}, [threads, configs, now, queuedThreadKeys, pendingOrder]);
208+
}, [threads, configs, now, queuedThreadKeys, pendingOrder, workingShelfEnabled]);
203209
const planners = useMemo(() => {
204210
const planner = (section: "pinned" | "active") =>
205211
createThreadMovePlanner({
206212
ordered: sections[section],
207213
allThreads: threads,
208214
section,
215+
// A time-ordered inbox has no slots, so Active takes no drops while
216+
// the Working beta is on. The saved arrangement stays untouched.
209217
reorderableEnvironmentIds: new Set(
210218
[...configs].flatMap(([id, config]) =>
211219
(
212220
section === "pinned"
213221
? config.environment.capabilities.threadPinReorder
214-
: config.environment.capabilities.threadActiveReorder
222+
: !workingShelfEnabled && config.environment.capabilities.threadActiveReorder
215223
)
216224
? [id]
217225
: [],
218226
),
219227
),
220228
});
221229
return { pinned: planner("pinned"), active: planner("active") };
222-
}, [sections, threads, configs]);
230+
}, [sections, threads, configs, workingShelfEnabled]);
223231
const rows = useMemo(() => {
224232
const result: Row[] = [];
225233
let offset = 0;

‎apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
ThreadListV2SettledShelfHeader,
6262
ThreadListV2ShowMoreRow,
6363
ThreadListV2SnoozedShelfHeader,
64+
ThreadListV2WorkingShelfHeader,
6465
} from "./thread-list-v2-items";
6566
import { useThreadRowProviderInstanceResolver } from "./thread-provider-instance";
6667
import {
@@ -69,6 +70,7 @@ import {
6970
buildThreadListV2ListItems,
7071
isThreadListV2ListItem,
7172
threadListV2ListItemsAreEqual,
73+
threadListInboxReturns,
7274
THREAD_LIST_V2_SETTLED_INITIAL_COUNT,
7375
THREAD_LIST_V2_SETTLED_PAGE_COUNT,
7476
type ThreadListV2ListItem,
@@ -291,8 +293,11 @@ function ThreadNavigationSidebarPane(
291293
loaded: shelfPreferencesLoaded,
292294
settledShelfExpanded,
293295
snoozedShelfExpanded,
296+
workingShelfEnabled,
297+
workingShelfExpanded,
294298
toggleSettledShelf,
295299
toggleSnoozedShelf,
300+
toggleWorkingShelf,
296301
} = useThreadListV2ShelfPreferences();
297302
// The queued-start and snooze helpers need a clock while the pane stays open.
298303
const [nowMinute, setNowMinute] = useState(() => new Date().toISOString().slice(0, 16));
@@ -343,8 +348,13 @@ function ThreadNavigationSidebarPane(
343348
queuedThreadKeys,
344349
}),
345350
});
346-
return new Map([...sectionAvailability("pinned"), ...sectionAvailability("active")]);
351+
// The Working beta orders the inbox by time, so only pins can move.
352+
return new Map([
353+
...sectionAvailability("pinned"),
354+
...(workingShelfEnabled ? [] : sectionAvailability("active")),
355+
]);
347356
}, [
357+
workingShelfEnabled,
348358
pinReorderEnvironmentIds,
349359
activeReorderEnvironmentIds,
350360
threads,
@@ -356,6 +366,7 @@ function ThreadNavigationSidebarPane(
356366
snoozeWakeTick,
357367
]);
358368
const threadListV2Layout = useMemo(() => {
369+
threadListInboxReturns.observe(workingShelfEnabled ? threads : null);
359370
return buildThreadListV2Items({
360371
pendingOrder,
361372
threads: threads.filter((thread) => thread.archivedAt === null),
@@ -368,11 +379,16 @@ function ThreadNavigationSidebarPane(
368379
queuedThreadKeys,
369380
settledLimit: settledVisibleCount,
370381
now: new Date().toISOString(),
382+
workingShelfEnabled,
383+
workingShelfExpanded,
384+
inboxReturnAt: threadListInboxReturns.returnedAt,
371385
snoozedShelfExpanded,
372386
settledShelfExpanded,
373387
selectedThreadKey: props.selectedThreadKey ?? null,
374388
});
375389
}, [
390+
workingShelfEnabled,
391+
workingShelfExpanded,
376392
pendingOrder,
377393
queuedThreadKeys,
378394
nowMinute,
@@ -424,6 +440,9 @@ function ThreadNavigationSidebarPane(
424440
const items: SidebarListItem[] = buildThreadListV2ListItems({
425441
items: threadListV2Layout.items,
426442
pendingTasks: v2PendingTasks,
443+
workingCount: threadListV2Layout.workingCount,
444+
workingShelfExpanded,
445+
workingShelfHeaderIndex: threadListV2Layout.workingShelfHeaderIndex,
427446
snoozedCount: threadListV2Layout.snoozedCount,
428447
snoozedShelfExpanded,
429448
snoozedShelfHeaderIndex: threadListV2Layout.snoozedShelfHeaderIndex,
@@ -457,6 +476,7 @@ function ThreadNavigationSidebarPane(
457476
snoozedShelfExpanded,
458477
snoozeEnvironmentIds,
459478
threadListV2Layout,
479+
workingShelfExpanded,
460480
]);
461481
const listMenuActions = useMemo<MenuAction[]>(
462482
() => [
@@ -587,6 +607,8 @@ function ThreadNavigationSidebarPane(
587607
savedConnectionsById,
588608
listEnvironments,
589609
threadSearchMatchByKey,
610+
// Rows read it for their reorder menu items.
611+
workingShelfEnabled,
590612
}),
591613
[
592614
props.selectedThreadKey,
@@ -595,6 +617,7 @@ function ThreadNavigationSidebarPane(
595617
savedConnectionsById,
596618
listEnvironments,
597619
threadSearchMatchByKey,
620+
workingShelfEnabled,
598621
],
599622
);
600623
useThreadJumpShortcuts(listItems, handleSelectThread);
@@ -712,7 +735,7 @@ function ThreadNavigationSidebarPane(
712735
reorderSupported={
713736
item.item.pinned
714737
? pinReorderEnvironmentIds.has(thread.environmentId)
715-
: activeReorderEnvironmentIds.has(thread.environmentId)
738+
: !workingShelfEnabled && activeReorderEnvironmentIds.has(thread.environmentId)
716739
}
717740
canMoveUp={item.canMoveUp}
718741
canMoveDown={item.canMoveDown}
@@ -729,6 +752,16 @@ function ThreadNavigationSidebarPane(
729752
/>
730753
);
731754
}
755+
case "v2-working-shelf":
756+
return (
757+
<ThreadListV2WorkingShelfHeader
758+
count={item.count}
759+
disabled={item.disabled}
760+
expanded={item.expanded}
761+
onToggle={toggleWorkingShelf}
762+
pane="sidebar"
763+
/>
764+
);
732765
case "v2-snoozed-shelf":
733766
return (
734767
<ThreadListV2SnoozedShelfHeader
@@ -797,9 +830,11 @@ function ThreadNavigationSidebarPane(
797830
snoozeThread,
798831
toggleSettledShelf,
799832
toggleSnoozedShelf,
833+
toggleWorkingShelf,
800834
unpinThread,
801835
unsettleThread,
802836
unsnoozeThread,
837+
workingShelfEnabled,
803838
],
804839
);
805840
// The list ignores sort/group options, so only the environment and project

0 commit comments

Comments
 (0)