Skip to content
Closed
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
19 changes: 19 additions & 0 deletions docs/buzz-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,25 @@ Keep a first explanation brief: Solo, Team, agents, and one useful next step.
Offer more detail when asked. Do not pretend to highlight controls or navigate
the interface unless an available tool actually supports that action.

## Buzz Doctor

When background reads fail, **Buzz is having issues** appears once as a toast.
Choose **Retry** to check the affected reads again, or **View issues** to open
Buzz Doctor and retry an individual source. Available local agent choices and
loaded conversations stay usable. Retrying does not resend messages or repeat
saved actions. The notice clears after the issues recover; an open Doctor dialog
stays open and shows that everything is working again. Back closes the dialog and
Forward can restore it; **Close Buzz Doctor** ends that visit.
Retry actions show a spinner and ignore repeated clicks while running. **Retry
all** checks affected background reads in sequence. The toast hides while Doctor
or Settings is open and returns on closing if issues remain.
During a retry batch, every Retry action stays disabled; only the issue currently
being retried shows a spinner.
If a retry finishes with issues remaining, the button briefly shakes unless
reduced motion is enabled. During **Retry all**, each failed issue’s Retry button
shakes as its attempt finishes, while remaining disabled until the batch ends.
Use **F6** to reach notifications by keyboard, then Tab to their actions.

## Agents in Huddles

