From de9938c71b58a13b12d389b8264d16106be1a942 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 14 Jun 2026 12:34:44 +0800 Subject: [PATCH 1/3] feat: add personality module with IDENTITY.md / SOUL.md / USER.md support Add a personality module that loads three markdown files (IDENTITY.md, SOUL.md, USER.md) from .reasonix/personality/ and folds them into the system prompt to shape the agent's identity, behaviour, and user awareness. Backend (Go): - New internal/personality/ package: Load, Compose, ReadFile, WriteFile, DeleteFile, List, ProjectDirs - PersonalityConfig with Enabled toggle in config.Config - Integrated in boot.go: loads personality after output style, before memory/skills, when [personality] enabled = true - Desktop settings API: 5 new bound methods - Default: disabled (backward compatible) Frontend (TypeScript/React): - New 'personality' tab in SettingsPanel with enable toggle and three-file editor (IDENTITY.md / SOUL.md / USER.md) - PersonalityFileView and PersonalitySettingsView types - Full bridge bindings + mock implementation for browser dev mode - Locale keys added to en.ts, zh.ts, zh-TW.ts Change: ErrorBoundary now renders crash details instead of null (blank page) for easier debugging. Template examples in docs/personality-templates/. --- .../frontend/src/components/ErrorBoundary.tsx | 18 +- .../frontend/src/components/SettingsPanel.tsx | 196 ++++++++++++++++- desktop/frontend/src/lib/bridge.ts | 35 +++ desktop/frontend/src/lib/types.ts | 14 +- desktop/frontend/src/locales/en.ts | 13 ++ desktop/frontend/src/locales/zh-TW.ts | 13 ++ desktop/frontend/src/locales/zh.ts | 13 ++ desktop/settings_app.go | 96 ++++++++ .../personality-templates/IDENTITY.example.md | 7 + docs/personality-templates/README.md | 26 +++ docs/personality-templates/SOUL.example.md | 8 + docs/personality-templates/USER.example.md | 7 + internal/boot/boot.go | 12 + internal/config/config.go | 24 ++ internal/personality/personality.go | 207 ++++++++++++++++++ internal/personality/personality_test.go | 147 +++++++++++++ reasonix.example.toml | 6 + 17 files changed, 834 insertions(+), 8 deletions(-) create mode 100644 docs/personality-templates/IDENTITY.example.md create mode 100644 docs/personality-templates/README.md create mode 100644 docs/personality-templates/SOUL.example.md create mode 100644 docs/personality-templates/USER.example.md create mode 100644 internal/personality/personality.go create mode 100644 internal/personality/personality_test.go diff --git a/desktop/frontend/src/components/ErrorBoundary.tsx b/desktop/frontend/src/components/ErrorBoundary.tsx index 2861b61fe..eae0bb717 100644 --- a/desktop/frontend/src/components/ErrorBoundary.tsx +++ b/desktop/frontend/src/components/ErrorBoundary.tsx @@ -1,11 +1,11 @@ import { Component, type ReactNode } from "react"; import { reportCrash } from "../lib/crash"; -export class ErrorBoundary extends Component<{ children: ReactNode }, { crashed: boolean }> { - state = { crashed: false }; +export class ErrorBoundary extends Component<{ children: ReactNode }, { crashed: boolean; error: string }> { + state = { crashed: false, error: "" }; - static getDerivedStateFromError() { - return { crashed: true }; + static getDerivedStateFromError(error: unknown) { + return { crashed: true, error: String(error) }; } componentDidCatch(error: unknown, info: { componentStack?: string | null }) { @@ -13,6 +13,14 @@ export class ErrorBoundary extends Component<{ children: ReactNode }, { crashed: } render() { - return this.state.crashed ? null : this.props.children; + if (this.state.crashed) { + return ( +
+

React Error

