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-settings/agent-settings-section.tsx b/app/src/components/agent-settings/agent-settings-section.tsx index 3b6d47efb..3949109ec 100644 --- a/app/src/components/agent-settings/agent-settings-section.tsx +++ b/app/src/components/agent-settings/agent-settings-section.tsx @@ -11,10 +11,10 @@ import type { import { AgentSettingsPeople } from "./agent-settings-people.tsx"; /** - * The access bodies (people, apps, models) are deliberately flush (`w-full`) so - * the mounting surface owns their width. This gives them the SAME column the - * self-padded bodies (job description, learnings) bring — `max-w-3xl px-6` on - * one `pt-2` top rhythm — so nothing shifts as the rail switches sections. + * The flush bodies (job description, people, apps, models) deliberately own no + * width of their own, so the mounting surface does. This gives them the SAME + * column the one self-padded body (learnings) brings — `max-w-3xl px-6` on one + * `pt-2` top rhythm — so nothing shifts as the rail switches sections. */ function AccessColumn({ children }: { children: ReactNode }) { return ( @@ -34,7 +34,11 @@ export function AgentSettingsSectionView({ }: AgentSectionProps & { section: AgentSettingsSection }) { switch (section) { case "job-description": - return ; + return ( + + + + ); case "learnings": return ; case "people": 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..93010f768 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,43 @@ +import { Spinner } from "@houston-ai/core"; +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 { ContextEditorBox } from "../../context/context-editor"; -/** Instructions (CLAUDE.md) section. Read-only for non-managers. */ +/** + * Instructions (CLAUDE.md) section, drawn with the ONE standing-prose box + * (`ContextEditorBox`: always open, saves on blur). No heading of its own — + * the settings rail row already says "Job description", and no sibling + * section titles itself either — just the one-line helper over the box + * (explain ONCE). 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 }) - } - /> +
+

{t("instructions.helper")}

+ {instructions === undefined ? ( +
+ +
+ ) : ( + + saveInstructions.mutateAsync({ name: "CLAUDE.md", content: c }) + } + placeholder={t("instructions.placeholder")} + ariaLabel={t("subTabs.instructions")} + /> + )} +
); } 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 - : ""} - -
-
-