Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const clientSettings: ClientSettings = {
threadDetailsUsageEnabled: false,
threadDetailsItems: {},
sidebarWorkingShelfEnabled: false,
sidebarInboxSortBySendEnabled: false,
loadBalancingEnabled: false,
loadBalancingWeights: { "environment-1": 75, "environment-2": 0 },
pullRequestMergeMethodOverrides: {},
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,7 @@ export function firstValidTimestampMs(
export { sortActiveThreadsByOrderKey as sortThreadsForSidebar } from "@t3tools/client-runtime/state/thread-sort";
export {
sortInboxThreadsByReturn,
sortInboxThreadsBySend,
sortWorkingThreadsBySend,
} from "@t3tools/client-runtime/state/thread-inbox";

Expand Down
52 changes: 40 additions & 12 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ import {
sidebarListItemId,
sidebarThreadKeyAtY,
sortInboxThreadsByReturn,
sortInboxThreadsBySend,
sortPinnedThreadsForSidebar,
sortThreadsForSidebar,
sortWorkingThreadsBySend,
Expand Down Expand Up @@ -356,6 +357,7 @@ interface SidebarThreadPartitionInput {
readonly workingShelfEnabled: boolean;
/** Boring mode: folders list active threads by the user's latest message. */
readonly sortActiveBySend: boolean;
readonly sendSortEnabled: boolean;
// Snooze wakes, the queued-turn grace and the Working shelf's exit grace depend
// on the clock, so the minute tick, the precise snooze-wake tick and the inbox
// tracker's expiry version are inputs: a new key repartitions.
Expand All @@ -370,6 +372,21 @@ function threadShellKey(thread: Pick<SidebarThreadSummary, "environmentId" | "id
return scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id));
}

// The inbox follows the saved drag order unless a time-based order is on.
function sortActiveInbox(
threads: readonly EnvironmentThreadShell[],
order: {
readonly workingShelfEnabled: boolean;
readonly sendSortEnabled: boolean;
readonly returnedAt: (thread: EnvironmentThreadShell) => number | undefined;
},
): EnvironmentThreadShell[] {
const observedReturnAt = order.workingShelfEnabled ? order.returnedAt : undefined;
if (order.sendSortEnabled) return sortInboxThreadsBySend(threads, observedReturnAt);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
if (order.workingShelfEnabled) return sortInboxThreadsByReturn(threads, observedReturnAt);
return sortThreadsForSidebar(threads);
}

const NO_DRAGGED_ORDER: readonly string[] = [];
const NO_DRAGGED_ORDERS: {
readonly settledOrder: readonly string[];
Expand All @@ -382,8 +399,10 @@ function resolveDraggedOrders(input: {
readonly settledThreads: readonly EnvironmentThreadShell[];
readonly activeThreads: readonly EnvironmentThreadShell[];
readonly workingShelfEnabled: boolean;
readonly sendSortEnabled: boolean;
}): typeof NO_DRAGGED_ORDERS {
const { drag, threadByKey, settledThreads, activeThreads, workingShelfEnabled } = input;
const { drag, threadByKey, settledThreads, activeThreads, workingShelfEnabled, sendSortEnabled } =
input;
const thread = threadByKey.get(drag.activeKey);
if (thread === undefined) return NO_DRAGGED_ORDERS;
const others = (list: readonly EnvironmentThreadShell[]) =>
Expand All @@ -393,12 +412,13 @@ function resolveDraggedOrders(input: {
...others(settledThreads),
applySidebarThreadDrop(thread, "settled", drag.occurredAt),
]).map(threadShellKey),
activeOrder: workingShelfEnabled
? sortInboxThreadsByReturn(
[...others(activeThreads), applySidebarThreadDrop(thread, "active", drag.occurredAt)],
inboxReturns.returnedAt,
).map(threadShellKey)
: undefined,
activeOrder:
workingShelfEnabled || sendSortEnabled
? sortActiveInbox(
[...others(activeThreads), applySidebarThreadDrop(thread, "active", drag.occurredAt)],
{ workingShelfEnabled, sendSortEnabled, returnedAt: inboxReturns.returnedAt },
).map(threadShellKey)
: undefined,
};
}

Expand Down Expand Up @@ -441,6 +461,7 @@ function useSidebarThreads(input: {
// Folders already separate the work, so boring mode has no Working shelf.
const workingShelfEnabled =
useClientSettings((s) => s.sidebarWorkingShelfEnabled) && folderGroups === null;
const sendSortEnabled = useClientSettings((s) => s.sidebarInboxSortBySendEnabled);
const inboxReturnsVersion = useSyncExternalStore(
inboxReturns.subscribe,
inboxReturns.getSnapshot,
Expand Down Expand Up @@ -485,6 +506,7 @@ function useSidebarThreads(input: {
optimisticDrop,
workingShelfEnabled,
sortActiveBySend: folderGroups !== null,
sendSortEnabled,
clockKey: `${nowMinute}:${snoozeWakeClock}:${inboxReturnsVersion}`,
});

Expand Down Expand Up @@ -565,6 +587,7 @@ function useSidebarThreads(input: {
...partition,
threads,
workingShelfEnabled,
sendSortEnabled,
searchableThreads: [
...pinnedThreads,
...activeThreads,
Expand Down Expand Up @@ -620,6 +643,7 @@ function partitionSidebarThreads(input: SidebarThreadPartitionInput) {
optimisticDrop,
workingShelfEnabled,
sortActiveBySend,
sendSortEnabled,
} = input;
const preciseNow = new Date().toISOString();
const visible = filterSidebarV2VisibleThreads(threads, scopedProjectKeys, delegatedUnderParent);
Expand Down Expand Up @@ -679,9 +703,11 @@ function partitionSidebarThreads(input: SidebarThreadPartitionInput) {
const sortedPinned = sortPinnedThreadsForSidebar(pinned);
const sortedActive = sortActiveBySend
? sortWorkingThreadsBySend(active)
: workingShelfEnabled
? sortInboxThreadsByReturn(active, inboxObservation.returnedAt)
: sortThreadsForSidebar(active);
: sortActiveInbox(active, {
workingShelfEnabled,
sendSortEnabled,
returnedAt: inboxObservation.returnedAt,
});
return {
pinnedThreads:
optimisticDrop?.section !== "pinned" || optimisticDrop.order === null
Expand Down Expand Up @@ -1295,6 +1321,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve

const {
workingShelfEnabled,
sendSortEnabled,
pinnedThreads,
draggableThreadKeys,
activeReorderableThreadKeys,
Expand Down Expand Up @@ -2031,6 +2058,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve
settledThreads,
activeThreads,
workingShelfEnabled,
sendSortEnabled,
});
const sidebarSortingStrategy = createSidebarSortingStrategy({
items: sidebarListItems,
Expand Down Expand Up @@ -2069,7 +2097,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve
activeOrder: activeKeys,
activeKeysById,
activeReorderableKeys: activeReorderableThreadKeys,
activeTimeOrdered: workingShelfEnabled,
activeTimeOrdered: workingShelfEnabled || sendSortEnabled,
}).kind !== "none"
);
},
Expand Down Expand Up @@ -2103,7 +2131,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve
activeOrder: activeKeys,
activeKeysById,
activeReorderableKeys: activeReorderableThreadKeys,
activeTimeOrdered: workingShelfEnabled,
activeTimeOrdered: workingShelfEnabled || sendSortEnabled,
});
if (plan.kind === "none") return;
if (plan.kind === "settle" && settlingThreadKeysRef.current.has(activeKey)) return;
Expand Down
21 changes: 21 additions & 0 deletions apps/web/src/components/settings/LabsSettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function LabsSettingsPanel() {
<SettingsPageContainer>
<SettingsSection id="beta" title="Beta">
<BoringSidebarRow />
<InboxSortBySendRow />
{/* Clerk is only mounted when this build has T3 Connect. */}
{hasCloudPublicConfig() ? <GuestLinksRowWithSignIn /> : <GuestLinksRow />}
<LinkedEnvironmentsRow />
Expand Down Expand Up @@ -75,6 +76,26 @@ function BoringSidebarMultilineRow() {
);
}

function InboxSortBySendRow() {
const settings = useScopedSettings();
const updateSettings = useUpdateScopedSettings();
return (
<SettingsRow
{...searchableSetting("inbox-sort-by-send")}
description="Order the inbox by the last message you sent, newest first. Agent activity does not move threads. Dragging within the inbox is off while this is on."
control={
<Switch
checked={settings.sidebarInboxSortBySendEnabled}
onCheckedChange={(checked) =>
updateSettings({ sidebarInboxSortBySendEnabled: Boolean(checked) })
}
aria-label="Sort inbox by last message"
/>
}
/>
);
}

/** Signed out of T3 Connect, the row offers to sign in instead of its switch. */
function GuestLinksRowWithSignIn() {
const { isSignedIn } = useAuth();
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,10 @@ export function useSettingsRestore(onRestored?: () => void) {
...(settings.sidebarWorkingShelfEnabled !== DEFAULT_UNIFIED_SETTINGS.sidebarWorkingShelfEnabled
? ["Working section"]
: []),
...(settings.sidebarInboxSortBySendEnabled !==
DEFAULT_UNIFIED_SETTINGS.sidebarInboxSortBySendEnabled
? ["Sort inbox by last message"]
: []),
...(settings.sidebarAutoSettleAfterDays !== DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays
? ["Auto-settle inactive threads"]
: []),
Expand Down Expand Up @@ -660,6 +664,7 @@ export function useSettingsRestore(onRestored?: () => void) {
sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode,
sidebarProjectSortOrder: DEFAULT_UNIFIED_SETTINGS.sidebarProjectSortOrder,
sidebarWorkingShelfEnabled: DEFAULT_UNIFIED_SETTINGS.sidebarWorkingShelfEnabled,
sidebarInboxSortBySendEnabled: DEFAULT_UNIFIED_SETTINGS.sidebarInboxSortBySendEnabled,
sidebarAutoSettleAfterDays: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays,
sidebarAutoSettleKeepPullRequests: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleKeepPullRequests,
sidebarAutoSettleOnMerge: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge,
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/components/settings/settingsSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,12 @@ export const SETTINGS_SEARCH_ITEMS = [
targetId: "boring-sidebar",
searchTerms: ["beta boring sidebar folder rows multiline branch pull request details"],
},
{
id: "inbox-sort-by-send",
title: "Sort inbox by last message",
to: "/settings/labs",
searchTerms: ["beta order recent newest latest message sent threads inbox sidebar"],
},
{
id: "tool-call-visibility",
title: "Tool calls in chat",
Expand Down
20 changes: 17 additions & 3 deletions docs/user/thread-sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ thread is over takes the accent color. Section labels also
identify empty sections and a collapsed settled shelf.

Drag within the pinned or active section to change its order. Other rows slide aside to show the
spot where the thread will land. Drops into either section keep the position you choose. On
spot where the thread will land. Drops into either section keep the position you choose. On web
and desktop, with **Working section** or **Sort inbox by last message** on, the active list orders
itself and dragging within it is off; pinned reordering still works. On
mobile, open a thread's menu and choose **Arrange threads**. Drag a handle within or between
**Pinned** and **Active** to reorder, pin, or unpin. Drop onto the **Settled** divider to
settle a thread. The dragged card shows the action before you release it. Expand **Snoozed**
Expand Down Expand Up @@ -144,8 +146,9 @@ This is on by default. Turn it off with **Settings → General → Working secti
or **Settings → General → Working section** on iOS and Android. Each device keeps its own
choice.

While this is on, the active list is ordered by when each thread last came back to you, so you
cannot drag or move threads within it. Your saved order returns when you turn it off.
While this is on, the active list is ordered by when each thread last came back to you, or by
your last message when **Sort inbox by last message** is also on. Either way, you cannot drag or
move threads within it. Your saved order returns when you turn both off.

### Group threads by project (beta)

Expand All @@ -159,6 +162,17 @@ Pinned threads stay above the folders, and Snoozed and Settled stay below them.
can drag a thread onto the composer to mention it, but not within the sidebar, and the Working
section is off. Each device keeps its own choice.

### Sort the inbox by your last message (beta)

Turn on **Settings → Labs → Sort inbox by last message** on web and desktop to order
the active list by the last message you sent, newest first, instead of your saved order. Agent
activity does not move a thread, so rows only change place when you send, create, or un-settle a
thread, or, with the Working section on, when a thread comes back to you. You cannot drag or move
threads within the active list while this is on. The boring sidebar already lists each folder by
your last message, so this setting only changes the list without folders. Each device keeps its own
choice. Turning it off brings back your saved order, or return-time order if the Working section is
still on.

## Settle finished work

Choose **Settle thread** from its menu to move finished work out of the active list
Expand Down
5 changes: 5 additions & 0 deletions packages/client-runtime/src/state/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ export interface EnvironmentThreadShell {
readonly blockedOnThreadWait?: boolean;
readonly latestUserMessageAt: string | null;
readonly latestUserAuthoredMessageAt?: string | null;
/** Set only when the server reports it; `latestRun.completedAt` can fall back to `updatedAt`. */
readonly latestRunCompletedAt?: string | null;
readonly hasPendingApprovals: boolean;
readonly hasPendingUserInput: boolean;
readonly pendingUserInputAt?: string | null;
Expand Down Expand Up @@ -307,6 +309,9 @@ export function presentThreadShell(
...(thread.latestUserAuthoredMessageAt === undefined
? {}
: { latestUserAuthoredMessageAt: nullableIso(thread.latestUserAuthoredMessageAt) }),
...(thread.latestRunCompletedAt === undefined
? {}
: { latestRunCompletedAt: nullableIso(thread.latestRunCompletedAt) }),
hasPendingApprovals:
thread.pendingRuntimeRequest !== null &&
thread.pendingRuntimeRequest.kind !== "user_input" &&
Expand Down
Loading
Loading