).
+export interface PanelMetaRow {
+ label: ReactNode
+ value: ReactNode
+}
+
+export function PanelMeta({ className, rows }: { className?: string; rows: PanelMetaRow[] }) {
+ return (
+
+ {rows.map((row, i) => (
+
+
- {row.label}
+ - {row.value}
+
+ ))}
+
+ )
+}
+
+// Monospace content block (job prompt, etc.) — mirrors the inspector's
+// input/output blocks: subtle bg, no border.
+export function PanelBlock({ children, className }: { children: ReactNode; className?: string }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export type PanelPillTone = 'bad' | 'good' | 'muted' | 'warn'
+
+const PILL_TONE: Record = {
+ bad: 'bg-destructive/10 text-destructive',
+ good: 'bg-primary/10 text-primary',
+ muted: 'bg-foreground/10 text-muted-foreground',
+ warn: 'bg-amber-500/10 text-amber-600 dark:text-amber-300'
+}
+
+export function PanelPill({ children, tone = 'muted' }: { children: ReactNode; tone?: PanelPillTone }) {
+ return (
+
+ {children}
+
+ )
+}
+
+// Self-describing centered "+" that sits as the LAST item in a PanelList. The
+// label rides aria/title only — no visible text.
+export function PanelAddButton({ icon = 'add', label, onClick }: { icon?: string; label: string; onClick: () => void }) {
+ return (
+
+ )
+}
+
+// Visible ghost action for a detail header (cron pause/resume/trigger, …).
+export function PanelAction({
+ children,
+ disabled,
+ icon,
+ onClick
+}: {
+ children: ReactNode
+ disabled?: boolean
+ icon: string
+ onClick: () => void
+}) {
+ return (
+
+ )
+}
diff --git a/apps/desktop/src/app/profiles/index.tsx b/apps/desktop/src/app/profiles/index.tsx
index c66bfe8e362..3fb4d5e999b 100644
--- a/apps/desktop/src/app/profiles/index.tsx
+++ b/apps/desktop/src/app/profiles/index.tsx
@@ -1,8 +1,10 @@
+import { useStore } from '@nanostores/react'
import type * as React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { PageLoader } from '@/components/page-loader'
import { Button } from '@/components/ui/button'
+import { Codicon } from '@/components/ui/codicon'
import {
Dialog,
DialogContent,
@@ -18,21 +20,34 @@ import {
createProfile,
deleteProfile,
getProfiles,
- getProfileSetupCommand,
getProfileSoul,
type ProfileInfo,
renameProfile,
updateProfileSoul
} from '@/hermes'
import { useI18n } from '@/i18n'
-import { AlertTriangle, Pencil, Save, Terminal, Trash2, Users } from '@/lib/icons'
+import { AlertTriangle, Save } from '@/lib/icons'
+import { profileColorSoft, resolveProfileColor } from '@/lib/profile-color'
import { slug } from '@/lib/sanitize'
import { cn } from '@/lib/utils'
import { notify, notifyError } from '@/store/notifications'
+import { $profileColors } from '@/store/profile'
import { useRefreshHotkey } from '../hooks/use-refresh-hotkey'
-import { OverlayMain, OverlayNewButton, OverlaySidebar, OverlaySplitLayout } from '../overlays/overlay-split-layout'
-import { OverlayView } from '../overlays/overlay-view'
+import {
+ Panel,
+ PanelAddButton,
+ PanelBody,
+ PanelDetail,
+ PanelEmpty,
+ PanelHeader,
+ PanelList,
+ PanelListRow,
+ PanelMeta,
+ PanelPill,
+ PanelRowMenu,
+ PanelSectionLabel
+} from '../overlays/panel'
const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/
@@ -49,7 +64,9 @@ export function ProfilesView({ onClose }: ProfilesViewProps) {
const p = t.profiles
const [profiles, setProfiles] = useState(null)
const [selectedName, setSelectedName] = useState(null)
+ const [query, setQuery] = useState('')
const [createOpen, setCreateOpen] = useState(false)
+ const [pendingRename, setPendingRename] = useState(null)
const [pendingDelete, setPendingDelete] = useState(null)
const [deleting, setDeleting] = useState(false)
@@ -83,6 +100,18 @@ export function ProfilesView({ onClose }: ProfilesViewProps) {
return profiles.find(p => p.name === selectedName) ?? profiles[0] ?? null
}, [profiles, selectedName])
+ const visibleProfiles = useMemo(() => {
+ const q = query.trim().toLowerCase()
+
+ if (!profiles || !q) {
+ return profiles ?? []
+ }
+
+ return profiles.filter(
+ profile => profile.name.toLowerCase().includes(q) || (profile.model ?? '').toLowerCase().includes(q)
+ )
+ }, [profiles, query])
+
const handleCreate = useCallback(
async (name: string, cloneFrom: null | string) => {
const trimmed = name.trim()
@@ -140,46 +169,79 @@ export function ProfilesView({ onClose }: ProfilesViewProps) {
}, [p, pendingDelete, refresh])
return (
-
+
{!profiles ? (
+ ) : profiles.length === 0 ? (
+ setCreateOpen(true)} size="sm">
+ {p.newProfile}
+