Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1d2db13
fix: mark first-seen completed threads unread
StiensWout Jun 18, 2026
69d7c25
test(web): cover first-seen unread detection
danieliser Jul 23, 2026
b55f82c
fix(web): preserve manual unread state
danieliser Jul 23, 2026
5c3c168
fix(web): harden unread thread tracking
danieliser Jul 23, 2026
47274fd
fix(web): avoid unread bootstrap races
danieliser Jul 23, 2026
9a130c0
fix(web): observe unread bootstrap atomically
danieliser Jul 23, 2026
b6691c4
fix(web): preserve unread through thread hydration
danieliser Jul 23, 2026
641f27a
fix(web): track draft route before unread visits
danieliser Jul 23, 2026
6148456
fix(web): preserve unread across draft promotion
danieliser Jul 24, 2026
8d14c1f
fix(web): clear stale active visits across routes
danieliser Jul 24, 2026
a31fdbe
fix(web): ignore stale active visit timestamps
danieliser Jul 24, 2026
ad827b5
feat(web): add persistent sidebar thread filters
danieliser Jul 23, 2026
d9584e4
fix(web): clarify unchecked sidebar filters
danieliser Jul 23, 2026
aaaaf32
feat(web): add recent and attention sidebar filters
danieliser Jul 23, 2026
9d1e838
fix(web): tighten sidebar filter menu
danieliser Jul 23, 2026
a161eb5
fix(web): harden sidebar filter edge cases
danieliser Jul 23, 2026
e3f7fb8
fix(web): preserve active thread filter status
danieliser Jul 23, 2026
e5df6b1
fix(web): remove dead focus from archived rows
danieliser Jul 23, 2026
71547f9
fix(web): harden sidebar filter semantics
danieliser Jul 23, 2026
f6f8951
fix(web): keep archived rows non-navigable
danieliser Jul 23, 2026
7411d82
fix(web): include turn activity in recent filter
danieliser Jul 23, 2026
1fca098
fix(web): match unread as secondary filter status
danieliser Jul 23, 2026
cfb698c
fix(web): keep sidebar filters current and navigable
danieliser Jul 23, 2026
613cf3d
fix(web): register active thread routes eagerly
danieliser Jul 24, 2026
95eba60
feat(web): add flat sidebar thread grouping
danieliser Jul 23, 2026
c1567c2
fix(web): show all threads in flat sidebar mode
danieliser Jul 23, 2026
baba9a6
feat(web): support sidebar filters in v2
danieliser Jul 23, 2026
16e4720
fix(web): align flat thread jump targets
danieliser Jul 23, 2026
5ac338e
fix(web): address sidebar grouping review feedback
danieliser Jul 23, 2026
1e98d34
fix(web): preserve flat sidebar visibility
danieliser Jul 24, 2026
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
62 changes: 39 additions & 23 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
useState,
} from "react";
import { flushSync } from "react-dom";
import { useNavigate } from "@tanstack/react-router";
import { useNavigate, useRouter } from "@tanstack/react-router";
import { useShallow } from "zustand/react/shallow";
import {
isAtomCommandInterrupted,
Expand Down Expand Up @@ -161,7 +161,11 @@ import {
deriveLogicalProjectKeyFromSettings,
selectProjectGroupingSettings,
} from "../logicalProject";
import { buildDraftThreadRouteParams } from "../threadRoutes";
import {
buildDraftThreadRouteParams,
resolveThreadRouteTarget,
shouldKeepActiveThreadVisitOnUnmount,
} from "../threadRoutes";
import {
type ComposerImageAttachment,
type DraftThreadEnvMode,
Expand Down Expand Up @@ -1149,10 +1153,7 @@ function ChatViewContent(props: ChatViewProps) {
const composerDraftTarget: ScopedThreadRef | DraftId =
routeKind === "server" ? routeThreadRef : props.draftId;
const serverThread = useThread(routeThreadRef);
const markThreadVisited = useUiStateStore((store) => store.markThreadVisited);
const activeThreadLastVisitedAt = useUiStateStore(
(store) => store.threadLastVisitedAtById[routeThreadKey],
);
const markActiveThreadVisited = useUiStateStore((store) => store.markActiveThreadVisited);
const settings = useEnvironmentSettings(environmentId);
// New-thread defaults live in the primary environment's settings.json (the
// settings UI never writes to remote environments), so read them from the
Expand All @@ -1164,6 +1165,7 @@ function ChatViewContent(props: ChatViewProps) {
const timestampFormat = settings.timestampFormat;
const autoOpenPlanSidebar = settings.autoOpenPlanSidebar;
const navigate = useNavigate();
const router = useRouter();
const { resolvedTheme } = useTheme();
// Granular store selectors — avoid subscribing to prompt changes.
const composerRuntimeMode = useComposerDraftStore(
Expand Down Expand Up @@ -1751,23 +1753,37 @@ function ChatViewContent(props: ChatViewProps) {
);

useEffect(() => {
if (!serverThread?.id) return;
const threadUpdatedAt = Date.parse(serverThread.updatedAt);
if (Number.isNaN(threadUpdatedAt)) return;
const lastVisitedAt = activeThreadLastVisitedAt ? Date.parse(activeThreadLastVisitedAt) : NaN;
if (!Number.isNaN(lastVisitedAt) && lastVisitedAt >= threadUpdatedAt) return;

markThreadVisited(
scopedThreadKey(scopeThreadRef(serverThread.environmentId, serverThread.id)),
serverThread.updatedAt,
);
}, [
activeThreadLastVisitedAt,
markThreadVisited,
Comment thread
cursor[bot] marked this conversation as resolved.
serverThread?.environmentId,
serverThread?.id,
serverThread?.updatedAt,
]);
if (routeKind !== "server") {
markActiveThreadVisited(routeThreadKey, null);
return;
}
const visitedAt =
!serverThread || Number.isNaN(Date.parse(serverThread.updatedAt))
? null
: serverThread.updatedAt;
markActiveThreadVisited(routeThreadKey, visitedAt);
}, [markActiveThreadVisited, routeKind, routeThreadKey, serverThread?.updatedAt]);

useEffect(
() => () => {
const currentRouteParams =
router.state.matches[router.state.matches.length - 1]?.params ?? {};
const currentRouteTarget = resolveThreadRouteTarget(currentRouteParams);
if (
shouldKeepActiveThreadVisitOnUnmount({
currentRouteTarget,
routeThreadKey,
draftId,
})
) {
return;
}
if (useUiStateStore.getState().activeThreadVisit?.threadId === routeThreadKey) {
markActiveThreadVisited(null, null);
}
},
[draftId, markActiveThreadVisited, routeThreadKey, router],
);

const selectedProviderByThreadId = composerActiveProvider ?? null;
const threadProvider =
Expand Down
Loading
Loading