* 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.
77 lines
3.3 KiB
TypeScript
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>
|
|
)
|
|
}
|