ProjectScreen never got the unified-upper-left-avatar (PR #305)
treatment: it still showed the project icon upper-left (doubling as Back via
screenCore.reset()), the name in the middle, and a bespoke
KrillApp.Avatar.icon() + DropdownMenu add-child control on the right —
visually divergent from every node editor. It also needed a one-off
behaviour: adding a child from the project must create it directly (no editor
redirect) and must not auto-wire the project as the new child’s source,
unlike every other add-child path.
Two divergent avatar affordances where one shared component belonged (same
class of issue as #305), plus a wiring-semantics question: the server’s
withParentSourceDefault auto-wires a parent as a new child’s source
whenever the created node has empty sources. Every add-child path leans on
this. ProjectScreen needed the opposite — a child with no source wiring.
EditorAvatarMenu gained an optional onSelect, resolved by the pure
resolveAvatarSelect (default = createChildAndEdit, so the two editor
call sites are byte-for-byte unchanged). ProjectScreen passes
screenCore.createChild.ScreenCore.createChild(type): build the child via the shared
buildChildNode (empty sources), create(), then — for targeting
metadata only — issue an edited carrying clearedWiring(meta) (new pure
shared helper: empty sources/executionSource, nodeAction preserved).ProjectScreen wrapped in a Box; title-only header showing the project
name with the NodeHeader leading spacer; EditorAvatarMenu overlaid at
TopStart; a Close button (screenCore.reset()) added to
ProjectBottomBar; empty-project hint cards switched to createChild.Conscious decision — convergence beats a reconciliation wait. The design
originally specified a reactive wait (create, then await the server-applied
default, then clear) to avoid a create-vs-clear ordering race. Tracing the
server gate (isNewNode = persisted == null, default applied only when
sources.isEmpty()) showed both orderings converge to empty sources: the
write the server sees first becomes the “new” node (default applied to its
empty sources), the second is a non-new update that persists empty sources.
So a synchronous create() + edited(clearedWiring) is correct without any
scope/coroutine machinery, matching the existing createChildAndEdit
shape. A brief transiently-wired window may occur; harmless — a just-created
child has no snapshot/logic to fire in that window. This supersedes the
design doc’s reactive approach (doc updated to match).
EditorAvatarMenu.onSelect
via a pure resolver) instead of forking — and keep the decision logic pure
(resolveAvatarSelect, clearedWiring) so it is unit-testable under the
no-flaky-tests rules without a Compose/coroutine harness.isNewNode gate already makes create/clear order-independent.
Verifying the convergence deleted an entire reactive-wait subsystem from
the plan. Check whether the invariant already holds before engineering a
guard for it.