The avatar UI element and the KrillApp.Client swarm node were the same
thing — both lived at installId(), and the avatar’s menu was computed by
NodeChildren.load(KrillApp.Client). That conflation was load-bearing for
the FTUE flow (the wide “what would you like to do?” menu shown in the
avatar’s speech bubble) but it blocked Phase 1+ of local-first-onboarding,
which needs the Client to be a normal swarm node hidden when childless and
re-rendered when the user starts building locally — distinct from the
always-visible avatar.
NodeChildren.load(node: Node) dispatched on node.type and the
KrillApp.Client arm returned a wide menu (Client.About, Server,
Server.Peer, plus the union of every type currently in the swarm). That
menu was the avatar menu in everything but name; it had nothing to do with
“things you can attach to a Client node” — it was “things the user wants to
poke from the avatar.” The arm even cleared its result when
ClientMetaData.ftue == true, which was UI gating leaking into a sealed-type
dispatch. Because KrillApp (the sealed root) is abstract, there was no
way to address the avatar’s intent without piggybacking on a concrete
subtype, and KrillApp.Client was the convenient hook.
Phase 0 of local-first-onboarding (krill#255, krill-oss#67) introduces a
new sealed-root menu lookup and reduces the Client-node menu to its real
shape. Three coordinated changes:
shared/.../NodeChildren.kt gains an overload
fun load(rootType: KClass<out KrillApp>): List<KrillApp> that returns
the wide avatar menu when rootType == KrillApp::class and an empty list
otherwise. The body is the same union-of-types-in-swarm computation that
used to live in the KrillApp.Client arm of load(node). The
KrillApp.Client arm itself is reduced to Project +
MenuCommand.KeepBuildingSwarm — the focused per-node menu the design
mandates. Update / Delete are intentionally absent (legacy
behaviour for this type).
shared/.../KrillApp.kt in krill-sdk 0.0.19 added
MenuCommand.KeepBuildingSwarm as a new sealed subtype of MenuCommand
(krill-oss#67). Every when (KrillApp) site in this repo had to grow a
branch for it; the only one that wasn’t already exhaustive-via-else
was IconManager.nodeIcon(node), which now maps KeepBuildingSwarm to
the placeholder shrimp drawable per Ben’s note on the upstream issue
(final icon to be supplied before Phase 2).
composeApp/.../client/ClientScreen.kt routes the avatar’s
speech-bubble menu computation through the new overload. The
discriminator is node.id == installId() — the avatar is the only
selection that can produce that id in Phase 0 (the Client node is not
rendered in the graph until Phase 1 lights it up via NodeLayout).
When that condition holds the speech bubble queries
children.load(KrillApp::class); otherwise it stays on the legacy
children.load(node) path.
ClientNodeManager.init() is unchanged — the Client node is still created
on first launch with ClientMetaData(ftue = true) and kept as the durable
home of the FTUE flag. The TOS dialog and PIN entry continue to gate the
speech-bubble menu render at the UI layer, so the legacy
if (meta.ftue) distinct.clear() defence inside the lookup wasn’t needed
in either of the two new code paths.
Regression coverage lives in
shared/src/jvmTest/.../NodeChildrenTest.kt (six cases): two for the new
sealed-root API (sealed-root-only matching, and the swarm-walk inclusions
KClass rejection. Tests use mockk to stub ClientNodeManager
because the real one needs HttpClient / FileOperations /
NodeObserver wiring that has nothing to do with menu computation, and
the project’s “tests that don’t flake” rules forbid touching production
filesystem paths or wall-clock for setup.KrillApp is sealed and abstract — you can’t
pass an instance of it. The natural workaround is to dispatch on a
KClass, which is what the new load(rootType: KClass<out KrillApp>)
overload does. If a future feature wants a similar “root-of-the-hierarchy
intent” affordance, mirror the shape: a KClass-keyed overload that
rejects every subclass and only honours the root token. Don’t synthesize
a placeholder Node — the meta field would have no honest concrete
type.when arm of a sealed-type
dispatch. The legacy if (meta.ftue) distinct.clear() inside
NodeChildren.load(KrillApp.Client) was a fence-line check duplicating
the speech-bubble’s own render gate; both fired on the same
ClientMetaData.ftue value, and the inner one couldn’t be tested
without standing up a full ClientNodeManager. New code should keep
per-type lookups pure-ish: take a node, return its menu, leave
“should I render this?” to the caller.MenuCommand.KeepBuildingSwarm discriminator and bumped krill-sdk to
0.0.19, but the release-sdk workflow in krill-oss dispatches via the
stale bsautner/krill org and failed with HTTP 401, so the artifact
never reached Maven Central. The downstream PR for this issue had to
use a local includeBuild("../krill-oss") to compile, with the 0.0.19
bump committed and the includeBuild reverted before push. Friction
filed at krill-oss#71. Future cross-repo SDK additions should verify
Central publication with curl -L https://repo1.maven.org/maven2/<group>/<artifact>/<version>/
before the downstream tracker is unblocked, not after.when (node.type) exhaustiveness is real, but when (command) over
KrillApp is incidentally non-exhaustive. The new menu command was
caught by IconManager’s exhaustive when (good); the
KrillScreen.kt-level when (command.value) blocks already had else
arms and silently absorbed the new command. That’s fine for Phase 0
(no user path leads to the new command yet), but anyone wiring it up in
Phase 1 should audit those else arms instead of trusting them — at
least one of them routes unrecognised commands into NodeListScreen(c),
which would crash trying to render a non-existent node screen for a
pure menu command.