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
24 changes: 24 additions & 0 deletions apps/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,30 @@
</head>
<body class="bb-app-shell">
<div id="root"></div>
<script>
// Boot watchdog: a transient asset failure (e.g. reloading right as the
// server restarts after a self-update) leaves module scripts failed with
// no retry and the page blank forever. If the app has not mounted after
// 10s, reload once; the one-shot flag prevents a reload loop when the
// failure is persistent.
(function () {
var RETRY_FLAG = "bb:boot-watchdog:retried";
setTimeout(function () {
var root = document.getElementById("root");
if (root && root.childElementCount > 0) {
try { sessionStorage.removeItem(RETRY_FLAG); } catch (e) {}
return;
}
try {
if (sessionStorage.getItem(RETRY_FLAG) === "true") return;
sessionStorage.setItem(RETRY_FLAG, "true");
} catch (e) {
return;
}
window.location.reload();
}, 10000);
})();
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions apps/app/src/components/ui/app-toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,17 @@ export interface AppToastOptions
action?: Action;
cancel?: Action;
description?: ReactNode;
/** Render a corner close button. For persistent toasts whose button slots
* are taken by real actions, so plain dismissal stays one click. */
showCloseButton?: boolean;
}

export interface AppToastContentProps {
action?: Action;
cancel?: Action;
description?: ReactNode;
id?: number | string;
showCloseButton?: boolean;
title: ReactNode;
tone: AppToastTone;
}
Expand Down Expand Up @@ -123,6 +127,7 @@ export function AppToastContent({
cancel,
description,
id,
showCloseButton,
title,
tone,
}: AppToastContentProps) {
Expand All @@ -133,6 +138,20 @@ export function AppToastContent({
<div
className="relative w-[var(--width,356px)] max-w-[calc(100vw-32px)] rounded-md border border-border bg-popover px-4 py-3 text-popover-foreground shadow-sm max-[600px]:w-[calc(100vw-32px)]"
>
{showCloseButton ? (
<Button
type="button"
variant="ghost"
size="sm"
aria-label="Dismiss"
className="absolute right-1.5 top-1.5 size-6 p-0 text-muted-foreground"
onClick={() => {
dismissToast(id);
}}
>
<Icon name="X" className="size-3.5" />
</Button>
) : null}
<div className="flex items-start gap-3">
<div className="mt-0.5 flex size-4 shrink-0 items-center justify-center text-foreground">
<Icon
Expand Down Expand Up @@ -189,6 +208,7 @@ function showAppToast({
className,
description,
duration,
showCloseButton,
...sonnerOptions
} = options ?? {};
const nextDuration =
Expand All @@ -201,6 +221,7 @@ function showAppToast({
cancel={cancel}
description={description}
id={id}
showCloseButton={showCloseButton}
title={title}
tone={tone}
/>
Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/hooks/cache-owners/cache-owner-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ const CACHE_OWNER_QUERY_KEY_IMPORTS: CacheOwnerQueryKeyImportRegistry = {
"threadsQueryKey",
],
"hooks/cache-owners/system-config-cache-owner.ts": ["systemConfigQueryKey"],
"hooks/cache-owners/system-version-cache-owner.ts": [
"systemVersionQueryKey",
],
"hooks/cache-owners/terminal-cache-owner.ts": [
"allTerminalsQueryKeyPrefix",
"TerminalQueryScope",
Expand Down
24 changes: 24 additions & 0 deletions apps/app/src/hooks/cache-owners/system-version-cache-owner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { QueryClient } from "@tanstack/react-query";
import type {
SystemSelfUpdateState,
SystemVersionResponse,
} from "@bb/server-contract";
import { systemVersionQueryKey } from "../queries/query-keys";

export interface ApplySelfUpdateStateToVersionCacheArgs {
queryClient: QueryClient;
selfUpdate: SystemSelfUpdateState;
}

/** Fold a schedule/cancel mutation result into the cached version response. */
export function applySelfUpdateStateToVersionCache(
args: ApplySelfUpdateStateToVersionCacheArgs,
): void {
args.queryClient.setQueryData<SystemVersionResponse>(
systemVersionQueryKey(),
(previous) =>
previous === undefined
? previous
: { ...previous, selfUpdate: args.selfUpdate },
);
}
8 changes: 8 additions & 0 deletions apps/app/src/hooks/queries/query-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const SYSTEM_PROVIDERS_QUERY_KEY = "systemProviders";
export const SYSTEM_CONFIG_QUERY_KEY = "systemConfig";
export const SYSTEM_EXECUTION_OPTIONS_QUERY_KEY = "systemExecutionOptions";
export const SYSTEM_VERSION_QUERY_KEY = "systemVersion";
export const SYSTEM_AGENT_ACTIVITY_QUERY_KEY = "systemAgentActivity";
export const HOST_PROVIDER_CLI_STATUS_QUERY_KEY = "hostProviderCliStatus";
export const SYSTEM_USAGE_LIMITS_QUERY_KEY = "systemUsageLimits";
export const HOST_PATH_EXISTENCE_QUERY_KEY = "hostPathExistence";
Expand Down Expand Up @@ -422,6 +423,9 @@ export type SystemProvidersQueryKey = readonly [
];
export type SystemConfigQueryKey = readonly [typeof SYSTEM_CONFIG_QUERY_KEY];
export type SystemVersionQueryKey = readonly [typeof SYSTEM_VERSION_QUERY_KEY];
export type SystemAgentActivityQueryKey = readonly [
typeof SYSTEM_AGENT_ACTIVITY_QUERY_KEY,
];
export type HostProviderCliStatusQueryKey = readonly [
typeof HOST_PROVIDER_CLI_STATUS_QUERY_KEY,
string | null,
Expand Down Expand Up @@ -1017,6 +1021,10 @@ export function systemVersionQueryKey(): SystemVersionQueryKey {
return [SYSTEM_VERSION_QUERY_KEY];
}

export function systemAgentActivityQueryKey(): SystemAgentActivityQueryKey {
return [SYSTEM_AGENT_ACTIVITY_QUERY_KEY];
}

export function hostProviderCliStatusQueryKey(
hostId: string | null,
): HostProviderCliStatusQueryKey {
Expand Down
74 changes: 73 additions & 1 deletion apps/app/src/hooks/queries/system-queries.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toRecord } from "@bb/core-ui";
import type {
SystemAgentActivityResponse,
SystemConfigResponse,
SystemExecutionOptionsResponse,
SystemSelfUpdateMode,
SystemSelfUpdateState,
SystemVersionResponse,
} from "@bb/server-contract";
import type { ProviderCliStatusResponse } from "@bb/host-daemon-contract";
import type { ProviderUsageResponse } from "@bb/host-daemon-contract";
import * as api from "@/lib/api";
import { applySelfUpdateStateToVersionCache } from "@/hooks/cache-owners/system-version-cache-owner";
import { useSystemRealtimeSubscription } from "@/hooks/useRealtimeSubscription";
import {
hostProviderCliStatusQueryKey,
systemAgentActivityQueryKey,
systemConfigQueryKey,
systemExecutionOptionsQueryKey,
systemUsageLimitsQueryKey,
Expand Down Expand Up @@ -96,12 +101,79 @@ export function useSystemConfig(options?: QueryOptions) {
});
}

/** Poll cadence while a scheduled self-update is pending, so the UI notices
* staging failures and the post-update version bump. */
const SCHEDULED_SELF_UPDATE_REFETCH_MS = 30_000;

export function useSystemVersion(options?: QueryOptions) {
return useQuery<SystemVersionResponse>({
queryKey: systemVersionQueryKey(),
queryFn: ({ signal }) => api.getSystemVersion(signal),
enabled: options?.enabled ?? true,
...SERVER_SESSION_QUERY_POLICY,
refetchInterval: (query) =>
query.state.data?.selfUpdate.scheduled != null
? SCHEDULED_SELF_UPDATE_REFETCH_MS
: false,
});
}

/**
* Poll cadence for the update toast's busy/idle action label. 1s keeps the
* label effectively live; the endpoint is a single indexed count on a small
* table, and the poll only runs while the update choice is on screen (an
* update is available, capable, and not yet scheduled or dismissed).
*/
const AGENT_ACTIVITY_REFETCH_MS = 1_000;

/**
* Live agent load, used to pick "Update now" vs "Update when agents finish"
* on the update toast. Unlike the sidebar's thread cache this counts every
* thread — side-chats and archived included — so the label is global truth.
*/
export function useSystemAgentActivity(options?: QueryOptions) {
return useQuery<SystemAgentActivityResponse>({
queryKey: systemAgentActivityQueryKey(),
queryFn: ({ signal }) => api.getSystemAgentActivity(signal),
enabled: options?.enabled ?? true,
refetchInterval: AGENT_ACTIVITY_REFETCH_MS,
staleTime: 0,
});
}

/**
* Busy/idle for the update toasts, with the shared safe default: unknown
* activity reads as busy, since deferring is always the harmless choice.
* Queued follow-ups count as busy — an update would orphan them, so
* "Update now" would not actually be immediate.
*/
export function useAgentsBusy(options?: QueryOptions): boolean {
const { data } = useSystemAgentActivity(options);
return data === undefined
? true
: data.busyThreadCount > 0 || data.queuedThreadCount > 0;
}

function useApplySelfUpdateState() {
const queryClient = useQueryClient();
return (selfUpdate: SystemSelfUpdateState): void => {
applySelfUpdateStateToVersionCache({ queryClient, selfUpdate });
};
}

export function useScheduleSelfUpdate() {
const applySelfUpdateState = useApplySelfUpdateState();
return useMutation({
mutationFn: (mode: SystemSelfUpdateMode) => api.scheduleSelfUpdate(mode),
onSuccess: applySelfUpdateState,
});
}

export function useCancelSelfUpdate() {
const applySelfUpdateState = useApplySelfUpdateState();
return useMutation({
mutationFn: () => api.cancelSelfUpdate(),
onSuccess: applySelfUpdateState,
});
}

Expand Down
Loading
Loading