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
7 changes: 5 additions & 2 deletions apps/mobile/src/state/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ import { environmentCatalog } from "../connection/catalog";
import { connectionAtomRuntime } from "../connection/runtime";
import { environmentSnapshotAtom } from "./shell";

export const threadEnvironment = createThreadEnvironmentAtoms(connectionAtomRuntime);
export const threadEnvironment = createThreadEnvironmentAtoms(
connectionAtomRuntime,
environmentSnapshotAtom,
);
export const environmentThreads = createEnvironmentThreadStateAtoms(connectionAtomRuntime);
export const environmentThreadDetails = createEnvironmentThreadDetailAtoms(
environmentThreads.stateAtom,
);
export const environmentThreadShells = createEnvironmentThreadShellAtoms({
catalogValueAtom: environmentCatalog.catalogValueAtom,
snapshotAtom: environmentSnapshotAtom,
snapshotAtom: threadEnvironment.snapshotAtom,
});

const EMPTY_THREAD_STATE_ATOM = Atom.make(AsyncResult.success(EMPTY_ENVIRONMENT_THREAD_STATE)).pipe(
Expand Down
42 changes: 42 additions & 0 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
sortProjectsForSidebar,
sortScopedProjectsForSidebar,
shouldCreateNewThreadInCurrentProject,
shouldNavigateAfterThreadPark,
THREAD_JUMP_HINT_SHOW_DELAY_MS,
type SidebarListItem,
type SidebarListMarker,
Expand Down Expand Up @@ -2492,3 +2493,44 @@ describe("resolveSidebarDropVerb", () => {
expect(resolveSidebarDropVerb("active", "snoozed")).toBeNull();
});
});

describe("navigation after parking a thread", () => {
it.each([
["settle", "settled", null, "thread", true],
["settle", "active", null, "thread", false],
["settle", "settled", null, "other-thread", false],
["snooze", null, "2099-01-01T00:00:00.000Z", "thread", true],
["snooze", null, null, "thread", false],
["snooze", null, "2026-09-12T09:00:00.000Z", "thread", false],
["snooze", null, "2099-01-01T00:00:00.000Z", "thread", false, true],
["snooze", null, "2099-01-01T00:00:00.000Z", "other-thread", false],
] as const)(
"%s with state %s / %s on %s navigates: %s",
(
action,
settledOverride,
snoozedUntil,
currentThreadKey,
expected,
hasPendingApprovals: boolean = false,
) => {
expect(
shouldNavigateAfterThreadPark({
threadKey: "thread",
currentThreadKey,
action,
now: "2026-09-12T10:00:00.000Z",
thread: {
settledOverride,
snoozedUntil,
snoozedAt: null,
session: null,
latestTurn: null,
hasPendingApprovals,
hasPendingUserInput: false,
},
}),
).toBe(expected);
},
);
});
20 changes: 20 additions & 0 deletions apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import type { ContextMenuItem } from "@t3tools/contracts";
import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings";
import type { AsyncResult } from "effect/unstable/reactivity";
import { planPinnedReorder } from "@t3tools/client-runtime/state/thread-sort";
import {
effectiveSnoozed,
type ThreadSnoozeShell,
} from "@t3tools/client-runtime/state/thread-settled";
import {
getThreadSortTimestamp,
resolveSettledThreadTimestamp,
Expand All @@ -21,6 +25,22 @@ import type { SidebarThreadSummary, Thread } from "../types";
import { cn } from "../lib/utils";
import { isLatestTurnSettled } from "../session-logic";

export function shouldNavigateAfterThreadPark(input: {
readonly threadKey: string;
readonly currentThreadKey: string | null;
readonly action: "settle" | "snooze";
readonly now: string;
readonly thread: (ThreadSnoozeShell & Pick<SidebarThreadSummary, "settledOverride">) | null;
}): boolean {
return (
input.threadKey === input.currentThreadKey &&
input.thread !== null &&
(input.action === "settle"
? input.thread.settledOverride === "settled"
: effectiveSnoozed(input.thread, { now: input.now }))
);
}

const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]";
export const THREAD_JUMP_HINT_SHOW_DELAY_MS = 200;
// Visible sidebar rows are prewarmed into the thread-detail cache so opening a
Expand Down
33 changes: 30 additions & 3 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ import {
resolveSidebarThreadStatus,
searchSidebarThreads,
shouldCreateNewThreadInCurrentProject,
shouldNavigateAfterThreadPark,
shouldRecedeSidebarThread,
resolveWorkingStartedAt,
sidebarListItemId,
Expand Down Expand Up @@ -3038,7 +3039,15 @@ export default function Sidebar() {
}
// Only move forward if the user is still on the settled thread —
// a navigation made during the await wins over ours.
if (routeThreadKeyRef.current === threadKey) {
if (
shouldNavigateAfterThreadPark({
threadKey,
currentThreadKey: routeThreadKeyRef.current,
action: "settle",
now: new Date().toISOString(),
thread: readThreadShell(threadRef),
})
) {
navigateAfterSettle?.();
}
} finally {
Expand Down Expand Up @@ -3567,7 +3576,17 @@ export default function Sidebar() {
const settled = await run(settleThread(threadRef), "Failed to settle thread").finally(
() => settlingThreadKeysRef.current.delete(activeKey),
);
if (settled && routeThreadKeyRef.current === activeKey) navigateAfterSettle?.();
if (
settled &&
shouldNavigateAfterThreadPark({
Comment thread
Bil0000 marked this conversation as resolved.
threadKey: activeKey,
currentThreadKey: routeThreadKeyRef.current,
action: "settle",
now: new Date().toISOString(),
thread: readThreadShell(threadRef),
})
)
navigateAfterSettle?.();
return;
}
case "move-active":
Expand Down Expand Up @@ -3664,7 +3683,15 @@ export default function Sidebar() {
}
// Only move forward if the user is still on the snoozed thread —
// a navigation made during the await wins over ours.
if (routeThreadKeyRef.current === threadKey) {
if (
shouldNavigateAfterThreadPark({
threadKey,
currentThreadKey: routeThreadKeyRef.current,
action: "snooze",
now: new Date().toISOString(),
thread: readThreadShell(threadRef),
})
) {
navigateAfterSnooze?.();
}
return { status: "success" } as const;
Expand Down
7 changes: 5 additions & 2 deletions apps/web/src/state/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ import { environmentCatalog } from "../connection/catalog";
import { connectionAtomRuntime } from "../connection/runtime";
import { environmentSnapshotAtom } from "./shell";

export const threadEnvironment = createThreadEnvironmentAtoms(connectionAtomRuntime);
export const threadEnvironment = createThreadEnvironmentAtoms(
connectionAtomRuntime,
environmentSnapshotAtom,
);
const environmentThreads = createEnvironmentThreadStateAtoms(connectionAtomRuntime);
export const environmentThreadDetails = createEnvironmentThreadDetailAtoms(
environmentThreads.stateAtom,
);
export const environmentThreadShells = createEnvironmentThreadShellAtoms({
catalogValueAtom: environmentCatalog.catalogValueAtom,
snapshotAtom: environmentSnapshotAtom,
snapshotAtom: threadEnvironment.snapshotAtom,
});

const EMPTY_THREAD_STATE_ATOM = Atom.make(AsyncResult.success(EMPTY_ENVIRONMENT_THREAD_STATE)).pipe(
Expand Down
Loading
Loading