103 lines
6.1 KiB
Markdown
103 lines
6.1 KiB
Markdown
# Profile Modal — Handoff
|
||
|
||
A global, reusable profile detail/settings modal for Hermes Desktop (Electron + React renderer). Built on branch `pr/746`. This doc is a self-contained handoff so another agent can continue the work.
|
||
|
||
## What it is
|
||
|
||
A single **80vw × 80vh** modal, mounted once at the app root, opened from anywhere via a context hook. It has a **left section-nav** (Profile / Wallet / Advanced) and a scrollable right content pane. It replaces the old inline "appearance" modal that used to live inside the Agents screen.
|
||
|
||
It was built to grow — the user has "more plans with profile" (e.g. a real Wallet screen).
|
||
|
||
## Files
|
||
|
||
| File | Role |
|
||
| --- | --- |
|
||
| `src/renderer/src/components/profile/ProfileModal.tsx` | The modal UI (header, left nav, panes, mutations). |
|
||
| `src/renderer/src/components/profile/ProfileModalProvider.tsx` | Mounts the modal at app root; holds open state. |
|
||
| `src/renderer/src/components/profile/ProfileModalContext.ts` | Context + `useProfileModal()` hook + `OpenProfileOptions` type. |
|
||
| `src/renderer/src/App.tsx` | `<ProfileModalProvider>` wraps the app (inside `FontProvider`). |
|
||
| `src/renderer/src/screens/Layout/ProfileSwitcher.tsx` | Sidebar popover; active profile button calls `openProfile`. |
|
||
| `src/renderer/src/screens/Agents/Agents.tsx` | Profiles screen; pencil/card edit calls `openProfile` (inline modal removed). |
|
||
| `src/renderer/src/assets/main.css` | All `.profile-modal-*` styles (search that prefix). |
|
||
| `src/renderer/src/assets/icons/index.tsx` | Re-exports lucide icons; added `User`, `Wallet`. |
|
||
| `src/shared/i18n/locales/en/agents.ts` | i18n keys (English source; see i18n note). |
|
||
| `lat.md/sidebar-navigation.md` | Architecture doc, section "Profile detail modal" (keep in sync). |
|
||
|
||
## How to open it
|
||
|
||
```ts
|
||
import { useProfileModal } from "../../components/profile/ProfileModalContext";
|
||
|
||
const { openProfile } = useProfileModal();
|
||
|
||
openProfile("fatha", {
|
||
onChanged: reloadMyList, // called after every successful mutation
|
||
onDeleted: (name) => { /* e.g. fall back to "default" if it was active */ },
|
||
});
|
||
```
|
||
|
||
The modal **self-loads** its data via `window.hermesAPI.listProfiles()` (there is no single-profile IPC) and re-reads after every mutation, so callers only need `onChanged`/`onDeleted` to refresh their own lists.
|
||
|
||
## Current structure
|
||
|
||
**Header:** small `ProfileAvatar` (28px) + the profile **name only** (no "Edit {name}") + close (X).
|
||
|
||
**Left nav** (`PROFILE_SECTIONS` in `ProfileModal.tsx`), each item = icon + label:
|
||
- **Profile** (`User` icon) — large avatar + gateway dot, name + `default` tag, Upload/Remove image, the provider/model/skills/gateway chips, and the color swatches.
|
||
- **Wallet** (`Wallet` icon) — **placeholder only**: centered "Coming soon" (`.profile-modal-coming-soon`). Not yet implemented.
|
||
- **Advanced** (`Settings` icon) — the Delete Profile danger zone.
|
||
|
||
**Behaviors:**
|
||
- Every profile is editable (avatar + color), **including `default`**.
|
||
- Only `default` **cannot be deleted** — its Advanced pane shows `agents.defaultNotDeletable` instead of the delete button.
|
||
- Dismiss: overlay click, Escape, close (X), or Done button.
|
||
|
||
## Profile data shape (`listProfiles()`)
|
||
|
||
```ts
|
||
{ name, path, isDefault, isActive, model, provider, hasEnv, hasSoul,
|
||
skillCount, gatewayRunning, color?, avatar? }
|
||
```
|
||
|
||
## IPC available on `window.hermesAPI` (no new IPC was added)
|
||
|
||
- `listProfiles()` → `ProfileInfo[]`
|
||
- `setProfileColor(name, color)` → `{ success, error? }`
|
||
- `setProfileAvatar(name, dataUrl)` → `{ success, error? }`
|
||
- `removeProfileAvatar(name)` → `{ success, error? }`
|
||
- `deleteProfile(name)` → `{ success, error? }`
|
||
- `createProfile(name, clone)`, `setActiveProfile(name)` (used elsewhere)
|
||
|
||
Avatar files are converted with `fileToAvatarDataUrl` (`src/renderer/src/utils/imageResize.ts`). Colors come from `PROFILE_COLORS` (`src/shared/profileColors.ts`).
|
||
|
||
## How to add a new section (e.g. build out Wallet)
|
||
|
||
1. Add the id to the `ProfileSection` union and an entry to `PROFILE_SECTIONS` (`{ id, labelKey, Icon }`) in `ProfileModal.tsx`.
|
||
2. Add a `{section === "<id>" && (<div className="profile-modal-pane"> … </div>)}` block in the content area.
|
||
3. Add the nav label key to `src/shared/i18n/locales/en/agents.ts` (e.g. `sectionWallet`).
|
||
4. Style with existing `.profile-modal-*` classes or add new ones in `main.css`.
|
||
|
||
## Project conventions (important)
|
||
|
||
- **i18n**: source/fallback locale is `en`. New UI strings go in `src/shared/i18n/locales/en/agents.ts`; the other 10 locales (`es, he, id, ja, pl, pt-BR, pt-PT, tr, zh-CN, zh-TW`) **fall back to en automatically** (`FALLBACK_LOCALE` in `src/shared/i18n/index.ts`), so you don't have to edit them to ship — translate later.
|
||
- **lat.md sync (required)**: this repo uses [lat.md]. After any code change, update the relevant section in `lat.md/` and run `lat check` (a Stop hook enforces this). The profile modal is documented in `lat.md/sidebar-navigation.md` → "Profile detail modal". Wiki links like `[[src/.../ProfileModal.tsx#ProfileModal]]` must resolve.
|
||
- **Node for tooling**: the repo's `.nvmrc` pins Node 21, but `vitest`/typecheck need **Node ≥22** (`nvm use 22.19.0`) — Node 21 fails to load `vitest.config.ts` (`ERR_REQUIRE_ESM`).
|
||
- **react-refresh**: keep hooks/context in `.ts` files separate from the provider component (`.tsx`) — that's why context/hook and provider are split.
|
||
|
||
## Verify
|
||
|
||
```bash
|
||
nvm use 22.19.0
|
||
npm run typecheck:web
|
||
npx eslint src/renderer/src/components/profile/*.ts src/renderer/src/components/profile/*.tsx
|
||
lat check
|
||
```
|
||
|
||
Manual (`npm run dev`, Node ≥22): sidebar profile popover → click the active profile → modal opens; switch nav sections; edit color/avatar (reflects live in sidebar + Agents); Wallet shows "Coming soon"; Advanced deletes non-default profiles (default shows the not-deletable note).
|
||
|
||
## Known open items / ideas
|
||
|
||
- **Wallet** is a stub — needs the real screen + likely new IPC for wallet/balance.
|
||
- Advanced currently holds only Delete; the user mentioned "wallet etc. in advanced … add later," so more settings can live there.
|
||
- No automated tests yet for `ProfileModal` (the dashboard adapter has tests as a pattern to follow under `src/renderer/src/screens/Chat/dashboardEventAdapter.test.ts`).
|