Files
hermes-agent/apps/desktop/src/app/chat/chat-swap-overlay.tsx
Siddharth Balyan 3c6c132366 Setup profile is minted by the backend and found by role, not by name (#119456)
* feat: setup profile is minted by the backend and found by role, not by name

The guided onboarding runs in a profile the desktop used to create itself
(profiles.create with a soul, "already exists" treated as success) and
recognise by the literal "hermes-setup". The upcoming setup toolset grants
catalog installs to that profile, so the marker that grants it must be
written only by the backend.

- profile.yaml carries `role: setup`; read_profile_meta / write_profile_meta /
  ProfileInfo know it; profiles.list and GET /api/profiles report it.
- hermes_cli/setup_profile.py: ensure (find by role, adopt a pre-role
  hermes-setup dir, else clone default + soul + role) and reset (soul,
  memories, skills back to the created state, in place). The soul text moves
  here from the renderer.
- tui_gateway/methods_onboarding.py: onboarding.ensure_setup_profile and
  onboarding.reset_setup_profile. Neither takes a name; profiles.create and
  profiles.configure already reject `role` (unknown key, 4000).
- Copies never inherit the role: --clone-all, profile import, and a
  distribution that ships profile.yaml drop it.
- setup.status for a named profile reports `ready` once the boot bootstrap
  settled. Since one host backend serves every profile (#118246) the
  desktop's setup-profile probe lands on this branch, which never set
  `ready`, and the kickoff waited forever.
- Desktop: SETUP_PROFILE, ensureSetupProfile(profiles.create) and
  composeSetupSoul are gone. store/setup-profile.ts holds the name the
  backend returned (or the roster's role row after a relaunch); kickoff,
  handoff and the build card use it. The dev reset calls the reset RPC.

* fix: write the setup soul as bytes so Windows keeps \n line endings

* refactor(desktop): drop the renderer's setup-profile store; the backend is the only owner

Kickoff reads the name straight from onboarding.ensure_setup_profile and records it on
$setupSession, which every later step already carries. The handoff recovery check reads
the roster row's role. No renderer module holds a setup-profile name or a fallback lookup.
2026-09-23 01:25:04 +05:30

77 lines
3.3 KiB
TypeScript

import { useEffect, useState } from 'react'
import { useOnboardingChatActive } from '@/components/onboarding-chat/assembly'
import { GlyphSpinner } from '@/components/ui/glyph-spinner'
import { useI18n } from '@/i18n'
import { cn } from '@/lib/utils'
// Shown over the conversation while the live gateway swaps to another profile's
// backend (lazily spawned). Keeps the last profile name through the fade-out so
// the label doesn't blank. Purely visual — pointer-events-none.
export function ChatSwapOverlay({ profile }: { profile: string | null }) {
const { t } = useI18n()
const onboarding = useOnboardingChatActive()
const [label, setLabel] = useState<null | string>(profile)
useEffect(() => {
if (profile) {
setLabel(profile)
}
}, [profile])
// The first run swaps profiles twice — into the setup profile, then into the
// task profile — and neither is a thing the user asked for or has a name for.
// "Waking up <setup profile>…" over a greeting that is already on screen reads
// as a stall in the one moment that has to feel instant. The flow narrates
// its own handoff (the handoff card) and the greeting is banked, so there
// is nothing here to cover.
if (onboarding) {
return null
}
return (
<div
aria-hidden
className={cn(
'pointer-events-none absolute inset-0 z-50 flex items-center justify-center transition-opacity duration-150 ease-out',
profile ? 'opacity-100' : 'opacity-0'
)}
>
<div className="flex items-center gap-2 bg-[color-mix(in_srgb,var(--dt-card)_92%,transparent)] px-4 py-2 font-mono text-[0.8125rem] text-foreground shadow-composer">
{/* Was a local 80ms setInterval + setState braille ticker — the same
mechanism class (per-tick DOM mutation scheduling style recalc)
that GlyphSpinner was rewritten to remove. `braille` is exactly the
frame set and 80ms cadence this used. `justify-start` keeps the
glyph left-aligned in its w-3 box the way the bare span was, and
`paused` restores the old "no ticking once the swap is done"
behaviour while the overlay fades out still mounted. */}
<GlyphSpinner className="w-3 justify-start text-(--ui-accent)" paused={!profile} spinner="braille" />
{t.composer.wakingProfile(label ?? '')}
</div>
</div>
)
}
// Subtle corner badge for a PAINT-FIRST wake (#89843): the stored transcript
// is already on screen and usable, but the active-profile gate hasn't caught
// up yet (shared-remote serves every profile through the primary socket).
// Deliberately quiet — a pill in the corner, not an overlay — because the
// content is real; only the background profile sync is still settling.
export function ChatSyncBadge({ profile }: { profile: string | null }) {
const { t } = useI18n()
if (!profile) {
return null
}
return (
<div
aria-live="polite"
className="pointer-events-none absolute right-3 top-2 z-30 flex items-center gap-1.5 rounded-full border border-border/50 bg-[color-mix(in_srgb,var(--dt-card)_92%,transparent)] px-2 py-0.5 font-mono text-[0.6875rem] text-muted-foreground shadow-composer"
>
<GlyphSpinner className="w-3 justify-start text-(--ui-accent)" spinner="braille" />
{t.desktop.hydrationSyncing(profile)}
</div>
)
}