On supported desktop hosts, a running local agent with voice support joins your
Expand Down
2 changes: 1 addition & 1 deletion docs/design-system-adoption.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Use this map to find the shared owner before changing a product surface.
| Modals | Dialog and AlertDialog | Use the [modal composition](../src/shared/design-system/DESIGN.md#modal-composition): shared title/description lockup, normalized sizes, secondary Close, and fixed footer gutter outside scrolling content. Page search, community setup, and workflow confirmations retain their dismissal and focus contracts. |
| Panels and headers | Panel and PanelHeader | Settings, channels, and companion cards share appearance. Features own grids, scrolling, docks, and subscriptions. |
| Read loading | LoadingRegion | Profile Settings uses a same-scope verified snapshot while refreshing; cold reads reserve the actual form geometry. Delayed status does not delay results, errors, or write feedback. |
| Feedback | Toast (Sonner) | Sidebar preferences and Settings recovery use the shared bottom-right Sonner stack with source-owned recovery. Completed-action feedback can outlive its row. Channel live-update recovery stays inline above the composer; form errors, blocked pages, and lasting paused-state context stay inline. |
| Feedback | Toast (Sonner) | Buzz Doctor consolidates background-read failures in one persistent bottom-right toast with Retry and a Dialog of individual issues/actions. Services retain retry ownership; represented sources suppress duplicate notices. Settings recovery uses source-owned notices. Completed-action feedback can outlive its row. Form errors, failed sends, blocked pages and lasting paused-state context stay inline. |
| Hints | Tooltip | Control hints and agent activity support keyboard access and dismissal. Controls keep their own accessible names. |
| Solo chats and activity | NavigationItem, Button, IconButton, Panel, PanelHeader | Solo chat history, agent choice, and activity actions retain unread, admission, draft, and focus behavior. Base UI owns their menus. |
| Media stages | `surface-inverse` and `text-inverse` | Preserve the stage’s existing values and measure text pairings. Renderers own image and video pixels. |
Expand Down
5 changes: 5 additions & 0 deletions docs/navigation-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ unchanged-looking stop. Opening the content explicitly again creates a fresh vis

### Dialogs

Buzz Doctor is a read-only, community-scoped global dialog using this contract.
Its visit retains the underlying workspace. Restoring it reads the current
session's issues; it never replays Retry. Recovery can show a healthy state in
the open dialog without closing it or discarding its return path.

Back closes the dialog before leaving its underlying view. Forward can reopen it.

For `channel A → channel B → Members`:
Expand Down
31 changes: 29 additions & 2 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { AgentAvatarArtworkProvider } from "../features/agents/AgentAvatarArtwor
import { Welcome } from "../features/identity/Welcome";
import { NewMenu } from "./shell/NewMenu";
import { PrimarySidebar } from "./shell/PrimarySidebar";
import { BuzzDoctor } from "../features/relay/BuzzDoctor";
import { conversationFocusTarget, paneFocusTarget } from "./focus-navigation";
import { ChannelNavigationProvider } from "../features/channel-navigation/ChannelNavigationState";
import { AgentsContext } from "../features/agents/react";
import { ToastProvider } from "../shared/design-system/ui/Toast";
Expand Down Expand Up @@ -593,7 +595,32 @@ function ConnectedAppContent({ services }: { services: AppServices }) {
</div>
);
return (
<>
<BuzzDoctor
relay={services.relay}
navigation={services.navigation}
scope={
client.viewer && client.selected
? {
viewer: client.viewer,
communityOrigin: communityDestination(client.selected).url,
}
: null
}
open={
route.target.kind === "doctor" &&
!route.failure &&
!route.waiting &&
!launching
}
onClose={route.leaveDoctor}
focusTarget={() =>
(workspace.target.kind === "conversation"
? conversationFocusTarget(workspace.target.channelId)
: paneFocusTarget("main")) ??
document.getElementById("main-content") ??
undefined
}
>
<SignInEnding identity={services.federatedIdentity} />
{isTauri() && (
<PairingPrompt
Expand Down Expand Up @@ -869,6 +896,6 @@ function ConnectedAppContent({ services }: { services: AppServices }) {
)}
</Dialog>
</ChannelNavigationProvider>
</>
</BuzzDoctor>
);
}
42 changes: 42 additions & 0 deletions src/app/navigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,48 @@ afterEach(async () => {
window.history.replaceState(null, "", "/");
});

it("keeps the underlying destination beneath Buzz Doctor and retires its visit on explicit Close", async () => {
vi.stubGlobal(
"ResizeObserver",
class {
observe() {}
unobserve() {}
disconnect() {}
},
);
activation.release();
const current = createServices();
services = current;
render(<App services={current} />);
// This fixture has no Messages plugin; its default destination settles as unavailable.
await waitFor(() =>
expect(current.navigation.snapshot().status).toBe("failed"),
);
const beneath = current.navigation.snapshot().entry.target;
void current.navigation.open({ version: 1, kind: "doctor", scope: null });
expect(
await screen.findByRole("dialog", { name: "Buzz Doctor" }),
).toBeVisible();
await waitFor(() =>
expect(current.navigation.snapshot().status).toBe("opened"),
);
await act(async () => current.navigation.back());
await waitFor(() =>
expect(screen.queryByRole("dialog", { name: "Buzz Doctor" })).toBeNull(),
);
expect(current.navigation.snapshot().entry.target).toEqual(beneath);
await act(async () => current.navigation.forward());
expect(
await screen.findByRole("dialog", { name: "Buzz Doctor" }),
).toBeVisible();
fireEvent.click(screen.getByRole("button", { name: "Close Buzz Doctor" }));
await waitFor(() =>
expect(screen.queryByRole("dialog", { name: "Buzz Doctor" })).toBeNull(),
);
await act(async () => current.navigation.forward());
expect(screen.queryByRole("dialog", { name: "Buzz Doctor" })).toBeNull();
});

it("waits for a starting plugin before opening its addressed Settings card", async () => {
vi.stubGlobal(
"ResizeObserver",
Expand Down
47 changes: 31 additions & 16 deletions src/app/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
} from "react";
import type { AppServices } from "./services";
import { communityDestination } from "../features/communities/destination";
import { targetKey, type OpenTarget } from "../features/navigation/targets";
import {
isDialogTarget,
targetKey,
type OpenTarget,
} from "../features/navigation/targets";
import type { PageNavigation } from "../features/navigation/service";
import type {
OpenAttempt,
Expand Down Expand Up @@ -58,6 +62,7 @@ export function useAppNavigation(
const startup = plugins.configuration.status;
const target = state.entry.target;
const settings = target.kind === "settings";
const dialog = isDialogTarget(target);
const provider = settings ? target.provider : undefined;
const providerAvailable = useSyncExternalStore(
(listener) =>
Expand Down Expand Up @@ -89,8 +94,7 @@ export function useAppNavigation(
const lastNonSettings = useRef<
{ target: OpenTarget; entryId: string } | undefined
>(undefined);
if (target.kind !== "settings")
lastNonSettings.current = { target, entryId: state.entry.id };
if (!dialog) lastNonSettings.current = { target, entryId: state.entry.id };
const scope = "scope" in target ? target.scope : undefined;
const pageKey =
target.kind === "page"
Expand Down Expand Up @@ -261,12 +265,12 @@ export function useAppNavigation(
useLayoutEffect(() => {
if (owner.waiting || owner.failure) return;
const previous = workspace.current;
if (!settings && previous?.presentation.resume(owner.attempt)) {
if (!dialog && previous?.presentation.resume(owner.attempt)) {
setPresentation({ owner, request: previous.presentation.request });
previous.owner = owner;
return;
}
if (!settings) {
if (!dialog) {
previous?.presentation.dispose();
workspace.current = undefined;
}
Expand Down Expand Up @@ -303,12 +307,12 @@ export function useAppNavigation(
};
},
},
!settings && !!owner.page,
!dialog && !!owner.page,
);
setPresentation({ owner, request: next.request });
if (settings) return next.dispose;
if (dialog) return next.dispose;
workspace.current = { owner, presentation: next };
}, [services, owner, settings]);
}, [services, owner, dialog]);
const retained = workspace.current;
const sameWorkspaceVisit =
retained &&
Expand All @@ -321,17 +325,19 @@ export function useAppNavigation(
const request =
presentation?.owner === owner
? presentation.request
: !settings && sameWorkspaceVisit
: !dialog && sameWorkspaceVisit
? retainedRequest
: undefined;
const workspacePage = settings
? retainedRequest && retained?.owner.page
: page;
const workspacePage = dialog ? retainedRequest && retained?.owner.page : page;
const workspaceTarget =
settings && retainedRequest && retained
dialog && retainedRequest && retained
? retained.owner.attempt.entry.target
: target;
const workspaceRequest = settings ? retainedRequest : request;
const workspaceRequest = dialog ? retainedRequest : request;
useEffect(() => {
if (target.kind === "doctor" && !waiting && !failure)
request?.complete({ status: "opened" });
}, [target, waiting, failure, request]);
useEffect(() => {
if (state.attempt.signal.aborted) return;
if (failure) {
Expand Down Expand Up @@ -453,9 +459,9 @@ export function useAppNavigation(
target: workspaceTarget,
request: workspaceRequest,
selected:
workspacePage?.key ?? (settings ? "home" : (pageKey ?? target.kind)),
workspacePage?.key ?? (dialog ? "home" : (pageKey ?? target.kind)),
attempt:
settings && retainedRequest && retained
dialog && retainedRequest && retained
? retained.owner.attempt.id
: state.attempt.id,
},
Expand All @@ -475,6 +481,15 @@ export function useAppNavigation(
}, settings);
}
},
leaveDoctor() {
const current = navigation.snapshot().entry.target;
if (current.kind !== "doctor") return;
const key = targetKey(current);
navigation.dismiss(
(target) => targetKey(target) === key,
lastNonSettings.current?.target ?? { version: 1, kind: "home" },
);
},
dismissPluginImport(request: PageNavigation | undefined) {
const current = request?.target;
if (!request || current?.kind !== "settings" || !current.pluginImport)
Expand Down
6 changes: 0 additions & 6 deletions src/app/shell/PrimarySidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import { SoloRegions } from "../../features/channel-navigation/SoloRegions";
import { SoloInventory } from "../../features/channel-navigation/SoloInventory";
import { soloDraft } from "../../features/channel-navigation/routes";
import { RecoveryNotice } from "../../features/relay/RecoveryNotice";
import {
ChannelInventory,
sidebarEntryLocation,
Expand Down Expand Up @@ -276,11 +275,6 @@ function ScopedSidebar({
panelId={panelId}
{...frame}
>
{/* Relay outages for every inventory recover through one
automatic owner; its notice is the single Retry. */}
{!connection.cached && (
<RecoveryNotice recovery={connection.session.recovery} />
)}
<LiveSignIn
session={connection.session}
identity={communityIdentity(
Expand Down
9 changes: 8 additions & 1 deletion src/bundled/channels/LiveStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
useInlineRecovery,
} from "../../features/relay/RecoveryNotice";
import { liveRecoveryStatus } from "../../features/relay/live-recovery";
import {
useDoctorHosted,
useDoctorIssue,
} from "../../features/relay/BuzzDoctor";
import { Button } from "../../shared/design-system/ui/Button";
import styles from "./LiveStatus.module.css";

Expand All @@ -33,8 +37,10 @@ export function LiveStatus({
/** A recovery or retry control that had focus went away; move focus on. */
onRecovered?: (() => void) | undefined;
}) {
const doctor = useDoctorIssue(live);
const hosted = useDoctorHosted();
const { recovering: retrying, shown } = useInlineRecovery(
diagnostics ? undefined : recovery,
diagnostics || hosted ? undefined : recovery,
);
const focused = useRef(false);
const snapshot = useSyncExternalStore(
Expand Down Expand Up @@ -180,6 +186,7 @@ export function LiveStatus({
</>
);
}
if (doctor) return null;
if (recovery && shown)
return (
<section
Expand Down
4 changes: 3 additions & 1 deletion src/bundled/channels/SoloWorkspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
type RefObject,
} from "react";
import type { RelaySession } from "../../features/relay/session";
import { useDoctorIssue } from "../../features/relay/BuzzDoctor";
import { readView, writeView } from "../../shared/view-state";
import { Button } from "../../shared/design-system/ui/Button";
import { NavigationSectionHeading } from "../../shared/design-system/ui/NavigationSection";
Expand Down Expand Up @@ -100,6 +101,7 @@ export function SoloWorkspaces({
useChatConversions(session);
const handoff = useChannelNavigation();
const preferences = session.soloPreferences;
const doctor = useDoctorIssue(preferences);
const collapsedKey = "solo:collapsed-projects";
const expandedKey = "solo:expanded-projects";
const snapshot = useSyncExternalStore(
Expand Down Expand Up @@ -424,7 +426,7 @@ export function SoloWorkspaces({
Loading workspaces…
</p>
)}
{snapshot.status === "error" && (
{snapshot.status === "error" && !doctor && (
<div role="alert" className={styles.listMessage}>
<p>{snapshot.error ?? "Workspaces couldn’t load."}</p>
<Button onClick={() => void preferences.refresh()}>
Expand Down
Loading
Loading