detached 65114be

refactor(daemon): define render coordinator contract

Inert S1 contract: coordinator state, options, deadline bounds, resize
metadata ownership, lifecycle/sync/preview signatures, and the session
installation hook. No scheduling, composition, or producer migration —
behavior arrives in the GREEN slice.
commit
65114bebf59dfcbed120e187bc2de380987b31c4
author
brice · · 2026-07-11 14:41 UTC
parent
1c718e0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +167 -0
A internal/usecase/daemon/render_coordinator.go +165 -0
@@ -0,0 +1,165 @@
1+package daemon
2+
3+import (
4+ "sync"
5+ "time"
6+
7+ "github.com/bnema/vev/internal/domain"
8+ "github.com/bnema/vev/internal/ports"
9+)
10+
11+// S1 render coordinator contract. One coordinator serves one attached
12+// session: producers mutate authoritative state and publish an invalidation;
13+// only the coordinator decides when the transitional composition target runs.
14+// Producers never compose or send. This file currently carries the inert
15+// contract — state, options, and signatures exist so the behavioral
16+// specification compiles — while wake scheduling, ACK gating, synchronized
17+// output handling, and producer migration land in the GREEN slice.
18+
19+// Coordinator deadline bounds. Urgent transitions (input echo, overlay state
20+// changes) must become visible within urgentRenderDeadline; bulk output
21+// coalesces inside the adaptive [minOutputRenderDeadline,
22+// maxOutputRenderDeadline] window.
23+const (
24+ urgentRenderDeadline = 2 * time.Millisecond
25+ minOutputRenderDeadline = 8 * time.Millisecond
26+ maxOutputRenderDeadline = 16 * time.Millisecond
27+)
28+
29+// invalidationClass ranks a producer transition's latency demand.
30+type invalidationClass uint8
31+
32+const (
33+ // invalidateOutput is bulk PTY/screen churn served by the adaptive window.
34+ invalidateOutput invalidationClass = iota
35+ // invalidateUrgent is an interactive transition served within 2ms.
36+ invalidateUrgent
37+)
38+
39+// renderInvalidation is one producer-published state transition.
40+type renderInvalidation struct {
41+ class invalidationClass
42+ // reset requests a sticky FullRedraw: once requested it survives
43+ // coalescing and ACK deferral until a wake actually delivers it.
44+ reset bool
45+ // producer identifies the publishing call-site for provenance.
46+ producer string
47+}
48+
49+// renderWake is one coalesced composition request delivered to the
50+// transitional composition target (replaced by the S2 pipeline).
51+type renderWake struct {
52+ reset bool
53+ urgent bool
54+ coalesced int
55+ // watchdog marks a flush forced by the synchronized-output watchdog.
56+ watchdog bool
57+}
58+
59+// resizeRequestMetadata is the coordinator-owned latest requested resize
60+// state: S3's transaction entry point reads it through resizeSnapshot. It is
61+// metadata ownership only — S1/S2 dispatch stays with the retained PR #71
62+// attachment timer path.
63+type resizeRequestMetadata struct {
64+ size domain.Size
65+ source *attachedClient
66+ epoch uint64
67+}
68+
69+// renderCoordinatorOptions wires one coordinator instance.
70+type renderCoordinatorOptions struct {
71+ clock ports.Clock
72+ // wake is the transitional composition target.
73+ wake func(renderWake)
74+ // ackReady reports whether the attachment may compose another output
75+ // state (the outputStateStream window has capacity).
76+ ackReady func() bool
77+ // syncActive reports whether a synchronized-output batch is open.
78+ syncActive func() bool
79+ // onInvalidate observes every published invalidation (test-visible hook).
80+ onInvalidate func(renderInvalidation)
81+}
82+
83+// renderCoordinator fans in producer invalidations for one attached session.
84+type renderCoordinator struct {
85+ mu sync.Mutex
86+ opts renderCoordinatorOptions
87+
88+ // Coalesced pending state: latest wins, reset stays sticky, and at most
89+ // one deadline wake may be armed at a time (cap-1).
90+ pending bool
91+ pendingReset bool
92+ pendingUrgent bool
93+ coalesced int
94+
95+ // previewWake receives the same coalesced wakes while a preview
96+ // subscription is installed.
97+ previewWake func(renderWake)
98+
99+ // attachment is the currently bound client identity; callbacks from any
100+ // other identity are stale and must not mutate coordinator state.
101+ attachment *attachedClient
102+ torndown bool
103+
104+ resize resizeRequestMetadata
105+}
106+
107+// newRenderCoordinator constructs a coordinator bound to opts. It starts no
108+// goroutine; deadline scheduling is armed lazily by invalidate.
109+func newRenderCoordinator(opts renderCoordinatorOptions) *renderCoordinator {
110+ return &renderCoordinator{opts: opts}
111+}
112+
113+// invalidate publishes one producer state transition. Consecutive
114+// invalidations coalesce under a single armed deadline; the latest state and
115+// the stickiest reset win.
116+func (c *renderCoordinator) invalidate(renderInvalidation) {}
117+
118+// notifyAck reports that the client acknowledged an output state, releasing
119+// at most one deferred wake.
120+func (c *renderCoordinator) notifyAck() {}
121+
122+// noteSyncBegin records that a synchronized-output batch opened for gen and
123+// arms the completion watchdog.
124+func (c *renderCoordinator) noteSyncBegin(uint64) {}
125+
126+// noteSyncEnd records that the batch for gen completed, flushing pending
127+// state in one wake.
128+func (c *renderCoordinator) noteSyncEnd(uint64) {}
129+
130+// subscribePreview installs fn as the preview observer for coalesced wakes.
131+func (c *renderCoordinator) subscribePreview(func(renderWake)) {}
132+
133+// teardownPreview removes the preview subscription.
134+func (c *renderCoordinator) teardownPreview() {}
135+
136+// attach binds ac as the coordinator's current attachment identity.
137+func (c *renderCoordinator) attach(*attachedClient) {}
138+
139+// noteDetach invalidates pending wakes and stale callbacks for a detaching
140+// attachment.
141+func (c *renderCoordinator) noteDetach(*attachedClient) {}
142+
143+// noteReplace hands the coordinator from old to replacement; callbacks
144+// captured by old become stale.
145+func (c *renderCoordinator) noteReplace(old, replacement *attachedClient) {}
146+
147+// notePark invalidates pending wakes when the attachment parks for resume.
148+func (c *renderCoordinator) notePark(*attachedClient) {}
149+
150+// noteSessionTeardown terminally invalidates the coordinator.
151+func (c *renderCoordinator) noteSessionTeardown() {}
152+
153+// recordResizeRequest records the latest requested geometry and source before
154+// the request delegates to the retained PR #71 attachment path. It returns
155+// the strictly monotonically increased epoch, or 0 when the source is stale.
156+func (c *renderCoordinator) recordResizeRequest(domain.Size, *attachedClient) uint64 { return 0 }
157+
158+// resizeSnapshot returns a locked copy of the latest resize metadata.
159+func (c *renderCoordinator) resizeSnapshot() resizeRequestMetadata { return resizeRequestMetadata{} }
160+
161+// renderCoordinator returns the coordinator installed for this session.
162+func (s *session) renderCoordinator() *renderCoordinator { return s.coordinator.Load() }
163+
164+// installRenderCoordinator publishes rc as the session's coordinator.
165+func (s *session) installRenderCoordinator(rc *renderCoordinator) { s.coordinator.Store(rc) }
@@ -0,0 +1,165 @@
1+package daemon
2+
3+import (
4+ "sync"
5+ "time"
6+
7+ "github.com/bnema/vev/internal/domain"
8+ "github.com/bnema/vev/internal/ports"
9+)
10+
11+// S1 render coordinator contract. One coordinator serves one attached
12+// session: producers mutate authoritative state and publish an invalidation;
13+// only the coordinator decides when the transitional composition target runs.
14+// Producers never compose or send. This file currently carries the inert
15+// contract — state, options, and signatures exist so the behavioral
16+// specification compiles — while wake scheduling, ACK gating, synchronized
17+// output handling, and producer migration land in the GREEN slice.
18+
19+// Coordinator deadline bounds. Urgent transitions (input echo, overlay state
20+// changes) must become visible within urgentRenderDeadline; bulk output
21+// coalesces inside the adaptive [minOutputRenderDeadline,
22+// maxOutputRenderDeadline] window.
23+const (
24+ urgentRenderDeadline = 2 * time.Millisecond
25+ minOutputRenderDeadline = 8 * time.Millisecond
26+ maxOutputRenderDeadline = 16 * time.Millisecond
27+)
28+
29+// invalidationClass ranks a producer transition's latency demand.
30+type invalidationClass uint8
31+
32+const (
33+ // invalidateOutput is bulk PTY/screen churn served by the adaptive window.
34+ invalidateOutput invalidationClass = iota
35+ // invalidateUrgent is an interactive transition served within 2ms.
36+ invalidateUrgent
37+)
38+
39+// renderInvalidation is one producer-published state transition.
40+type renderInvalidation struct {
41+ class invalidationClass
42+ // reset requests a sticky FullRedraw: once requested it survives
43+ // coalescing and ACK deferral until a wake actually delivers it.
44+ reset bool
45+ // producer identifies the publishing call-site for provenance.
46+ producer string
47+}
48+
49+// renderWake is one coalesced composition request delivered to the
50+// transitional composition target (replaced by the S2 pipeline).
51+type renderWake struct {
52+ reset bool
53+ urgent bool
54+ coalesced int
55+ // watchdog marks a flush forced by the synchronized-output watchdog.
56+ watchdog bool
57+}
58+
59+// resizeRequestMetadata is the coordinator-owned latest requested resize
60+// state: S3's transaction entry point reads it through resizeSnapshot. It is
61+// metadata ownership only — S1/S2 dispatch stays with the retained PR #71
62+// attachment timer path.
63+type resizeRequestMetadata struct {
64+ size domain.Size
65+ source *attachedClient
66+ epoch uint64
67+}
68+
69+// renderCoordinatorOptions wires one coordinator instance.
70+type renderCoordinatorOptions struct {
71+ clock ports.Clock
72+ // wake is the transitional composition target.
73+ wake func(renderWake)
74+ // ackReady reports whether the attachment may compose another output
75+ // state (the outputStateStream window has capacity).
76+ ackReady func() bool
77+ // syncActive reports whether a synchronized-output batch is open.
78+ syncActive func() bool
79+ // onInvalidate observes every published invalidation (test-visible hook).
80+ onInvalidate func(renderInvalidation)
81+}
82+
83+// renderCoordinator fans in producer invalidations for one attached session.
84+type renderCoordinator struct {
85+ mu sync.Mutex
86+ opts renderCoordinatorOptions
87+
88+ // Coalesced pending state: latest wins, reset stays sticky, and at most
89+ // one deadline wake may be armed at a time (cap-1).
90+ pending bool
91+ pendingReset bool
92+ pendingUrgent bool
93+ coalesced int
94+
95+ // previewWake receives the same coalesced wakes while a preview
96+ // subscription is installed.
97+ previewWake func(renderWake)
98+
99+ // attachment is the currently bound client identity; callbacks from any
100+ // other identity are stale and must not mutate coordinator state.
101+ attachment *attachedClient
102+ torndown bool
103+
104+ resize resizeRequestMetadata
105+}
106+
107+// newRenderCoordinator constructs a coordinator bound to opts. It starts no
108+// goroutine; deadline scheduling is armed lazily by invalidate.
109+func newRenderCoordinator(opts renderCoordinatorOptions) *renderCoordinator {
110+ return &renderCoordinator{opts: opts}
111+}
112+
113+// invalidate publishes one producer state transition. Consecutive
114+// invalidations coalesce under a single armed deadline; the latest state and
115+// the stickiest reset win.
116+func (c *renderCoordinator) invalidate(renderInvalidation) {}
117+
118+// notifyAck reports that the client acknowledged an output state, releasing
119+// at most one deferred wake.
120+func (c *renderCoordinator) notifyAck() {}
121+
122+// noteSyncBegin records that a synchronized-output batch opened for gen and
123+// arms the completion watchdog.
124+func (c *renderCoordinator) noteSyncBegin(uint64) {}
125+
126+// noteSyncEnd records that the batch for gen completed, flushing pending
127+// state in one wake.
128+func (c *renderCoordinator) noteSyncEnd(uint64) {}
129+
130+// subscribePreview installs fn as the preview observer for coalesced wakes.
131+func (c *renderCoordinator) subscribePreview(func(renderWake)) {}
132+
133+// teardownPreview removes the preview subscription.
134+func (c *renderCoordinator) teardownPreview() {}
135+
136+// attach binds ac as the coordinator's current attachment identity.
137+func (c *renderCoordinator) attach(*attachedClient) {}
138+
139+// noteDetach invalidates pending wakes and stale callbacks for a detaching
140+// attachment.
141+func (c *renderCoordinator) noteDetach(*attachedClient) {}
142+
143+// noteReplace hands the coordinator from old to replacement; callbacks
144+// captured by old become stale.
145+func (c *renderCoordinator) noteReplace(old, replacement *attachedClient) {}
146+
147+// notePark invalidates pending wakes when the attachment parks for resume.
148+func (c *renderCoordinator) notePark(*attachedClient) {}
149+
150+// noteSessionTeardown terminally invalidates the coordinator.
151+func (c *renderCoordinator) noteSessionTeardown() {}
152+
153+// recordResizeRequest records the latest requested geometry and source before
154+// the request delegates to the retained PR #71 attachment path. It returns
155+// the strictly monotonically increased epoch, or 0 when the source is stale.
156+func (c *renderCoordinator) recordResizeRequest(domain.Size, *attachedClient) uint64 { return 0 }
157+
158+// resizeSnapshot returns a locked copy of the latest resize metadata.
159+func (c *renderCoordinator) resizeSnapshot() resizeRequestMetadata { return resizeRequestMetadata{} }
160+
161+// renderCoordinator returns the coordinator installed for this session.
162+func (s *session) renderCoordinator() *renderCoordinator { return s.coordinator.Load() }
163+
164+// installRenderCoordinator publishes rc as the session's coordinator.
165+func (s *session) installRenderCoordinator(rc *renderCoordinator) { s.coordinator.Store(rc) }
M internal/usecase/daemon/session.go +2 -0
@@ -67,6 +67,8 @@ type session struct {
6767 mruAt atomic.Uint64
6868 snapDirty atomic.Bool
6969 snapEligible atomic.Bool
70+ // coordinator fans in this session's producer render invalidations.
71+ coordinator atomic.Pointer[renderCoordinator]
7072 // clipFiles records clipboard-image-transfer temp file paths (see
7173 // clipboard.go) written for this session, removed best-effort in
7274 // killSession.
@@ -67,6 +67,8 @@ type session struct {
67 mruAt atomic.Uint6467 mruAt atomic.Uint64
68 snapDirty atomic.Bool68 snapDirty atomic.Bool
69 snapEligible atomic.Bool69 snapEligible atomic.Bool
70+ // coordinator fans in this session's producer render invalidations.
71+ coordinator atomic.Pointer[renderCoordinator]
70 // clipFiles records clipboard-image-transfer temp file paths (see72 // clipFiles records clipboard-image-transfer temp file paths (see
71 // clipboard.go) written for this session, removed best-effort in73 // clipboard.go) written for this session, removed best-effort in
72 // killSession.74 // killSession.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close