From f15e285de3acf1502fd174b2b9cc825d17e86782 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 21 Sep 2026 00:38:54 +0000 Subject: [PATCH 1/6] feat(desktop): WeChat-style chat list toolbar; Org nav opens openXYOS Add search and new-group (+) to the conversation history header, wired to the existing group-chat flow. Organization nav embeds openXYOS when integrated and otherwise falls back to the host org-ui workspace, so the old host workbench is no longer the default Org page. Desktop first-run still lands on /chat/main. Co-authored-by: XYAI Labs --- CHANGELOG.md | 1 + dashboard/src/locales/en.json | 1 + dashboard/src/locales/zh.json | 1 + .../src/pages/Chat/chatSidebar.partial.less | 25 ++- .../Chat/chatThemeOverrides.partial.less | 6 +- .../components/MinimalAgentSessionNav.tsx | 27 ++- .../Chat/components/SessionList.test.tsx | 90 +++++++++ .../src/pages/Chat/components/SessionList.tsx | 30 +-- .../components/SessionListToolbar.test.tsx | 134 +++++++++++++ .../Chat/components/SessionListToolbar.tsx | 184 ++++++++++++++++++ .../Organization/OrganizationEntry.test.tsx | 114 ++++------- .../pages/Organization/OrganizationEntry.tsx | 57 +++++- tests/unit/desktop/test_nsis_uninstall.py | 5 + 13 files changed, 559 insertions(+), 116 deletions(-) create mode 100644 dashboard/src/pages/Chat/components/SessionList.test.tsx create mode 100644 dashboard/src/pages/Chat/components/SessionListToolbar.test.tsx create mode 100644 dashboard/src/pages/Chat/components/SessionListToolbar.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index fa12307c..d2371de3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### 变更 +- 「对话」会话列表标题栏右侧补上微信式搜索与「+」新建群聊(接入已有 `openGroupChat`)。组织导航不再落到宿主旧工作台(功能卡片 +「打开原 App」壳),集成桌面嵌入 openXYOS;未集成时进入宿主 org-ui 总览。桌面首启仍落 `/chat/main`,不进组织页。 / Chat session list header now has WeChat-style search and a + new-group action (existing `openGroupChat`). Org nav embeds openXYOS when integrated, otherwise the host org-ui workspace — not the old workbench grid. Desktop first-run still lands on `/chat/main`. - Windows NSIS 文件复制结束后不再多点一次「下一步」才到结束页;结束页仍让用户勾选「运行 FreeOS」或直接关闭,不会自动启动、也不会自动关窗。静默安装(`/S`)仍不拉起界面。 / After Windows NSIS file copy, Setup advances to the finish page without an extra Next. The user still chooses Run FreeOS or close. No auto-launch / auto-close. Silent `/S` stays headless. - 桌面首次启动不再要求注册/登录。首屏是可选模型配置(云密钥或本机 Ollama,可跳过);跳过或保存后进入默认智能体对话,而不是停在工作台列表。已有提供商或会话的用户不会被再次拦住。账号仍可稍后在头像菜单里领取,供保存/导出/组织房间使用。首次运行与安全条目一致:不预填云密钥,本机 Ollama 优先。 diff --git a/dashboard/src/locales/en.json b/dashboard/src/locales/en.json index cbf02c66..aea91757 100644 --- a/dashboard/src/locales/en.json +++ b/dashboard/src/locales/en.json @@ -1534,6 +1534,7 @@ "bootstrapKickoff": "Hi — let's get started", "createSessionFailed": "Failed to create a session — please try again", "searchSessions": "Search conversations", + "newGroupChat": "New group chat", "noSearchResults": "No matching conversations", "expandMore": "Show more", "pin": "Pin", diff --git a/dashboard/src/locales/zh.json b/dashboard/src/locales/zh.json index 4b0542e0..9d26a523 100644 --- a/dashboard/src/locales/zh.json +++ b/dashboard/src/locales/zh.json @@ -1534,6 +1534,7 @@ "bootstrapKickoff": "你好,我们开始吧", "createSessionFailed": "创建会话失败,请重试", "searchSessions": "搜索会话", + "newGroupChat": "新建群聊", "noSearchResults": "没有匹配的会话", "expandMore": "展开更多", "pin": "置顶", diff --git a/dashboard/src/pages/Chat/chatSidebar.partial.less b/dashboard/src/pages/Chat/chatSidebar.partial.less index b66ca9e8..dba489a9 100644 --- a/dashboard/src/pages/Chat/chatSidebar.partial.less +++ b/dashboard/src/pages/Chat/chatSidebar.partial.less @@ -110,15 +110,34 @@ .sessionHeader { display: flex; flex-direction: column; - gap: 10px; - padding: 16px 16px 12px; + gap: 8px; + padding: 12px 12px 8px; flex-shrink: 0; } +.sessionHeaderRow { + display: flex; + align-items: center; + gap: 8px; + min-height: 32px; +} + .sessionTitle { + flex: 1; + min-width: 0; + margin: 0; font-size: 15px; font-weight: 700; - color: var(--fn-color-brand); + line-height: 1.2; + color: var(--fn-text-primary); +} + +.sessionHeaderActions { + display: flex; + align-items: center; + gap: 2px; + flex-shrink: 0; + margin-left: auto; } .sessionAddBtn { diff --git a/dashboard/src/pages/Chat/chatThemeOverrides.partial.less b/dashboard/src/pages/Chat/chatThemeOverrides.partial.less index 860be500..fe60437b 100644 --- a/dashboard/src/pages/Chat/chatThemeOverrides.partial.less +++ b/dashboard/src/pages/Chat/chatThemeOverrides.partial.less @@ -338,7 +338,7 @@ .sessionSearchWrap { position: relative; flex-shrink: 0; - margin: 0 0 10px; + margin: 0; } .sessionSearchIcon { @@ -381,12 +381,10 @@ } .sessionHeader { - padding: 0 0 12px; - align-items: flex-start; + padding: 0 0 10px; background: transparent; border: none; box-shadow: none; - flex-shrink: 0; } .sessionCreateBtn { diff --git a/dashboard/src/pages/Chat/components/MinimalAgentSessionNav.tsx b/dashboard/src/pages/Chat/components/MinimalAgentSessionNav.tsx index bef066c5..46150e08 100644 --- a/dashboard/src/pages/Chat/components/MinimalAgentSessionNav.tsx +++ b/dashboard/src/pages/Chat/components/MinimalAgentSessionNav.tsx @@ -21,6 +21,7 @@ import { isAgentChatReady } from "../../../utils/agentError"; import { sortSessions, toSession, type Session } from "../hooks/useSessions"; import { formatThreadTitle } from "../utils/threadTitle"; import { onSessionEvent, onStreamEvent } from "../hooks/chatStore"; +import SessionListToolbar from "./SessionListToolbar"; import SharedExpertHint from "./SharedExpertHint"; import styles from "../index.module.less"; @@ -276,6 +277,7 @@ export default function MinimalAgentSessionNav({ const [byAgent, setByAgent] = useState>({}); const [workingIds, setWorkingIds] = useState>(new Set()); const [loading, setLoading] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); const [collapsedFolders, setCollapsedFolders] = useState>(() => loadCollapsedFolders(), ); @@ -494,6 +496,17 @@ export default function MinimalAgentSessionNav({ [activeAgentId, onPinActive, patchLocal], ); + const searchNeedle = searchQuery.trim().toLowerCase(); + const visibleAgents = useMemo(() => { + if (!searchNeedle) return sortedAgents; + return sortedAgents.filter((agent) => { + if (agent.name.toLowerCase().includes(searchNeedle)) return true; + return (byAgent[agent.agent_id] ?? []).some((session) => + session.name.toLowerCase().includes(searchNeedle), + ); + }); + }, [byAgent, searchNeedle, sortedAgents]); + if (agents.length === 0) { return (
@@ -513,8 +526,18 @@ export default function MinimalAgentSessionNav({ return (
- {sortedAgents.map((agent) => { - const list = byAgent[agent.agent_id] ?? []; + + {visibleAgents.map((agent) => { + const list = (byAgent[agent.agent_id] ?? []).filter((session) => + searchNeedle + ? session.name.toLowerCase().includes(searchNeedle) || + agent.name.toLowerCase().includes(searchNeedle) + : true, + ); const ready = isAgentChatReady(agent.state); const expanded = !collapsedFolders.has(agent.agent_id); diff --git a/dashboard/src/pages/Chat/components/SessionList.test.tsx b/dashboard/src/pages/Chat/components/SessionList.test.tsx new file mode 100644 index 00000000..32479baa --- /dev/null +++ b/dashboard/src/pages/Chat/components/SessionList.test.tsx @@ -0,0 +1,90 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, it, vi } from "vitest"; +import type { OctopAgent } from "../../../context/AgentContext"; +import type { Session } from "../hooks/useSessions"; +import SessionList from "./SessionList"; + +vi.mock("../../../context/AgentContext", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + useAgent: () => ({ setActiveAgent: vi.fn() }), + }; +}); + +function agent(id: string, name: string): OctopAgent { + return { + id: Number(id.replace(/\D/g, "") || 1), + agent_id: id, + name, + description: "desc", + persona_mbti: null, + default_model: null, + system_prompt: null, + template_name: null, + state: "running", + last_error: null, + icon: null, + icon_name: null, + icon_url: null, + color: null, + config: {}, + }; +} + +const sessions: Session[] = [ + { + id: "s1", + name: "周会纪要", + threadId: "s1", + updatedAt: null, + channelType: "dashboard", + hasActivity: true, + pinned: false, + }, + { + id: "s2", + name: "代码审查", + threadId: "s2", + updatedAt: null, + channelType: "dashboard", + hasActivity: true, + pinned: false, + }, +]; + +describe("SessionList", () => { + it("filters sessions from the header search control", () => { + render( + + + , + ); + + expect(screen.getByText("周会纪要")).toBeInTheDocument(); + expect(screen.getByText("代码审查")).toBeInTheDocument(); + fireEvent.click(screen.getByTestId("chat-session-search-toggle")); + fireEvent.change(screen.getByTestId("chat-session-search"), { + target: { value: "周会" }, + }); + expect(screen.getByText("周会纪要")).toBeInTheDocument(); + expect(screen.queryByText("代码审查")).toBeNull(); + }); +}); diff --git a/dashboard/src/pages/Chat/components/SessionList.tsx b/dashboard/src/pages/Chat/components/SessionList.tsx index c8b76640..f8b7aa1e 100644 --- a/dashboard/src/pages/Chat/components/SessionList.tsx +++ b/dashboard/src/pages/Chat/components/SessionList.tsx @@ -9,7 +9,6 @@ import { Trash2, Pin, PinOff, - Search, GitFork, } from "lucide-react"; import type { Session } from "../hooks/useSessions"; @@ -18,6 +17,7 @@ import { isAgentChatReady } from "../../../utils/agentError"; import { showConfirmModal } from "../../../utils/confirmModal"; import { ExpertIcon } from "../../Experts/components/iconForName"; import SessionChannelIcon from "./SessionChannelIcon"; +import SessionListToolbar from "./SessionListToolbar"; import SharedExpertHint from "./SharedExpertHint"; import styles from "../index.module.less"; @@ -442,31 +442,13 @@ export default function SessionList({ () => activeAgentId ?? sortedAgents[0]?.agent_id ?? null, [activeAgentId, sortedAgents], ); - const activeAgent = useMemo( - () => sortedAgents.find((a) => a.agent_id === expandedAgentId) ?? null, - [sortedAgents, expandedAgentId], - ); - const showSessions = isAgentChatReady(activeAgent?.state); - return (
- {showSessions ? ( -
- - setSearchQuery(e.target.value)} - placeholder={t("chat.searchSessions", "搜索会话")} - aria-label={t("chat.searchSessions", "搜索会话")} - /> -
- ) : null} + {agents.length === 0 ? (
diff --git a/dashboard/src/pages/Chat/components/SessionListToolbar.test.tsx b/dashboard/src/pages/Chat/components/SessionListToolbar.test.tsx new file mode 100644 index 00000000..e610482e --- /dev/null +++ b/dashboard/src/pages/Chat/components/SessionListToolbar.test.tsx @@ -0,0 +1,134 @@ +import type { ComponentProps } from "react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OctopAgent } from "../../../context/AgentContext"; +import SessionListToolbar from "./SessionListToolbar"; + +const navigate = vi.fn(); +const setActiveAgent = vi.fn(); + +vi.mock("react-router-dom", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, useNavigate: () => navigate }; +}); + +vi.mock("../../../context/AgentContext", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + useAgent: () => ({ setActiveAgent }), + }; +}); + +vi.mock("../../../utils/openGroupChat", () => ({ + openGroupChat: vi.fn(), +})); + +import { openGroupChat } from "../../../utils/openGroupChat"; + +vi.mock("../../../utils/antdMessage", () => ({ + message: { success: vi.fn(), error: vi.fn() }, +})); + +function agent(id: string, name: string): OctopAgent { + return { + id: Number(id.replace(/\D/g, "") || 1), + agent_id: id, + name, + description: null, + persona_mbti: null, + default_model: null, + system_prompt: null, + template_name: null, + state: "running", + last_error: null, + icon: null, + icon_name: null, + icon_url: null, + color: null, + config: {}, + }; +} + +function renderToolbar( + props: Partial> = {}, +) { + const onSearchQueryChange = props.onSearchQueryChange ?? vi.fn(); + return render( + + + , + ); +} + +describe("SessionListToolbar", () => { + beforeEach(() => { + navigate.mockReset(); + setActiveAgent.mockReset(); + openGroupChat.mockReset(); + }); + + it("puts search and new-group controls on the conversation header", () => { + renderToolbar(); + expect(screen.getByTestId("chat-session-toolbar")).toBeInTheDocument(); + expect(screen.getByRole("heading", { name: "对话" })).toBeInTheDocument(); + expect( + screen.getByTestId("chat-session-search-toggle"), + ).toBeInTheDocument(); + expect(screen.getByTestId("chat-session-new-group")).toBeInTheDocument(); + expect(screen.queryByTestId("chat-session-search")).toBeNull(); + }); + + it("opens the search field and reports query changes", () => { + const onSearchQueryChange = vi.fn(); + renderToolbar({ onSearchQueryChange }); + fireEvent.click(screen.getByTestId("chat-session-search-toggle")); + const input = screen.getByTestId("chat-session-search"); + fireEvent.change(input, { target: { value: "周会" } }); + expect(onSearchQueryChange).toHaveBeenCalledWith("周会"); + }); + + it("creates a group chat from the plus button", async () => { + openGroupChat.mockResolvedValue({ + record: { + id: "t1", + threadId: "t1", + hostAgentId: "a1", + memberIds: ["a1", "a2"], + title: "群", + createdAt: 1, + lastActive: 1, + }, + created: true, + }); + renderToolbar(); + fireEvent.click(screen.getByTestId("chat-session-new-group")); + fireEvent.click(screen.getByLabelText("分析师")); + fireEvent.click(screen.getByLabelText("研究员")); + fireEvent.click(screen.getByRole("button", { name: "创建" })); + await waitFor(() => { + expect(openGroupChat).toHaveBeenCalledWith( + expect.objectContaining({ + memberIds: ["a1", "a2"], + memberNames: ["分析师", "研究员"], + }), + ); + }); + expect(setActiveAgent).toHaveBeenCalledWith("a1"); + expect(navigate).toHaveBeenCalledWith("/chat/a1/t1", { + state: { prefillInput: "@分析师 @研究员 " }, + }); + }); +}); diff --git a/dashboard/src/pages/Chat/components/SessionListToolbar.tsx b/dashboard/src/pages/Chat/components/SessionListToolbar.tsx new file mode 100644 index 00000000..e3b41964 --- /dev/null +++ b/dashboard/src/pages/Chat/components/SessionListToolbar.tsx @@ -0,0 +1,184 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { Checkbox, Form, Input, Modal } from "antd"; +import { Plus, Search } from "lucide-react"; +import { useAgent } from "../../../context/AgentContext"; +import type { OctopAgent } from "../../../context/AgentContext"; +import { message } from "../../../utils/antdMessage"; +import { openGroupChat } from "../../../utils/openGroupChat"; +import { expertMentionToken } from "../utils/expertMention"; +import styles from "../index.module.less"; + +interface SessionListToolbarProps { + searchQuery: string; + onSearchQueryChange: (value: string) => void; + agents: OctopAgent[]; +} + +export default function SessionListToolbar({ + searchQuery, + onSearchQueryChange, + agents, +}: SessionListToolbarProps) { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { setActiveAgent } = useAgent(); + const [searchOpen, setSearchOpen] = useState(false); + const [groupOpen, setGroupOpen] = useState(false); + const [creating, setCreating] = useState(false); + const [form] = Form.useForm<{ title?: string; members: string[] }>(); + + const showSearch = searchOpen || Boolean(searchQuery.trim()); + const newGroupLabel = t("chat.newGroupChat", "新建群聊"); + + const createGroup = async () => { + const values = await form.validateFields(); + const members = values.members || []; + if (members.length < 2) { + message.error( + t("projects.groupMembersRequired", "请至少选择两位(同事或智能助手)"), + ); + return; + } + const named = members + .map((id) => agents.find((agent) => agent.agent_id === id)?.name) + .filter((name): name is string => Boolean(name)); + setCreating(true); + try { + const { record, created } = await openGroupChat({ + memberIds: members, + memberNames: named, + title: values.title, + }); + const prefill = named.map((name) => expertMentionToken(name)).join(" "); + message.success( + created + ? t("projects.groupCreated", "群聊已创建") + : t("chat.expertPickerGroupOpened", "已进入群聊"), + ); + setGroupOpen(false); + form.resetFields(); + setActiveAgent(record.hostAgentId); + navigate(`/chat/${record.hostAgentId}/${record.threadId}`, { + state: { prefillInput: `${prefill} ` }, + }); + } catch (err) { + message.error( + err instanceof Error + ? err.message + : t("projects.groupCreateFailed", "无法创建群聊"), + ); + } finally { + setCreating(false); + } + }; + + return ( +
+
+

+ {t("nav.conversations", "对话")} +

+
+ + +
+
+ {showSearch ? ( +
+ + onSearchQueryChange(e.target.value)} + placeholder={t("chat.searchSessions", "搜索会话")} + aria-label={t("chat.searchSessions", "搜索会话")} + data-testid="chat-session-search" + autoFocus + /> +
+ ) : null} + + { + setGroupOpen(false); + form.resetFields(); + }} + onOk={() => void createGroup()} + okText={t("common.create", "创建")} + confirmLoading={creating} + destroyOnHidden + > +
+ + + + { + if (!value || value.length < 2) { + throw new Error( + t( + "projects.groupMembersRequired", + "请至少选择两位(同事或智能助手)", + ), + ); + } + }, + }, + ]} + > + ({ + label: agent.name, + value: agent.agent_id, + }))} + /> + +
+
+
+ ); +} diff --git a/dashboard/src/pages/Organization/OrganizationEntry.test.tsx b/dashboard/src/pages/Organization/OrganizationEntry.test.tsx index 69446fbc..04af1270 100644 --- a/dashboard/src/pages/Organization/OrganizationEntry.test.tsx +++ b/dashboard/src/pages/Organization/OrganizationEntry.test.tsx @@ -2,100 +2,56 @@ import { render, screen } from "@testing-library/react"; import { MemoryRouter, Route, Routes } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; import OrganizationEntry from "./OrganizationEntry"; -import { orgModuleApi, type OrgOverview } from "../../api/modules/orgModule"; - -const overview: OrgOverview = { - enabled: true, - runtime: "in_host", - sidecar_optional: true, - sidecar_reachable: false, - sidecar_embed_ok: false, - sidecar_url: "http://127.0.0.1:3780", - start_available: false, - install_ready: false, - start_command: "", - home: "/tmp", - last_sync: null, - freeos: { - employees: 0, - employee_states: {}, - agents: 0, - spawned_colleagues: 0, - org_skills: 0, - skill_packages: 0, - mcp: 0, - tasks: 0, - }, - openxyos: { - reachable: false, - url: "http://127.0.0.1:3780", - detail: "", - modules: 12, - governance: true, - tenant_id: "default", - approvals: 0, - }, - last_loop: null, - notes: [], - catalog: [], -}; +import { orgModuleApi } from "../../api/modules/orgModule"; vi.mock("../../api/modules/orgModule", () => ({ orgModuleApi: { - identityStatus: vi.fn(async () => ({ - integrated: true, - authority: "organization", - })), - overview: vi.fn(async () => overview), - setEnabled: vi.fn(), - startSidecar: vi.fn(), - restartSidecar: vi.fn(), - probeLivez: vi.fn(), - assemble: vi.fn(), - produce: vi.fn(), - pack: vi.fn(), - runLoop: vi.fn(), - setModules: vi.fn(), - downloadSource: vi.fn(), + identityStatus: vi.fn(), }, })); -vi.mock("../../hooks/useServerTimezone", () => ({ - useServerTimezone: () => "UTC", -})); - -vi.mock("../../utils/desktopFolder", () => ({ - pickDesktopFolder: vi.fn(), - canPickDesktopFolder: () => false, -})); - -vi.mock("../../utils/antdMessage", () => ({ - message: { success: vi.fn(), error: vi.fn(), info: vi.fn() }, -})); +function renderEntry() { + return render( + + + } /> + workspace
} + /> + + , + ); +} describe("OrganizationEntry", () => { beforeEach(() => { - vi.mocked(orgModuleApi.overview).mockResolvedValue(overview); + vi.mocked(orgModuleApi.identityStatus).mockReset(); + }); + + it("embeds openXYOS when desktop organization is integrated", async () => { vi.mocked(orgModuleApi.identityStatus).mockResolvedValue({ integrated: true, authority: "organization", }); + renderEntry(); + expect(await screen.findByTestId("org-openxyos-frame")).toBeInTheDocument(); + expect(screen.getByTestId("org-openxyos-frame")).toHaveAttribute( + "src", + "/organization-app/dashboard?freeos_embed=1", + ); + expect(screen.queryByTestId("org-native-workbench")).toBeNull(); + expect(screen.queryByTestId("org-original-app-hint")).toBeNull(); }); - it("keeps the native workbench as Organization home when desktop is integrated", async () => { - render( - - - } /> - - , - ); - expect( - await screen.findByTestId("org-native-workbench"), - ).toBeInTheDocument(); - expect( - await screen.findByTestId("org-original-app-hint"), - ).toBeInTheDocument(); + it("falls back to the host org-ui workspace when not integrated", async () => { + vi.mocked(orgModuleApi.identityStatus).mockResolvedValue({ + integrated: false, + authority: "studio", + }); + renderEntry(); + expect(await screen.findByTestId("org-ui-workspace")).toBeInTheDocument(); expect(document.querySelector("iframe")).toBeNull(); + expect(screen.queryByTestId("org-native-workbench")).toBeNull(); }); }); diff --git a/dashboard/src/pages/Organization/OrganizationEntry.tsx b/dashboard/src/pages/Organization/OrganizationEntry.tsx index c8148b2f..5e57a09d 100644 --- a/dashboard/src/pages/Organization/OrganizationEntry.tsx +++ b/dashboard/src/pages/Organization/OrganizationEntry.tsx @@ -1,9 +1,58 @@ -import OrganizationPage from "./index"; +import { useEffect, useState } from "react"; +import { Navigate } from "react-router-dom"; +import { Spin } from "antd"; +import { orgModuleApi } from "../../api/modules/orgModule"; + +const OPENXYOS_EMBED = "/organization-app/dashboard?freeos_embed=1"; /** - * Organization home is the native workbench. Desktop managed Node + iframe - * remains a transitional preview for unmigrated App verticals, not this route. + * Organization nav opens the integrated openXYOS app (same-origin embed). + * Host-native slices stay at /organization/workspace and siblings. + * The old assemble/pack workbench is no longer the Org landing page. + * First-run still lands on /chat/main — only this nav entry changed. */ export default function OrganizationEntry() { - return ; + const [integrated, setIntegrated] = useState(null); + + useEffect(() => { + let active = true; + void orgModuleApi + .identityStatus() + .then((status) => { + if (active) setIntegrated(Boolean(status.integrated)); + }) + .catch(() => { + if (active) setIntegrated(false); + }); + return () => { + active = false; + }; + }, []); + + if (integrated === true) { + return ( +
+