fix(web): show file summary after task completion (#143)

Co-authored-by: Youhai <185432073+Youhai020616@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-07-31 20:20:58 +08:00
1 parent 4be0262381
commit 4db89cbe3f
7 files changed
+108 -51

No files matched your search

+29 -26
View File
@@ -215,6 +215,13 @@ function headerStats(
*/
export const DRAFT_SESSION_ID = "new";
/**
* Server-enforced ceiling on paths per files/stat call (STAT_MAX_PATHS in the sessions routes,
* which 400s above it). A Task-level summary aggregates candidates across the whole Task and can
* exceed it, so cache misses are checked in chunks of this size.
*/
const STAT_PATHS_PER_REQUEST = 100;
export function ChatPage() {
const navigate = useNavigate();
const params = useParams<{ sessionId?: string }>();
@@ -451,10 +458,10 @@ export function ChatPage() {
}
}, [stream.taskState, reloadSessions, reloadAgents]);
// Existence cache for message file cards (session-level): normalized relative path -> whether
// it exists; while a lookup is in flight, the cache shares a single Promise, so a batch of
// concurrent mounts only issues one files/stat call.
const statCacheRef = useRef(new Map<string, boolean | Promise<boolean>>());
// Positive-only existence cache for file summary cards (session-level): normalized relative
// path -> true, or the shared in-flight lookup. Missing files aren't retained — a later Task may
// create the same path, so its summary must re-check instead of inheriting stale false state.
const statCacheRef = useRef(new Map<string, true | Promise<boolean>>());
// Session switch: resets the cost, the file-card existence cache, and the per-turn thinking
// level (it's per-session UI state), avoiding stale data from the previous Session (Files
@@ -466,34 +473,30 @@ export function ChatPage() {
statCacheRef.current = new Map();
}, [routeSessionId]);
// Batched existence check (message file cards): merges only the cache-miss paths into a single
// files/stat call, and the result lands in the session-level cache — during streaming, the
// candidate set is re-checked on every change, and the cache ensures only new paths trigger a
// request. On request failure, the placeholder is cleared (don't permanently cache a "couldn't
// find" as "doesn't exist"), returned as not-existing this time, and re-checked on the next mount.
// Batched existence check for file summaries: cache stable positive results and share in-flight
// requests, but never retain a negative result. Each pending lookup mutates the cache only while
// it is still the current entry, so an old request can't delete or overwrite a newer one.
const statFiles = useCallback(
async (paths: string[]): Promise<ReadonlySet<string>> => {
const sessionId = selected?.sessionId ?? null;
const cache = statCacheRef.current;
const misses = sessionId === null ? [] : paths.filter((p) => !cache.has(p));
if (sessionId !== null && misses.length > 0) {
const batch = api
.statSessionFiles(sessionId, misses)
.then((res) => new Set(res.existing))
.catch(() => null);
for (const p of misses) {
cache.set(
p,
batch.then((existing) => {
if (existing === null) {
cache.delete(p);
return false;
}
const exists = existing.has(p);
cache.set(p, exists);
return exists;
}),
);
for (let i = 0; i < misses.length; i += STAT_PATHS_PER_REQUEST) {
const chunk = misses.slice(i, i + STAT_PATHS_PER_REQUEST);
const batch = api
.statSessionFiles(sessionId, chunk)
.then((res) => new Set(res.existing))
.catch(() => null);
for (const p of chunk) {
const pending = batch.then((existing) => existing?.has(p) ?? false);
cache.set(p, pending);
void pending.then((exists) => {
if (cache.get(p) !== pending) return;
if (exists) cache.set(p, true);
else cache.delete(p);
});
}
}
}
const result = new Set<string>();