+
{this.state.error}
+
+ ); + } + return this.props.children; } } diff --git a/desktop/frontend/src/components/SettingsPanel.tsx b/desktop/frontend/src/components/SettingsPanel.tsx index 222b0de31..d7d172f0c 100644 --- a/desktop/frontend/src/components/SettingsPanel.tsx +++ b/desktop/frontend/src/components/SettingsPanel.tsx @@ -21,7 +21,7 @@ import { TEXT_SIZES, applyTextSize, getTextSize, type TextSize } from "../lib/te import { FONT_FAMILIES, applyFontFamily, getFontFamily, getCustomFontName, setCustomFontName, type FontFamily } from "../lib/fontFamily"; import { getDisplayMode, onDisplayModeChange, setDisplayMode as setLocalDisplayMode } from "../lib/displayMode"; import { DEFAULT_STATUS_BAR_ITEMS, normalizeStatusBarItems, type StatusBarItemId } from "../lib/statusBarItems"; -import type { BotAllowlistView, BotConnectionView, BotInstallStartResult, BotSettingsView, HookConfigView, HooksSettingsView, NetworkView, ProviderView, SettingsTab, SettingsView } from "../lib/types"; +import type { BotAllowlistView, BotConnectionView, BotInstallStartResult, BotSettingsView, HookConfigView, HooksSettingsView, NetworkView, PersonalitySettingsView, ProviderView, SettingsTab, SettingsView } from "../lib/types"; import { InlineConfirmButton } from "./InlineConfirmButton"; import { Tooltip } from "./Tooltip"; import { AnchoredPopover } from "./AnchoredPopover"; @@ -32,7 +32,7 @@ import { SoundSelect } from "./SoundSelect"; import { getSuccessPreference, setSuccessPreference, getAttentionPreference, setAttentionPreference, playSuccessChime, playAttentionChime, type SoundWavPref } from "../lib/sound"; import { ModalCloseButton } from "./ModalCloseButton"; -const SETTINGS_TABS: SettingsTab[] = ["general", "models", "bots", "mcp", "skills", "memory", "hooks", "permissions", "sandbox", "network", "appearance", "updates"]; +const SETTINGS_TABS: SettingsTab[] = ["general", "models", "bots", "mcp", "skills", "memory", "hooks", "permissions", "sandbox", "network", "appearance", "personality", "updates"]; export type SettingsInitialFocus = { target: "bot-allowlist"; connectionId?: string }; // SettingsPanel is the desktop settings centre — a centred modal with left @@ -187,6 +187,7 @@ export function SettingsPanel({ /> )} + {tab === "personality" && } {tab === "updates" && s && ( Promise) => Promise; }; +// --- Personality section --- + +const PERSONALITY_FILE_NAMES = ["IDENTITY.md", "SOUL.md", "USER.md"]; + +const PERSONALITY_FILE_LABELS: Record = { + "IDENTITY.md": "IDENTITY.md — Who you are", + "SOUL.md": "SOUL.md — Your behavioural traits", + "USER.md": "USER.md — About the user", +}; + +const PERSONALITY_FILE_HINTS: Record = { + "IDENTITY.md": "Define the agent's core identity, beliefs, and values. This replaces the default 'You are Reasonix...' framing.", + "SOUL.md": "Describe behavioural traits, communication style, emotional tone, and quirks. This shapes how the agent expresses itself.", + "USER.md": "Describe the user — their role, preferences, goals, and context. The agent uses this to personalise responses.", +}; + +function PersonalitySection() { + const t = useT(); + const [settings, setSettings] = useState(null); + const [busy, setBusy] = useState(false); + const [err, setErr] = useState(null); + const [activeFile, setActiveFile] = useState(PERSONALITY_FILE_NAMES[0]); + const [editContent, setEditContent] = useState(""); + const [dirty, setDirty] = useState(false); + + const load = async () => { + try { + const s = await app.GetPersonalitySettings(); + setSettings(s); + const file = s.files.find((f) => f.name === activeFile); + if (file && !dirty) { + setEditContent(file.content); + } + } catch (e) { + setErr(String(e)); + } + }; + + useEffect(() => { void load(); }, []); + + const selectFile = (name: string) => { + setActiveFile(name); + setDirty(false); + const file = settings?.files.find((f) => f.name === name); + setEditContent(file?.content ?? ""); + }; + + const save = async () => { + setBusy(true); + setErr(null); + try { + await app.SavePersonalityFile(activeFile, editContent); + setDirty(false); + await load(); + } catch (e) { + setErr(String(e)); + } finally { + setBusy(false); + } + }; + + const remove = async (name: string) => { + setBusy(true); + setErr(null); + try { + await app.DeletePersonalityFile(name); + if (activeFile === name) { + const next = PERSONALITY_FILE_NAMES.find((n) => n !== name) ?? PERSONALITY_FILE_NAMES[0]; + setActiveFile(next); + setEditContent(""); + } + await load(); + } catch (e) { + setErr(String(e)); + } finally { + setBusy(false); + } + }; + + const toggleEnabled = async (enabled: boolean) => { + setBusy(true); + setErr(null); + try { + await app.SetPersonalityEnabled(enabled); + await load(); + } catch (e) { + setErr(String(e)); + } finally { + setBusy(false); + } + }; + + return ( + + {err &&
{err}
} + + +
+ + +
+
+ + {!settings ? ( +
{t("common.loading")}
+ ) : ( + <> + +
+ {PERSONALITY_FILE_NAMES.map((name) => { + const file = settings.files.find((f) => f.name === name); + return ( + + ); + })} +
+
+ + +