diff --git a/apps/web/src/ProjectsNav.tsx b/apps/web/src/ProjectsNav.tsx index e53805f6..3ac56afa 100644 --- a/apps/web/src/ProjectsNav.tsx +++ b/apps/web/src/ProjectsNav.tsx @@ -89,7 +89,8 @@ function readCollapsedGroups() { export function ProjectsNav() { const projects = useProjectsNavState(); const sidebar = useSidebarState(); - const { heads, current, openStream, openPicker, seen } = useCatalog(); + const { heads, current, openStream, openPicker, seen, collecting } = + useCatalog(); const [closed, setClosed] = useState(readCollapsedGroups); const [query, setQuery] = useState(""); const now = useNow(); @@ -204,6 +205,7 @@ export function ProjectsNav() { ) : ( @@ -302,10 +304,12 @@ function NewMark() { function ProjectsEmpty({ loaded, + collecting, error, onRetry, }: { loaded: boolean; + collecting: boolean; error: string; onRetry: () => void; }) { @@ -319,9 +323,19 @@ function ProjectsEmpty({ ); return ( -
-

{loaded ? "No current changes" : "Loading projects…"}

- {loaded && ( +
+

+ {collecting + ? "Collecting projects…" + : loaded + ? "No current changes" + : "Loading projects…"} +

+ {loaded && !collecting && (

Agent hooks and diffx sync publish changed checkouts here.

diff --git a/apps/web/src/session-context.tsx b/apps/web/src/session-context.tsx index 946f157a..43697186 100644 --- a/apps/web/src/session-context.tsx +++ b/apps/web/src/session-context.tsx @@ -191,24 +191,30 @@ export function SessionProvider({ children }: { children: ReactNode }) { ); const { remove: removeHead, retry: reloadHeads } = heads; // Syncs the local checkouts; null when it fails or one is running. - const collectLatest = useCallback(async () => { - if (collectingRef.current) return null; - collectingRef.current = true; - setCollecting(true); - setCollectError(""); - try { - const { data, error } = await api.POST("/api/v2/collections"); - if (!data) throw new Error(errorDetail(error, "Unable to sync changes")); - reloadHeads(); - return data; - } catch (error) { - setCollectError(errorDetail(error, "Unable to sync changes")); - return null; - } finally { - collectingRef.current = false; - setCollecting(false); - } - }, [reloadHeads]); + const collectLatest = useCallback( + async (initial = false) => { + if (collectingRef.current) return null; + collectingRef.current = true; + setCollecting(true); + setCollectError(""); + try { + const { data, error } = await api.POST("/api/v2/collections", { + params: { query: { initial } }, + }); + if (!data) + throw new Error(errorDetail(error, "Unable to sync changes")); + reloadHeads(); + return data; + } catch (error) { + setCollectError(errorDetail(error, "Unable to sync changes")); + return null; + } finally { + collectingRef.current = false; + setCollecting(false); + } + }, + [reloadHeads], + ); const headsRef = useRef(heads.contexts); useEffect(() => { headsRef.current = heads.contexts; @@ -283,7 +289,11 @@ export function SessionProvider({ children }: { children: ReactNode }) { if (!collectOnLoad || collected.current === route) return; // Effect replay must not treat the in-flight collection as a failed one. collected.current = route; - void collectLatest().then((result) => { + void collectLatest(true).then((result) => { + if (result?.failures.length) + setCollectError( + `Unable to sync ${result.failures.length} ${result.failures.length === 1 ? "checkout" : "checkouts"}: ${result.failures.map((failure) => `${failure.path} (${failure.detail})`).join(", ")}`, + ); const contexts = result?.contexts ?? []; const head = route.kind === "live" @@ -386,6 +396,8 @@ export function SessionProvider({ children }: { children: ReactNode }) { // The root URL keeps waiting for changed work while showing the empty state. const empty = heads.loaded && !selectedId && route.kind === "root" && !collectOnLoad; + const statusError = + state.status === "error" ? state.detail : heads.error || collectError; useEffect(() => { if (!restorePickerFocus.current || (!empty && state.status === "loading")) return; @@ -423,36 +435,36 @@ export function SessionProvider({ children }: { children: ReactNode }) {

- {missing - ? "No current snapshot" - : empty - ? heads.contexts.length - ? "No changes to review" - : "No review contexts yet" - : state.status === "error" || heads.error - ? "Cannot load diffx" + {statusError + ? collectError + ? "Cannot sync changes" + : "Cannot load diffx" + : missing + ? "No current snapshot" + : empty + ? heads.contexts.length + ? "No changes to review" + : "No review contexts yet" : collectOnLoad ? "Collecting latest changes" : "Loading diffx"}

- {missing - ? collectError || - "This stream's snapshots expired or were deleted. Pick current work from Projects." - : empty - ? remote - ? "Create a collector token in settings, then run diffx sync to publish your changes." - : "Run diffx to review a checkout, or diffx sync to publish remotely." - : state.status === "error" - ? state.detail - : heads.error || - (collectOnLoad - ? "Reading checkouts for new changes…" - : "Reading review context…")} + {statusError || + (missing + ? "This stream's snapshots expired or were deleted. Pick current work from Projects." + : empty + ? remote + ? "Create a collector token in settings, then run diffx sync to publish your changes." + : "Run diffx to review a checkout, or diffx sync to publish remotely." + : collectOnLoad + ? "Reading checkouts for new changes…" + : "Reading review context…")}

{empty && remote && (