('[aria-label="不再提示"]')?.click(),
+ );
+ expect(hint(none.container)).toBeNull();
+ } finally {
+ await none.unmount();
+ }
+ // Dismissal is remembered on this device.
+ const again = await renderComponent();
+ try {
+ expect(hint(again.container)).toBeNull();
+ } finally {
+ await again.unmount();
+ }
+ window.localStorage.removeItem("openbot.sidebar.primaryHintDismissed");
+ // A chosen 主 Bot, an older Server (undefined) or no Bots never show the hint.
+ for (const extra of [{ primaryBotId: "reviewer" }, { primaryBotId: undefined }, { bots: [] }]) {
+ const view = await renderComponent();
+ try {
+ expect(hint(view.container)).toBeNull();
+ } finally {
+ await view.unmount();
+ }
+ }
+});
diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx
index 2542fed5..8d5b6d72 100644
--- a/apps/web/src/components/Sidebar.tsx
+++ b/apps/web/src/components/Sidebar.tsx
@@ -77,6 +77,15 @@ interface SidebarProps {
}
/** The shared sidebar of the design contract (docs/design/desktop-ui-2026-10/Sidebar.dc.html). */
+const primaryHintKey = "openbot.sidebar.primaryHintDismissed";
+function readPrimaryHintDismissed() {
+ try {
+ return window.localStorage.getItem(primaryHintKey) === "1";
+ } catch {
+ return false;
+ }
+}
+
export function Sidebar({
creatingBot,
onWork,
@@ -177,6 +186,22 @@ export function Sidebar({
const primaryEntry = primaryBotId
? entries.find((entry) => entry.key === `bot:${primaryBotId}`)
: undefined;
+ // A workspace upgraded from before 主 Bot existed has none; nudge once, per device, until the
+ // Owner picks one or dismisses it. The Server never elects one silently during an upgrade.
+ const [primaryHintDismissed, setPrimaryHintDismissed] = useState(readPrimaryHintDismissed);
+ const suggestPrimary =
+ primaryBotId === null &&
+ bots.length > 0 &&
+ onSetPrimaryBot !== undefined &&
+ !primaryHintDismissed;
+ function dismissPrimaryHint() {
+ setPrimaryHintDismissed(true);
+ try {
+ window.localStorage.setItem(primaryHintKey, "1");
+ } catch {
+ // Storage can be unavailable (private window); the hint then hides for this session only.
+ }
+ }
const sections = arrangeSidebar(
primaryEntry ? entries.filter((entry) => entry !== primaryEntry) : entries,
organization,
@@ -440,6 +465,17 @@ export function Sidebar({
<>
{primaryEntry ? (
{renderRow(primaryEntry, "", true)}
+ ) : suggestPrimary ? (
+
+
+
+ 选一个主 Bot
+ 没有 @ 人的消息会先交给它。右键任意 Bot,选「设为主 Bot」。
+
+
+
) : null}
{sections.map((section) => (