Previously on The V3 Saga
The Lizard Brain had won. “Fuckit” became doctrine. The navigation experiment was deleted with prejudice. But a question lingered in the morning light: “What is the sidebar, really?”
The Morning Question
09:00 — Good morning. Oskar watches over us. Context at 36%. Fresh start.
“UI sagas,” says the developer. Fragments that drill down. Fragments that pick. Fragments that return. Dashboard → Agents → Store → Credential → back back back.
The Squirrel stirs. “State machines! Event sourcing! Actor model!”
The Lizard Brain blinks. “Mobile apps.”
The Mobile Revelation
09:30 — The sidebar is a phone. One screen at a time. Stack-based navigation. Back always pops. No split attention.
MOBILE DESKTOP
─────────────────────────────── ───────────────────────────────
One thing at a time Many things visible
Full screen takeover Windows, panels, sidebars
Stack-based (linear) Spatial (parallel)
Back button is THE way Click anywhere, close any
Navigation is TEMPORAL Navigation is SPATIAL
SwiftUI struggles to port mobile to desktop because the paradigms are fundamentally different. We’re doing the opposite: embedding a mobile context inside a desktop app.
The chat stays visible. The sidebar is the phone. Simple.
The Sanity Test
10:00 — A principle crystallizes:
“If every action can be issued by user OR AI, you need a sane structure. Or you will have insane AI.”
User clicks button → Navigate("stores", {mode: "pick"})
AI decides to help → Navigate("stores", {mode: "pick"})
Same code path. Same state transitions. Sane AI.
The Squirrel tries: “But REST—”
“Forget REST,” says the Lizard Brain. “REST is resource-centric. We are action-centric. Server has live objects. Client has signals. Fragments arrive ready to render.”
The Unusual Features
10:30 — Dialogr is not a normal web app.
Normal web app: Request → Load from DB → Hydrate → Do thing → Save → Forget.
Dialogr: Objects ALREADY LIVE. Just reference them. Do thing. They STAY ALIVE.
Immutable versions. Natural GC. Tab A keeps using Agent v1 while Tab B gets Agent v2. No locks. No races. Go’s garbage collector does the cleanup.
func HandleQuestion(msg string) {
agent := agentDomain.Find(agentID) // grab current version
agent.Run(msg, sse) // use it
// reference goes out of scope
// next question gets fresh Find()
}
Long-running jobs capture their context at start. Admin changes config mid-job? Job keeps using v1. Want new config? Cancel and restart explicitly.
No magic. No surprises.
The Two Controllers
11:00 — The tab has two controllers sharing the same domain objects:
┌─────────────────────────┐ ┌─────────────────────────┐
│ CHAT CONTROLLER │ │ SIDEBAR CONTROLLER │
│ - message history │ │ - nav stack │
│ - tool traces │ │ - current screen │
│ - streaming state │ │ - pending actions │
│ │ │ │
│ For: conversations │ │ For: admin config │
└─────────────────────────┘ └─────────────────────────┘
Chat shows expandable traces. Trace has “Edit Tool” button (admin only). Click it → sidebar navigates to tool editor. Two controllers, same objects, different concerns.
The Datastar Alignment
11:30 — The Lizard Brain was right three months ago.
Signal hierarchy = Controller hierarchy:
chat.*— Chat Controller statesidebar.*— Sidebar Controller state_*— Local/visual only (no server trip for a class change!)
Lazy loading by interaction. DOM is the interaction surface. Server decides what’s on it, when.
INITIAL: [⚡ proximity_search 12 results [▼]]
(no details in DOM)
CLICK EXPAND → @get('/trace/abc/expand')
SERVER SENDS: Full trace with SQL, params, action buttons
(NOW it's in DOM, ready for interaction)
View Transitions for mobile-style animations. data-view-transition wraps fragment swaps. Old card slides out, new slides in. Server hints direction.
The Orthogonal Templates
12:00 — The composition problem. Store wants to pick a credential. Credential domain owns the picker template. Who knows what?
The Squirrel: “Callbacks! Props! Context providers!”
The Lizard Brain: “One struct.”
type PickerConfig struct {
ReturnTo string // where to go after pick
ResultSignal string // which signal to write result to
AllowCreate bool
}
Template doesn’t know or care who called it. Same template, different callers:
// Store picking credential
credential.CredentialPicker(creds, PickerConfig{
ReturnTo: "/store/123/picked",
ResultSignal: "sidebar.picked",
})
// Agent picking credential
credential.CredentialPicker(creds, PickerConfig{
ReturnTo: "/agent/456/picked",
ResultSignal: "sidebar.picked",
})
Contract explicit. Compiler checks. Template reusable. DRY.
The Document
12:30 — 1000+ lines of architecture written to v3/docs/sidebar-navigation.md. Nine key decisions:
- Sidebar = mobile nav controller
- Chat always visible
- Actions = Tools (same API for user and AI)
- Full screen = popup, not takeover
- Constrain complexity
- Signal hierarchy
- Lazy loading
- Handlers read signals, return signals + fragments
- Orthogonal templates via PickerConfig
The Squirrel is quiet. The Lizard Brain nods.
The Tally
Architecture designed: 1 (complete)
Documentation written: ~1000 lines
REST abandoned: officially
Patterns discovered: 5+
Type safety: "good enough" (strings are honest)
Coffee consumed: insufficient
Some mornings you fix prod bugs. Some mornings you design the future.
This was a designing morning. 🦎
Next episode: The View Transition — In which the credential form learns to slide, and theory becomes practice.
See also:
The Saga (in which architecture becomes practice):
- The View Transition Strikes Back - Theory meets browser, crossfade wins
- The Lizard Brain vs The Caffeinated Squirrel - Where “fuckit” became doctrine
- The URL Awakens - Where URL became the source of truth
The References (mobile-style web architecture):
- SwiftUI Navigation - The mobile paradigm we’re embedding
- Datastar Signals - Signal hierarchy done right
- Go Garbage Collection - The cleanup you don’t have to think about
