From 86f8b8e213a7f1dee3d56c120b1bb461eeaf7bef Mon Sep 17 00:00:00 2001 From: Julian Arango Date: Mon, 10 Aug 2026 09:17:09 -0500 Subject: [PATCH 1/3] feat: tabbed Admin header with drilled Analytics, one standing-context editor Admin drops its index/detail grammar for the shared page-header lozenge cluster (Integrations/team grammar): Admin identity lozenge = Company context landing, People/Billing/Analytics as tabs, Analytics drilling into a back-chip + lens-cluster header (new PageHeaderBackChip). The rail's Admin row always reopens the dashboard home. All standing-prose surfaces (Company context, About me, agent Job description, team context) now share one ContextEditorPage/Box: always open, save-on-blur, no empty states; Company context gains a titled hero and a 3-part greyed example. Old InstructionsContent and the team editor's duplicate are deleted, with their dead locale strings. Co-Authored-By: Claude Fable 5 --- app/src/components/about-me/about-me-view.tsx | 30 ++-- .../agent-admin/agent-admin-instructions.tsx | 19 ++- .../agent/job-description-parts.tsx | 129 ---------------- app/src/components/context/context-editor.tsx | 142 ++++++++++++++++++ app/src/components/context/context-slots.ts | 18 --- .../organization/admin-analytics-header.tsx | 101 +++++++++++++ .../components/organization/admin-header.tsx | 112 ++++++++++++++ .../components/organization/admin-index.tsx | 90 ----------- ...tion-detail.tsx => admin-section-body.tsx} | 17 +-- .../organization/analytics-lens-store.ts | 26 ++++ .../components/organization/analytics-tab.tsx | 49 ++---- .../organization/company-context-tab.tsx | 33 ++-- .../components/organization/org-view-model.ts | 58 ++++++- .../organization/organization-view.tsx | 99 ++++++------ .../page-header/page-header-back-chip.tsx | 53 +++++++ app/src/components/shell/page-shell.tsx | 12 +- .../components/shell/sidebar-nav-sections.tsx | 13 +- .../team-view/team-context-card.tsx | 2 - .../team-view/team-context-editor.tsx | 76 ++-------- app/src/locales/en/agents.json | 7 +- app/src/locales/en/context.json | 10 +- app/src/locales/en/teams.json | 20 +-- app/src/locales/es/agents.json | 7 +- app/src/locales/es/context.json | 10 +- app/src/locales/es/teams.json | 20 +-- app/src/locales/pt/agents.json | 7 +- app/src/locales/pt/context.json | 10 +- app/src/locales/pt/teams.json | 20 +-- app/tests/org-view-model.test.ts | 41 ++++- app/tests/settings-view-gates.test.ts | 18 ++- app/tests/sidebar-nav-sections.test.ts | 10 +- packages/web/e2e/agent-policy.spec.ts | 5 +- packages/web/e2e/context-surface.spec.ts | 44 +++--- packages/web/e2e/spaces-gating.spec.ts | 4 +- packages/web/e2e/support/settings-nav.ts | 69 ++++++--- .../web/e2e/team-settings-manager.spec.ts | 14 +- packages/web/e2e/time-worked.spec.ts | 28 ++-- 37 files changed, 807 insertions(+), 616 deletions(-) delete mode 100644 app/src/components/agent/job-description-parts.tsx create mode 100644 app/src/components/context/context-editor.tsx create mode 100644 app/src/components/organization/admin-analytics-header.tsx create mode 100644 app/src/components/organization/admin-header.tsx delete mode 100644 app/src/components/organization/admin-index.tsx rename app/src/components/organization/{admin-section-detail.tsx => admin-section-body.tsx} (66%) create mode 100644 app/src/components/organization/analytics-lens-store.ts create mode 100644 app/src/components/shell/page-header/page-header-back-chip.tsx diff --git a/app/src/components/about-me/about-me-view.tsx b/app/src/components/about-me/about-me-view.tsx index a994eef19..e45f2b483 100644 --- a/app/src/components/about-me/about-me-view.tsx +++ b/app/src/components/about-me/about-me-view.tsx @@ -1,8 +1,7 @@ -import { Spinner } from "@houston-ai/core"; import { useTranslation } from "react-i18next"; -import { InstructionsContent } from "../agent/job-description-parts"; -import { useContextSlot, useContextSlotLabels } from "../context/context-slots"; -import { PageContainer, PageHero } from "../shell/page-shell"; +import { ContextEditorPage } from "../context/context-editor"; +import { useContextSlot } from "../context/context-slots"; +import { PageContainer } from "../shell/page-shell"; /** * About me: what every agent knows about the PERSON before it starts a turn. @@ -29,23 +28,18 @@ import { PageContainer, PageHero } from "../shell/page-shell"; export function AboutMeView() { const { t } = useTranslation("context"); const editor = useContextSlot("user"); - const labels = useContextSlotLabels("user"); return (
- - - {editor.ready ? ( - - ) : ( -
- -
- )} + +
); diff --git a/app/src/components/agent/agent-admin/agent-admin-instructions.tsx b/app/src/components/agent/agent-admin/agent-admin-instructions.tsx index b8573abdb..7e9c9c6f3 100644 --- a/app/src/components/agent/agent-admin/agent-admin-instructions.tsx +++ b/app/src/components/agent/agent-admin/agent-admin-instructions.tsx @@ -1,22 +1,35 @@ +import { useTranslation } from "react-i18next"; import { useInstructions, useSaveInstructions } from "../../../hooks/queries"; import type { AgentSectionProps } from "../../agent-settings/agent-settings-nav.ts"; -import { InstructionsContent } from "../job-description-parts"; +import { ContextEditorPage } from "../../context/context-editor"; -/** Instructions (CLAUDE.md) section. Read-only for non-managers. */ +/** + * Instructions (CLAUDE.md) section, in the ONE standing-prose grammar + * (`ContextEditorPage`): a level-2 hero — the settings page's rail already + * owns the screen's title — over the always-open box. Read-only for + * non-managers: the same face, locked, so they still read what the agent is + * told. + */ export function AgentAdminInstructions({ agent, readOnly = false, }: AgentSectionProps) { + const { t } = useTranslation("agents"); const path = agent.folderPath; const { data: instructions } = useInstructions(path); const saveInstructions = useSaveInstructions(path); return ( - saveInstructions.mutateAsync({ name: "CLAUDE.md", content: c }) } + placeholder={t("instructions.placeholder")} /> ); } diff --git a/app/src/components/agent/job-description-parts.tsx b/app/src/components/agent/job-description-parts.tsx deleted file mode 100644 index 13923547a..000000000 --- a/app/src/components/agent/job-description-parts.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { - Button, - cn, - EmptyDescription, - EmptyHeader, - EmptyTitle, -} from "@houston-ai/core"; -import { FileText } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -type SaveState = "idle" | "saving" | "saved"; - -export interface InstructionsContentLabels { - emptyTitle: string; - emptyDescription: string; - writeButton: string; - helper: string; - saving: string; - saved: string; - placeholder: string; -} - -export function InstructionsContent({ - content, - onSave, - labels, - readOnly = false, -}: { - content: string; - onSave: (content: string) => Promise; - labels?: InstructionsContentLabels; - /** - * Managed-agent read-only mode (matrix v2): a non-manager sees the - * instructions but cannot edit them. Hides the "Write" affordance, locks the - * textarea, and drops the save-on-blur. The gateway 403s writes regardless. - */ - readOnly?: boolean; -}) { - const { t } = useTranslation("agents"); - const resolved: InstructionsContentLabels = labels ?? { - emptyTitle: t("instructions.emptyTitle"), - emptyDescription: t("instructions.emptyDescription"), - writeButton: t("instructions.writeButton"), - helper: t("instructions.helper"), - saving: t("instructions.saving"), - saved: t("instructions.saved"), - placeholder: t("instructions.placeholder"), - }; - const [value, setValue] = useState(content); - const [editing, setEditing] = useState(false); - const [state, setState] = useState("idle"); - - useEffect(() => { - setValue(content); - }, [content]); - - const textareaRef = useCallback( - (el: HTMLTextAreaElement | null) => { - if (el && editing) el.focus(); - }, - [editing], - ); - - const handleBlur = async () => { - if (readOnly || value === content) return; - setState("saving"); - await onSave(value); - setState("saved"); - window.setTimeout(() => setState("idle"), 2000); - }; - - if (!value.trim() && !editing) { - return ( -
- - {resolved.emptyTitle} - {resolved.emptyDescription} - - {!readOnly && ( - - )} -
- ); - } - - return ( -
-
-

{resolved.helper}

- - {state === "saving" - ? resolved.saving - : state === "saved" - ? resolved.saved - : ""} - -
-
-