attachment_worker.go RawHistoryFolder

1package client
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"io"
8	"sync"
9	"sync/atomic"
10	"time"
11
12	"github.com/bnema/vev/internal/domain"
13	"github.com/bnema/vev/internal/ports"
14	"github.com/bnema/vev/internal/protocol"
15)
16
17// Attachment worker mechanisms.
18//
19// This file defines the attachment mechanisms owned by the autonomous
20// supervisor. It reuses the terminal input pump (terminalInputPump) and
21// foreground output gate (foregroundSendLease) rather than duplicating them.
22//
23// Ownership split:
24//
25//   - The supervisor owns the terminal: raw mode, the terminal writer, the
26//     single input pump, the UI publication transaction, resize events, and the
27//     process lifecycle. None of those move into a worker.
28//   - One AttachmentWorker owns exactly one broker logical stream and the typed
29//     session I/O on it. It cannot enter or restore raw mode, choose an
30//     endpoint, restore a source route, retain an attachment in the cache, or
31//     write the terminal directly.
32//   - Every terminal-facing action a worker performs crosses the
33//     supervisor-granted AttachmentForeground. The supervisor grants exactly
34//     one foreground at a time; on cancellation or supersession it finalizes
35//     that grant (closing Done and refusing output, UI, resize, and new input),
36//     lets the woken worker Ack/Preserve the read it holds through a bounded
37//     join, then revokes input authority before closing the owned stream.
38//   - Every worker result carries the AttachmentToken (generation/attempt) the
39//     supervisor granted, so a late result from a canceled or superseded
40//     generation is discarded rather than applied.
41
42// Picker and attachment consumers use the same terminal input pump; they must
43// never start rival readers.
44//
45// UI action admission is optional and reuses this host's single pump claim.
46// Binding occurs after grant claims the pump and before the worker starts;
47// release occurs when finalization starts, before the claim can return to the
48// picker. The UI-generated generation is authoritative and is stamped onto the
49// attachment token and every publication.
50
51// attachmentWorkerJoinTimeout bounds the supervisor's join of a cancelled
52// worker during the final input window. A worker that honors neither
53// cancellation nor a closed Done is abandoned rather than allowed to pin the
54// supervisor; the supervisor then revokes the retained consumer and closes the
55// stream anyway. It mirrors the attachment retirement join budget.
56const attachmentWorkerJoinTimeout = 5 * time.Second
57
58// errAttachmentForegroundRevoked reports a worker action refused because the
59// supervisor revoked its input/output authority.
60var errAttachmentForegroundRevoked = errors.New("client: attachment foreground authority revoked")
61
62// AttachmentToken identifies one attachment worker run. Generation is the
63// supervisor-assigned foreground generation. The host never allocates it while
64// UI action binding is deferred, so the supervisor supplies it and the host
65// stamps it onto the event and every publication; Attempt is the worker
66// identity, not a UI generation. A zero Generation is never granted.
67type AttachmentToken struct {
68	Generation uint64
69	Attempt    uint64
70}
71
72// IsZero reports whether the token was never assigned by a supervisor.
73func (t AttachmentToken) IsZero() bool { return t.Generation == 0 }
74
75// String renders the token for diagnostics.
76func (t AttachmentToken) String() string {
77	return fmt.Sprintf("gen=%d attempt=%d", t.Generation, t.Attempt)
78}
79
80// AttachmentEventKind classifies one worker's terminal outcome. Every kind
81// returns the client to the picker: a worker event never terminates the process
82// by itself. Only the supervisor's own state machine decides that.
83type AttachmentEventKind uint8
84
85const (
86	// AttachmentEventEnded is an orderly end: the logical stream reached EOF or
87	// the worker detached without failure.
88	AttachmentEventEnded AttachmentEventKind = iota + 1
89	// AttachmentEventLost is a transport or logical-stream loss. The logical
90	// stream's own Done/Err carries the stable cause.
91	AttachmentEventLost
92	// AttachmentEventFailed is a typed protocol or application failure on the
93	// stream, distinct from a carriage loss.
94	AttachmentEventFailed
95)
96
97// String renders the event kind for diagnostics.
98func (k AttachmentEventKind) String() string {
99	switch k {
100	case AttachmentEventEnded:
101		return "ended"
102	case AttachmentEventLost:
103		return "lost"
104	case AttachmentEventFailed:
105		return "failed"
106	default:
107		return "unknown"
108	}
109}
110
111// AttachmentEvent is the typed result of one worker run. Token is always the
112// token the supervisor granted: the host stamps it, so a worker cannot report
113// under another generation.
114type AttachmentEvent struct {
115	Token AttachmentToken
116	Kind  AttachmentEventKind
117	Err   error
118}
119
120// AttachmentInputEvent is one authorized terminal input delivery. Err carries a
121// terminal read failure (for example io.EOF) alongside any final bytes.
122type AttachmentInputEvent struct {
123	Data     []byte
124	Err      error
125	actionID uint64
126}
127
128// AttachmentForeground is the supervisor-granted handle through which a worker
129// touches the terminal. It is the worker's only path to input, resize, output,
130// UI publication, and the attached transition. Every method is refused once the
131// supervisor revokes the grant, so a canceled or superseded generation cannot
132// read input, resize, write output, publish UI, or mark itself attached.
133type AttachmentForeground interface {
134	// Token is the generation/attempt identity of this grant.
135	Token() AttachmentToken
136	// Stream is the supervisor-admitted logical stream this worker owns for its
137	// lifetime. The supervisor retains close authority.
138	Stream() ports.BrokerLogicalConnection
139	// Input blocks until the next authorized terminal input delivery. It returns
140	// false once authority is revoked or ctx ends.
141	Input(ctx context.Context) (AttachmentInputEvent, bool)
142	// AckInput commits the last Input delivery so the shared reader may accept
143	// the next one. It decides that delivery: at most one of AckInput and
144	// PreserveInput may decide a delivery, and a second decision for the same
145	// delivery is rejected as a no-op. It is a no-op once authority is revoked.
146	AckInput()
147	// PreserveInput re-queues the undecided bytes of the last Input delivery so
148	// the next authorized attempt can replay them exactly once. It decides that
149	// delivery: a duplicate decision (a later AckInput or PreserveInput for the
150	// same delivery, or a call with no outstanding delivery) is rejected as a
151	// no-op instead of re-queuing different bytes. It stays authorized while the
152	// supervisor finalizes this grant (after Done closes and before revocation),
153	// so a cancelled worker can still save input; it is a no-op once authority is
154	// revoked.
155	PreserveInput(data []byte)
156	// Resize blocks until the latest authorized terminal resize for this
157	// attachment. It returns false once authority is revoked or ctx ends.
158	Resize(ctx context.Context) (domain.Geometry, bool)
159	// Lifecycle blocks for one explicit typed lifecycle action.
160	Lifecycle(ctx context.Context) (AttachmentLifecycleActionKind, bool)
161	// Output publishes context and writes and flushes bytes in one authorized
162	// transaction. Missing UI or terminal returns ports.ErrUIUnavailable.
163	Output(uiContext ports.UIContext, data []byte) error
164	// PublishAttached publishes the committed attached presentation once the
165	// first frame was written, flushed, and committed. It is fenced exactly like
166	// Output and writes no terminal bytes: the screen it publishes is the frame
167	// that already drained, so no second writer and no extra flush is needed.
168	PublishAttached(uiContext ports.UIContext) error
169	// MarkAttached records the attached transition. It is refused once authority
170	// is revoked.
171	MarkAttached() bool
172	// Attached reports whether MarkAttached has succeeded.
173	Attached() bool
174	// Done is closed when the supervisor finalizes this grant. It stops new
175	// input, resize, output, and UI publication and wakes the worker, while
176	// AckInput and PreserveInput stay authorized for the read the worker already
177	// holds until the supervisor revokes the grant.
178	Done() <-chan struct{}
179}
180
181// AttachmentWorker is the contract for one broker logical stream's typed
182// session I/O. Run owns the stream returned by Stream (via the foreground) and
183// must honor ctx cancellation: the supervisor closes the stream after revoking
184// authority, which must unblock receive/send. Run must never enter or restore
185// raw mode, choose an endpoint, restore a source route, or touch the attachment
186// cache; none of those authorities are reachable from its arguments.
187type AttachmentWorker interface {
188	Run(ctx context.Context, fg AttachmentForeground) AttachmentEvent
189}
190
191// attachmentAuthorityState is the explicit lifecycle of the single foreground
192// slot. Finalization is a first-class state rather than a side effect of a
193// closed Done channel: while finalizing, only Ack/Preserve remain authorized,
194// every other action and a competing grant are refused.
195type attachmentAuthorityState uint8
196
197const (
198	// authorityIdle has no foreground owner.
199	authorityIdle attachmentAuthorityState = iota
200	// authorityActive grants the foreground every AttachmentForeground action.
201	authorityActive
202	// authorityFinalizing retains the same foreground only for Ack/Preserve
203	// while the supervisor joins a cancelled worker. New input, resize, output,
204	// UI publication, MarkAttached, and a competing grant are all refused.
205	authorityFinalizing
206)
207
208// attachmentAuthority owns the single supervisor foreground slot. It is safe for
209// concurrent use: the supervisor finalizes and revokes from its own goroutine
210// while a worker acts from its run goroutine.
211type attachmentAuthority struct {
212	mu      sync.Mutex
213	current *attachmentForeground
214	state   attachmentAuthorityState
215}
216
217// grant installs fg as the sole foreground in the active state. It fails while
218// another grant is live or while the shared input pump already has a consumer,
219// which is what keeps exactly one foreground owner.
220func (a *attachmentAuthority) grant(fg *attachmentForeground) bool {
221	if a == nil || fg == nil {
222		return false
223	}
224	a.mu.Lock()
225	defer a.mu.Unlock()
226	if a.current != nil {
227		return false
228	}
229	if fg.input != nil {
230		var ok bool
231		fg.consumer, ok = fg.input.tryClaim()
232		if !ok {
233			return false
234		}
235	}
236	a.current = fg
237	a.state = authorityActive
238	return true
239}
240
241// beginFinalize moves fg into the finalizing state: the worker keeps the slot
242// and its pump consumer for Ack/Preserve, but every other action is refused.
243// It reports false when fg no longer holds the slot.
244func (a *attachmentAuthority) beginFinalize(fg *attachmentForeground) bool {
245	if a == nil || fg == nil {
246		return false
247	}
248	a.mu.Lock()
249	defer a.mu.Unlock()
250	if a.current != fg {
251		return false
252	}
253	a.state = authorityFinalizing
254	return true
255}
256
257// revoke clears fg if it is still the foreground, releasing the pump consumer
258// and any future UI binding, and reports whether it was. It runs after the
259// bounded join. Input release and clearing the slot are atomic with respect to
260// the next grant.
261func (a *attachmentAuthority) revoke(fg *attachmentForeground) bool {
262	if a == nil || fg == nil {
263		return false
264	}
265	a.mu.Lock()
266	defer a.mu.Unlock()
267	if a.current != fg {
268		return false
269	}
270	if fg.input != nil && fg.consumer != 0 {
271		fg.input.revoke(fg.consumer)
272	}
273	a.current = nil
274	a.state = authorityIdle
275	return true
276}
277
278// foreground is the sole synchronized ownership accessor. It returns the
279// owner in both the active and finalizing states.
280func (a *attachmentAuthority) foreground() *attachmentForeground {
281	if a == nil {
282		return nil
283	}
284	a.mu.Lock()
285	defer a.mu.Unlock()
286	return a.current
287}
288
289// actionAuthorized reports whether fg may take a terminal-facing action now: it
290// must hold the slot and the slot must not be finalizing.
291func (a *attachmentAuthority) actionAuthorized(fg *attachmentForeground) bool {
292	if a == nil || fg == nil {
293		return false
294	}
295	a.mu.Lock()
296	defer a.mu.Unlock()
297	return a.current == fg && a.state == authorityActive
298}
299
300// act runs fn under the authority lock exactly when fg may still act, so an
301// action or state transition (the attached marker) is atomic with respect to
302// finalization and revocation.
303func (a *attachmentAuthority) act(fg *attachmentForeground, fn func()) bool {
304	if a == nil || fg == nil {
305		return false
306	}
307	a.mu.Lock()
308	defer a.mu.Unlock()
309	if a.current != fg || a.state != authorityActive {
310		return false
311	}
312	fn()
313	return true
314}
315
316// retain runs fn under the authority lock exactly when fg still owns the slot,
317// including while finalizing. It is the Ack/Preserve path: those only commit or
318// re-queue input the worker already held, so a cancelled generation may still
319// save it during the final input window. Every other action uses act or
320// actionAuthorized.
321func (a *attachmentAuthority) retain(fg *attachmentForeground, fn func()) bool {
322	if a == nil || fg == nil {
323		return false
324	}
325	a.mu.Lock()
326	defer a.mu.Unlock()
327	if a.current != fg {
328		return false
329	}
330	fn()
331	return true
332}
333
334// attachmentHostConfig supplies the supervisor-owned dependencies. Terminal,
335// clock, and input belong to the supervisor; UI seams and OnAttached are
336// optional.
337type attachmentHostConfig struct {
338	// Terminal is the controlling terminal the supervisor owns. Optional only
339	// so a mechanism test can omit output entirely; production supplies it.
340	Terminal ports.Terminal
341	// Clock supplies the bounded-join timer. Use ports.Clock, not the wall
342	// clock.
343	Clock ports.Clock
344	// Input is the supervisor-owned single terminal reader. The host claims one
345	// consumer per foreground and revokes it after finalization. Optional.
346	Input *terminalInputPump
347	// Actions carries explicit lifecycle decisions for the current attachment.
348	Actions <-chan AttachmentLifecycleAction
349	// UI is the supervisor-owned UI publication transaction. When nil it is
350	// taken from Terminal when that implements ports.UIOutputTransaction.
351	UI ports.UIOutputTransaction
352	// ActionUI optionally binds automation to the same claimed input consumer.
353	ActionUI *UI
354	// OnAttached observes a successful MarkAttached. Optional.
355	OnAttached func(AttachmentToken)
356}
357
358// attachmentHost is the supervisor-side owner of the terminal foreground. It
359// grants one worker generation at a time input/output authority over a single
360// admitted logical stream, joins that worker within a bound, and discards late
361// results. It owns no raw mode and restores no terminal: those stay with the
362// supervisor.
363type attachmentHost struct {
364	term      ports.Terminal
365	clock     ports.Clock
366	input     *terminalInputPump
367	ui        ports.UIOutputTransaction
368	actionUI  *UI
369	resizes   <-chan domain.Geometry
370	actions   <-chan AttachmentLifecycleAction
371	onAttach  func(AttachmentToken)
372	authority attachmentAuthority
373
374	geometryMu         sync.Mutex
375	geometry           domain.Geometry
376	geometryValid      bool
377	geometrySeq        uint64
378	geometryUpdate     chan struct{}
379	presentationUpdate chan struct{}
380	geometryCancel     context.CancelFunc
381	geometryDone       chan struct{}
382	ownerBoundary      bool
383
384	// navigationRequests carries a coalesced request from the attached worker
385	// to compose the client's session picker over the live attachment (the
386	// daemon's navigation PickerOffer). Only the supervisor consumes it.
387	navigationRequests chan struct{}
388	// internalActions carries supervisor-originated lifecycle decisions for the
389	// current foreground, such as the detach that precedes an attachment swap.
390	internalActions chan AttachmentLifecycleAction
391	// routeDemand wakes the supervisor to publish routes when the committed
392	// session changes; navigations carries the daemon's navigation requests
393	// Only the supervisor consumes these route and navigation channels.
394	routeDemand chan struct{}
395	navigations chan protocol.ServerMessage
396	// overlayActions carries driver actions the client picker overlay
397	// consumed to the supervisor, which settles them.
398	overlayActions chan overlayActionConsumed
399}
400
401// newAttachmentHost builds the reusable foreground host. A nil clock falls back
402// to the system clock; a nil terminal leaves output refused but still enforces
403// authority.
404func newAttachmentHost(cfg attachmentHostConfig) *attachmentHost {
405	clock := cfg.Clock
406	if supervisorNil(clock) {
407		clock = systemClock{}
408	}
409	ui := cfg.UI
410	resizes := (<-chan domain.Geometry)(nil)
411	var initialGeometry domain.Geometry
412	var initialGeometryValid bool
413	if !supervisorNil(cfg.Terminal) {
414		if supervisorNil(ui) {
415			ui, _ = cfg.Terminal.(ports.UIOutputTransaction)
416		}
417		resizes = cfg.Terminal.ResizeEvents()
418		if geometry, err := cfg.Terminal.Geometry(); err == nil && geometry.Valid() {
419			initialGeometry = geometry.NormalizePixels()
420			initialGeometryValid = true
421		}
422	}
423	return &attachmentHost{
424		term:               cfg.Terminal,
425		clock:              clock,
426		input:              cfg.Input,
427		ui:                 ui,
428		actionUI:           cfg.ActionUI,
429		resizes:            resizes,
430		actions:            cfg.Actions,
431		onAttach:           cfg.OnAttached,
432		geometry:           initialGeometry,
433		geometryValid:      initialGeometryValid,
434		geometryUpdate:     make(chan struct{}, 1),
435		presentationUpdate: make(chan struct{}, 1),
436		navigationRequests: make(chan struct{}, 1),
437		internalActions:    make(chan AttachmentLifecycleAction, 1),
438		routeDemand:        make(chan struct{}, 1),
439		navigations:        make(chan protocol.ServerMessage, 1),
440		overlayActions:     make(chan overlayActionConsumed, 1),
441	}
442}
443
444// Begin grants one generation's foreground and starts its worker on a
445// supervisor-joined goroutine. It never closes or otherwise mutates stream, so
446// on false the caller retains sole ownership of it. It returns false when:
447//
448//   - the host is nil;
449//   - the token is zero (no supervisor generation was assigned);
450//   - the worker or stream is nil, in any typed-nil shape;
451//   - another foreground is already live, or is finalizing a previous grant;
452//   - the shared input pump already has a consumer (a picker or rival
453//     foreground owns input).
454//
455// The granted token and event token always retain the caller-supplied
456// supervisor generation. When action UI is configured, its independent
457// generation is used only for UI publication and action fencing. The caller
458// must Wait or Cancel the returned run.
459func (h *attachmentHost) Begin(ctx context.Context, token AttachmentToken, worker AttachmentWorker, stream ports.BrokerLogicalConnection) (*attachmentRun, bool) {
460	if h == nil || token.IsZero() || supervisorNil(worker) || supervisorNil(stream) {
461		return nil, false
462	}
463	if ctx == nil {
464		ctx = context.Background()
465	}
466	workerCtx, cancel := context.WithCancel(ctx)
467	fg := h.newForeground(token, stream)
468	if !h.authority.grant(fg) {
469		cancel()
470		return nil, false
471	}
472	// A request or action left behind by a previous foreground never applies
473	// to this one.
474	h.drainForegroundSignals()
475	if h.actionUI != nil {
476		fg.uiGeneration = h.actionUI.bindForeground(workerCtx, fg.input, fg.consumer)
477	}
478	run := &attachmentRun{
479		host:    h,
480		token:   token,
481		stream:  stream,
482		fg:      fg,
483		cancel:  cancel,
484		outcome: make(chan AttachmentEvent, 1),
485		done:    make(chan struct{}),
486		retired: make(chan struct{}),
487	}
488	go func() {
489		defer close(run.done)
490		event := worker.Run(workerCtx, fg)
491		// The host stamps the granted token: a worker can never report under a
492		// generation it was not granted, and the settlement check is exact.
493		event.Token = token
494		run.outcome <- event
495	}()
496	return run, true
497}
498
499// Run is Begin followed immediately by Wait, for callers that join in place.
500// The second result is false when the run was superseded or cancelled, in which
501// case the worker's late event is discarded.
502func (h *attachmentHost) Run(ctx context.Context, token AttachmentToken, worker AttachmentWorker, stream ports.BrokerLogicalConnection) (AttachmentEvent, bool) {
503	run, ok := h.Begin(ctx, token, worker, stream)
504	if !ok {
505		return AttachmentEvent{}, false
506	}
507	return run.Wait(ctx)
508}
509
510// newForeground allocates a candidate; grant claims input before publishing it.
511func (h *attachmentHost) setInput(input *terminalInputPump) {
512	if h == nil {
513		return
514	}
515	h.input = input
516	h.ownerBoundary = true
517}
518
519func (h *attachmentHost) startGeometry(ctx context.Context) {
520	if h == nil {
521		return
522	}
523	if h.geometryDone != nil {
524		select {
525		case <-h.geometryDone:
526			h.geometryDone = nil
527			h.geometryCancel = nil
528		default:
529			return
530		}
531	}
532	geometryCtx, cancel := context.WithCancel(ctx)
533	h.geometryCancel = cancel
534	h.geometryDone = make(chan struct{})
535	go func() {
536		defer close(h.geometryDone)
537		for {
538			select {
539			case geometry, ok := <-h.resizes:
540				if !ok {
541					return
542				}
543				if !geometry.Valid() {
544					continue
545				}
546				h.geometryMu.Lock()
547				h.geometry = geometry.NormalizePixels()
548				h.geometryValid = true
549				h.geometrySeq++
550				h.geometryMu.Unlock()
551				select {
552				case h.geometryUpdate <- struct{}{}:
553				default:
554				}
555				// Presentation invalidation is deliberately independent from the
556				// attachment geometry sequence/wakeup. The supervisor consumes this
557				// coalesced signal and decides whether a picker presentation may be
558				// repainted; this collector never renders directly.
559				select {
560				case h.presentationUpdate <- struct{}{}:
561				default:
562				}
563			case <-geometryCtx.Done():
564				return
565			}
566		}
567	}()
568}
569
570func (h *attachmentHost) stopGeometry() {
571	if h == nil || h.geometryCancel == nil {
572		return
573	}
574	h.geometryCancel()
575	<-h.geometryDone
576	h.geometryCancel = nil
577	h.geometryDone = nil
578}
579
580func (h *attachmentHost) latestGeometry() (domain.Geometry, bool) {
581	if h == nil {
582		return domain.Geometry{}, false
583	}
584	h.geometryMu.Lock()
585	defer h.geometryMu.Unlock()
586	return h.geometry, h.geometryValid
587}
588
589func (h *attachmentHost) geometryAfter(sequence uint64) (domain.Geometry, uint64, bool) {
590	h.geometryMu.Lock()
591	defer h.geometryMu.Unlock()
592	return h.geometry, h.geometrySeq, h.geometryValid && h.geometrySeq > sequence
593}
594
595func (h *attachmentHost) newForeground(token AttachmentToken, stream ports.BrokerLogicalConnection) *attachmentForeground {
596	return &attachmentForeground{
597		authority:  &h.authority,
598		token:      token,
599		stream:     stream,
600		input:      h.input,
601		lease:      newForegroundSendLease(),
602		term:       h.term,
603		ui:         h.ui,
604		resizes:    h.resizes,
605		actions:    h.actions,
606		host:       h,
607		done:       make(chan struct{}),
608		decided:    true,
609		onAttached: h.onAttach,
610		repaint:    make(chan struct{}, 1),
611		tabSelect:  make(chan domain.TabStableID, 1),
612		routes:     make(chan protocol.RecentRouteSnapshot, 1),
613		replies:    make(chan protocol.ClientMessage, attachmentReplyCapacity),
614	}
615}
616
617// drainForegroundSignals drops any navigation request or internal lifecycle
618// action that belonged to an earlier foreground.
619func (h *attachmentHost) drainForegroundSignals() {
620	for {
621		select {
622		case <-h.navigationRequests:
623		case <-h.internalActions:
624		case <-h.routeDemand:
625		case <-h.navigations:
626		case <-h.overlayActions:
627		default:
628			return
629		}
630	}
631}
632
633// NavigationRequests wakes the supervisor when the attached worker asked for
634// the client picker over the live attachment.
635func (h *attachmentHost) NavigationRequests() <-chan struct{} {
636	if h == nil {
637		return nil
638	}
639	return h.navigationRequests
640}
641
642// beginNavigationOverlay composes the supervisor-owned picker over the current
643// foreground: from its return, attachment output is applied and acknowledged
644// but never written, and every authorized input delivery is handed to sink. It
645// reports false when no active foreground can host the overlay.
646func (h *attachmentHost) beginNavigationOverlay(sink pickerInputConsumer) bool {
647	if h == nil || sink == nil {
648		return false
649	}
650	fg := h.authority.foreground()
651	if fg == nil {
652		return false
653	}
654	return fg.setOverlay(attachmentOverlayNavigation, sink)
655}
656
657// endNavigationOverlay returns the terminal to the current foreground and asks
658// its worker for an authoritative repaint over the picker box.
659func (h *attachmentHost) endNavigationOverlay() {
660	if h == nil {
661		return
662	}
663	if fg := h.authority.foreground(); fg != nil {
664		fg.clearOverlay(attachmentOverlayNavigation)
665	}
666}
667
668// committedTarget reports the session the current foreground last committed
669// output for, so the supervisor can tell a commit to the attached session from
670// a commit elsewhere.
671func (h *attachmentHost) committedTarget() (protocol.ExactSessionTarget, bool) {
672	target, _, ok := h.committedView()
673	return target, ok
674}
675
676// committedView reports the session and the tab the current foreground last
677// committed output for.
678func (h *attachmentHost) committedView() (protocol.ExactSessionTarget, domain.TabStableID, bool) {
679	if h == nil {
680		return protocol.ExactSessionTarget{}, "", false
681	}
682	fg := h.authority.foreground()
683	if fg == nil {
684		return protocol.ExactSessionTarget{}, "", false
685	}
686	fg.overlayMu.Lock()
687	defer fg.overlayMu.Unlock()
688	return fg.committed, fg.committedTab, fg.committedKnown
689}
690
691// committedTargetOrZero is committedTarget with an unknown target as zero.
692func (h *attachmentHost) committedTargetOrZero() protocol.ExactSessionTarget {
693	target, ok := h.committedTarget()
694	if !ok {
695		return protocol.ExactSessionTarget{}
696	}
697	return target
698}
699
700// requestTabSelection asks the current foreground's worker to show one tab of
701// its attached session in place. The newest request replaces an unsent one.
702func (h *attachmentHost) requestTabSelection(token AttachmentToken, tab domain.TabStableID) bool {
703	if h == nil || tab == "" {
704		return false
705	}
706	fg := h.authority.foreground()
707	if fg == nil || fg.token != token {
708		return false
709	}
710	for {
711		select {
712		case fg.tabSelect <- tab:
713			return true
714		default:
715		}
716		select {
717		case <-fg.tabSelect:
718		default:
719		}
720	}
721}
722
723// requestDetach asks the current foreground's worker to detach explicitly, so
724// the daemon observes a clean Detach before the supervisor swaps attachments.
725func (h *attachmentHost) requestDetach(token AttachmentToken) {
726	if h == nil {
727		return
728	}
729	select {
730	case h.internalActions <- AttachmentLifecycleAction{Token: token, Kind: AttachmentDetachToPicker}:
731	default:
732	}
733}
734
735// attachmentOverlayKind names the client-composed overlay that owns the
736// terminal over a live attachment.
737type attachmentOverlayKind uint8
738
739const (
740	attachmentOverlayNone attachmentOverlayKind = iota
741	// attachmentOverlayNavigation is the supervisor-owned session picker.
742	attachmentOverlayNavigation
743	// attachmentOverlayMove is the worker-owned move-destination picker the
744	// serving daemon offered.
745	attachmentOverlayMove
746)
747
748// attachmentOverlayForeground is the optional overlay seam of the real
749// foreground. A worker reaches it by assertion so scripted foregrounds may
750// omit it; without it, overlays are simply never presented.
751type attachmentOverlayForeground interface {
752	requestNavigationPicker()
753	setOverlay(kind attachmentOverlayKind, sink pickerInputConsumer) bool
754	clearOverlay(kind attachmentOverlayKind)
755	overlayKind() attachmentOverlayKind
756	divertInput(data []byte, actionID uint64) bool
757	overlayRepaint() <-chan struct{}
758	overlayOutput(uiContext ports.UIContext, data []byte) error
759	noteCommitted(target protocol.ExactSessionTarget, tab domain.TabStableID)
760	tabSelections() <-chan domain.TabStableID
761	routeSnapshots() <-chan protocol.RecentRouteSnapshot
762	navigationReplies() <-chan protocol.ClientMessage
763	requestNavigation(message protocol.ServerMessage)
764}
765
766var _ attachmentOverlayForeground = (*attachmentForeground)(nil)
767
768// requestNavigationPicker records the daemon's navigation offer for the
769// supervisor. The request is coalesced; the supervisor decides whether the
770// current presentation can host the overlay.
771func (f *attachmentForeground) requestNavigationPicker() {
772	if f == nil || f.host == nil || !f.authority.actionAuthorized(f) {
773		return
774	}
775	select {
776	case f.host.navigationRequests <- struct{}{}:
777	default:
778	}
779}
780
781// setOverlay installs kind under the output lease, so no attachment frame is
782// mid-write when the overlay takes the terminal. An occupied slot refuses a
783// different overlay.
784func (f *attachmentForeground) setOverlay(kind attachmentOverlayKind, sink pickerInputConsumer) bool {
785	if f == nil || kind == attachmentOverlayNone {
786		return false
787	}
788	installed := false
789	f.lease.send(func() bool {
790		if !f.authority.actionAuthorized(f) {
791			return false
792		}
793		f.overlayMu.Lock()
794		defer f.overlayMu.Unlock()
795		if f.overlay != attachmentOverlayNone && f.overlay != kind {
796			return true
797		}
798		f.overlay = kind
799		f.overlaySink = sink
800		installed = true
801		return true
802	})
803	return installed
804}
805
806// clearOverlay releases kind under the output lease and asks the worker for an
807// authoritative repaint: the terminal still shows the picker box and any
808// attachment frames it suppressed were never written.
809func (f *attachmentForeground) clearOverlay(kind attachmentOverlayKind) {
810	if f == nil {
811		return
812	}
813	cleared := false
814	f.lease.send(func() bool {
815		f.overlayMu.Lock()
816		defer f.overlayMu.Unlock()
817		if f.overlay != kind {
818			return true
819		}
820		f.overlay = attachmentOverlayNone
821		f.overlaySink = nil
822		cleared = true
823		return true
824	})
825	if !cleared {
826		// The lease stops at finalization; the overlay dies with the grant.
827		f.overlayMu.Lock()
828		if f.overlay == kind {
829			f.overlay = attachmentOverlayNone
830			f.overlaySink = nil
831		}
832		f.overlayMu.Unlock()
833		return
834	}
835	select {
836	case f.repaint <- struct{}{}:
837	default:
838	}
839}
840
841func (f *attachmentForeground) overlayKind() attachmentOverlayKind {
842	if f == nil {
843		return attachmentOverlayNone
844	}
845	f.overlayMu.Lock()
846	defer f.overlayMu.Unlock()
847	return f.overlay
848}
849
850// divertInput hands one authorized delivery to the supervisor-owned overlay.
851// It reports false when no navigation overlay owns input, so the worker keeps
852// its session path. A driver action consumed here never reaches the daemon, so
853// it is marked before the picker sees the bytes and handed to the supervisor
854// afterwards, which settles it at its own event-loop boundary (or follows it
855// across the swap a commit starts).
856func (f *attachmentForeground) divertInput(data []byte, actionID uint64) bool {
857	if f == nil {
858		return false
859	}
860	f.overlayMu.Lock()
861	sink := f.overlaySink
862	kind := f.overlay
863	f.overlayMu.Unlock()
864	if kind != attachmentOverlayNavigation || sink == nil {
865		return false
866	}
867	tracked := actionID != 0 && f.uiGeneration != 0 && f.host != nil && f.host.actionUI != nil
868	if tracked {
869		f.host.actionUI.markOverlayInput(f.uiGeneration, actionID)
870	}
871	sink.ConsumeTerminalRead(data)
872	if tracked {
873		f.host.offerOverlayAction(overlayActionConsumed{generation: f.uiGeneration, actionID: actionID})
874	}
875	return true
876}
877
878// overlayActionConsumed names one driver action the client picker overlay
879// consumed on the given UI generation.
880type overlayActionConsumed struct {
881	generation uint64
882	actionID   uint64
883}
884
885// offerOverlayAction hands the latest overlay-consumed action to the
886// supervisor. Automation admits one action at a time and the worker is the only
887// producer, so replacing an unread stale entry never drops a live one.
888func (h *attachmentHost) offerOverlayAction(consumed overlayActionConsumed) {
889	for {
890		select {
891		case h.overlayActions <- consumed:
892			return
893		default:
894		}
895		select {
896		case <-h.overlayActions:
897		default:
898		}
899	}
900}
901
902// OverlayActions wakes the supervisor for each driver action the picker
903// overlay consumed.
904func (h *attachmentHost) OverlayActions() <-chan overlayActionConsumed {
905	if h == nil {
906		return nil
907	}
908	return h.overlayActions
909}
910
911func (f *attachmentForeground) overlayRepaint() <-chan struct{} {
912	if f == nil {
913		return nil
914	}
915	return f.repaint
916}
917
918// committedSnapshot reports the session and tab this foreground last
919// committed output for. It stays readable after the foreground is finished,
920// so a settled attachment can still name what it was showing.
921func (f *attachmentForeground) committedSnapshot() (protocol.ExactSessionTarget, domain.TabStableID, bool) {
922	if f == nil {
923		return protocol.ExactSessionTarget{}, "", false
924	}
925	f.overlayMu.Lock()
926	defer f.overlayMu.Unlock()
927	return f.committed, f.committedTab, f.committedKnown
928}
929
930func (f *attachmentForeground) noteCommitted(target protocol.ExactSessionTarget, tab domain.TabStableID) {
931	if f == nil {
932		return
933	}
934	f.overlayMu.Lock()
935	changed := !f.committedKnown || f.committed != target
936	f.committed = target
937	f.committedTab = tab
938	f.committedKnown = true
939	f.overlayMu.Unlock()
940	if changed {
941		f.demandRoutes()
942	}
943}
944
945// tabSelections carries the supervisor's in-place tab switches to the worker.
946func (f *attachmentForeground) tabSelections() <-chan domain.TabStableID {
947	if f == nil {
948		return nil
949	}
950	return f.tabSelect
951}
952
953// overlayOutput writes one overlay frame through the same lease and UI
954// transaction as attachment output, bypassing only the overlay suppression.
955func (f *attachmentForeground) overlayOutput(uiContext ports.UIContext, data []byte) error {
956	return f.write(uiContext, data, true)
957}
958
959// attachmentQueryForeground is the optional terminal-query seam of the real
960// foreground. Scripted foregrounds may omit it; the worker then skips palette
961// detection but still strips terminal replies from input.
962type attachmentQueryForeground interface {
963	writeTerminalQuery(data []byte) error
964}
965
966var _ attachmentQueryForeground = (*attachmentForeground)(nil)
967
968// writeTerminalQuery writes and flushes terminal query bytes (palette probes)
969// under the output lease. They draw nothing, so neither an overlay nor the UI
970// observation channel is involved.
971func (f *attachmentForeground) writeTerminalQuery(data []byte) error {
972	if f == nil {
973		return errAttachmentForegroundRevoked
974	}
975	var err error
976	ok := f.lease.send(func() bool {
977		if !f.authority.actionAuthorized(f) {
978			return false
979		}
980		if supervisorNil(f.term) || supervisorNil(f.term.Out()) {
981			err = ports.ErrUIUnavailable
982			return true
983		}
984		var n int
985		n, err = f.term.Out().Write(data)
986		if err == nil && n != len(data) {
987			err = io.ErrShortWrite
988		}
989		if err == nil {
990			err = f.term.Flush()
991		}
992		return true
993	})
994	if !ok {
995		return errAttachmentForegroundRevoked
996	}
997	return err
998}
999
1000// attachmentPredictionForeground is the optional predictive-echo seam of the
1001// real foreground. Scripted foregrounds may omit it; the worker then never
1002// predicts.
1003type attachmentPredictionForeground interface {
1004	writePrediction(data []byte) (bool, error)
1005}
1006
1007var _ attachmentPredictionForeground = (*attachmentForeground)(nil)
1008
1009// writePrediction writes and flushes predicted echo under the output lease.
1010// It reports false without writing while an overlay owns the terminal. The
1011// guesses are client-local drawing, so the UI observation channel is not
1012// involved.
1013func (f *attachmentForeground) writePrediction(data []byte) (bool, error) {
1014	if f == nil {
1015		return false, errAttachmentForegroundRevoked
1016	}
1017	var err error
1018	written := false
1019	ok := f.lease.send(func() bool {
1020		if !f.authority.actionAuthorized(f) {
1021			return false
1022		}
1023		if f.overlayKind() != attachmentOverlayNone {
1024			return true
1025		}
1026		if supervisorNil(f.term) || supervisorNil(f.term.Out()) {
1027			err = ports.ErrUIUnavailable
1028			return true
1029		}
1030		var n int
1031		n, err = f.term.Out().Write(data)
1032		if err == nil && n != len(data) {
1033			err = io.ErrShortWrite
1034		}
1035		if err == nil {
1036			err = f.term.Flush()
1037		}
1038		written = err == nil
1039		return true
1040	})
1041	if !ok {
1042		return false, errAttachmentForegroundRevoked
1043	}
1044	return written, err
1045}
1046
1047// attachmentRun is one in-flight worker generation owned by the supervisor.
1048type attachmentRun struct {
1049	host   *attachmentHost
1050	token  AttachmentToken
1051	stream ports.BrokerLogicalConnection
1052	fg     *attachmentForeground
1053	cancel context.CancelFunc
1054	// outcome is capacity one so a worker that settles after the supervisor has
1055	// already moved on can never block on its own return.
1056	outcome chan AttachmentEvent
1057	// done closes when the worker goroutine has returned.
1058	done      chan struct{}
1059	joinOnce  sync.Once
1060	stateMu   sync.Mutex
1061	retired   chan struct{}
1062	isRetired bool
1063}
1064
1065// Wait joins the run. It returns the worker's typed event with adopted=true
1066// only when the worker settled on its own and the supervisor had not cancelled;
1067// a run that was cancelled or superseded is joined and its late event discarded
1068// (adopted=false). The caller's ctx stops waiting for an outcome; retirement
1069// then uses the separate bounded worker-join budget.
1070func (r *attachmentRun) Wait(ctx context.Context) (AttachmentEvent, bool) {
1071	if ctx == nil {
1072		ctx = context.Background()
1073	}
1074	select {
1075	case event := <-r.outcome:
1076		// Settlement and external retirement compete at one state boundary.
1077		adopted := r.retire() && ctx.Err() == nil && event.Token == r.token
1078		r.cleanup()
1079		if adopted {
1080			return event, true
1081		}
1082	case <-r.retired:
1083		r.cleanup()
1084	case <-ctx.Done():
1085		r.Cancel()
1086	}
1087	return AttachmentEvent{}, false
1088}
1089
1090// retire is authoritative even when outcome and cancellation are both ready.
1091func (r *attachmentRun) retire() bool {
1092	r.stateMu.Lock()
1093	defer r.stateMu.Unlock()
1094	if r.isRetired {
1095		return false
1096	}
1097	r.isRetired = true
1098	close(r.retired)
1099	return true
1100}
1101
1102// Cancel marks retirement, runs the deterministic final input window, and
1103// returns once the worker is joined or the join bound expires and the retained
1104// consumer and stream are released.
1105func (r *attachmentRun) Cancel() {
1106	r.retire()
1107	r.cleanup()
1108}
1109
1110// cleanup is the ordered teardown. It refuses further output/UI/resize/new input
1111// and closes Done, wakes the worker, then joins within the bound while the pump
1112// consumer stays authorized for Ack/Preserve, so a worker woken by Done can
1113// still save the read it holds. Only after the join (or its timeout) does it
1114// revoke the consumer and foreground slot, then close the owned stream.
1115func (r *attachmentRun) cleanup() {
1116	r.joinOnce.Do(func() {
1117		r.fg.finalize()
1118		r.cancel()
1119		r.join()
1120		r.fg.finish()
1121		_ = r.stream.Close()
1122	})
1123}
1124
1125// join waits for the worker goroutine within the shared join bound. It allocates
1126// a timer only when the worker has not already returned.
1127func (r *attachmentRun) join() {
1128	select {
1129	case <-r.done:
1130		return
1131	default:
1132	}
1133	timer := r.host.clock.NewTimer(attachmentWorkerJoinTimeout)
1134	if supervisorNil(timer) {
1135		timer = systemClock{}.NewTimer(attachmentWorkerJoinTimeout)
1136	}
1137	defer stopSupervisorTimer(timer)
1138	select {
1139	case <-r.done:
1140	case <-timer.C():
1141	}
1142}
1143
1144// attachmentForeground is the supervisor-granted implementation of
1145// AttachmentForeground. It reuses the existing terminal input pump for input
1146// and the existing foreground send lease for output serialization.
1147type attachmentForeground struct {
1148	authority    *attachmentAuthority
1149	token        AttachmentToken
1150	stream       ports.BrokerLogicalConnection
1151	input        *terminalInputPump
1152	consumer     uint64
1153	lease        *foregroundSendLease
1154	term         ports.Terminal
1155	ui           ports.UIOutputTransaction
1156	uiGeneration uint64
1157	resizes      <-chan domain.Geometry
1158	actions      <-chan AttachmentLifecycleAction
1159	host         *attachmentHost
1160	done         chan struct{}
1161	// finalizedOnce and finishedOnce split the single teardown into the
1162	// deterministic final input window: finalize closes Done and stops output
1163	// first, finish revokes the retained consumer only after the join.
1164	finalizedOnce sync.Once
1165	finishedOnce  sync.Once
1166	// deliveryMu guards the at-most-once input decision. Input opens the window
1167	// by marking a freshly received delivery undecided; AckInput or PreserveInput
1168	// decides it at most once. decided starts true, so a decision with no
1169	// outstanding delivery is refused instead of silently committing nothing.
1170	deliveryMu  sync.Mutex
1171	decided     bool
1172	geometrySeq uint64
1173	attached    atomic.Bool
1174	onAttached  func(AttachmentToken)
1175
1176	// overlayMu guards the overlay slot and the committed target. The slot is
1177	// changed only inside the output lease, so an overlay never takes the
1178	// terminal while an attachment frame is mid-write.
1179	overlayMu      sync.Mutex
1180	overlay        attachmentOverlayKind
1181	overlaySink    pickerInputConsumer
1182	committed      protocol.ExactSessionTarget
1183	committedTab   domain.TabStableID
1184	committedKnown bool
1185	// repaint wakes the worker to request an authoritative repaint once an
1186	// overlay released the terminal.
1187	repaint chan struct{}
1188	// tabSelect carries one pending in-place tab switch to the worker.
1189	tabSelect chan domain.TabStableID
1190	// routes carries the newest client route snapshot to the worker; replies
1191	// carries the supervisor's navigation failures.
1192	routes  chan protocol.RecentRouteSnapshot
1193	replies chan protocol.ClientMessage
1194}
1195
1196// Token is the generation/attempt identity of this grant.
1197func (f *attachmentForeground) Token() AttachmentToken { return f.token }
1198
1199func (f *attachmentForeground) actionableGeneration() uint64 {
1200	if f.uiGeneration != 0 {
1201		return f.uiGeneration
1202	}
1203	return f.token.Generation
1204}
1205
1206func (f *attachmentForeground) uiPublished() {
1207	if f.uiGeneration == 0 || f.host == nil || f.host.actionUI == nil {
1208		return
1209	}
1210	f.host.actionUI.published(f.uiGeneration)
1211}
1212
1213func (f *attachmentForeground) uiReceipt(receipt protocol.UIReceipt) {
1214	if f.uiGeneration != 0 && f.host != nil && f.host.actionUI != nil {
1215		f.host.actionUI.receipt(f.uiGeneration, receipt)
1216	}
1217}
1218
1219// uiFollowSamePeer keeps the driver action that caused a same-peer offer
1220// pending until the in-place destination commits.
1221func (f *attachmentForeground) uiFollowSamePeer(actionID uint64) {
1222	if actionID != 0 && f.uiGeneration != 0 && f.host != nil && f.host.actionUI != nil {
1223		f.host.actionUI.follow(f.uiGeneration, actionID)
1224	}
1225}
1226
1227// uiSamePeerArrived runs before the destination's first output is published.
1228func (f *attachmentForeground) uiSamePeerArrived() {
1229	if f.uiGeneration != 0 && f.host != nil && f.host.actionUI != nil {
1230		f.host.actionUI.arriveInPlace(f.uiGeneration)
1231	}
1232}
1233
1234// uiSamePeerFailed fails the followed action of a refused same-peer switch.
1235func (f *attachmentForeground) uiSamePeerFailed() {
1236	if f.uiGeneration != 0 && f.host != nil && f.host.actionUI != nil {
1237		f.host.actionUI.failInPlace(f.uiGeneration)
1238	}
1239}
1240
1241// Stream is the supervisor-admitted logical stream this worker owns.
1242func (f *attachmentForeground) Stream() ports.BrokerLogicalConnection { return f.stream }
1243
1244// Done is closed when the supervisor starts finalizing this grant. It stops
1245// Input and Resize and wakes a worker so it can still Ack/Preserve the read it
1246// holds; the output gate is stopped at the same time.
1247func (f *attachmentForeground) Done() <-chan struct{} { return f.done }
1248
1249// finalize opens the final input window exactly once: it moves the grant into
1250// the finalizing authority state, stops the output gate (waiting for any
1251// in-flight output transaction to end), and closes Done so Input/Resize refuse
1252// new work and the worker wakes. The pump consumer and foreground slot stay
1253// owned, so the worker may still Ack/Preserve the read it holds.
1254// Lock order is authority -> send lease; beginFinalize releases authority before
1255// waiting on the lease, so output never observes authority while it holds the
1256// lease.
1257func (f *attachmentForeground) finalize() {
1258	if f == nil {
1259		return
1260	}
1261	f.finalizedOnce.Do(func() {
1262		f.authority.beginFinalize(f)
1263		if f.lease != nil {
1264			f.lease.stop()
1265		}
1266		if f.uiGeneration != 0 && f.host != nil && f.host.actionUI != nil {
1267			f.host.actionUI.releaseForeground(f.uiGeneration)
1268		}
1269		close(f.done)
1270	})
1271}
1272
1273// finish revokes the retained consumer and clears the foreground slot after the
1274// bounded join. It runs before the stream is closed, so undecided input either
1275// reached the pump during finalization or is dropped with the revocation.
1276func (f *attachmentForeground) finish() {
1277	if f == nil {
1278		return
1279	}
1280	f.finishedOnce.Do(func() {
1281		// Finish is lossless: input kept for a resume must survive it. The
1282		// supervisor drops unclaimed bytes (dropUnclaimed) when the terminal
1283		// returns to the picker, the owner-class boundary no attachment byte
1284		// may cross.
1285		f.authority.revoke(f)
1286	})
1287}
1288
1289// beginDelivery opens the decision window for the delivery the worker just
1290// received: the next AckInput or PreserveInput decides it, and a further
1291// decision is refused until Input receives another delivery.
1292func (f *attachmentForeground) beginDelivery() {
1293	f.deliveryMu.Lock()
1294	f.decided = false
1295	f.deliveryMu.Unlock()
1296}
1297
1298// decideDelivery reports whether this is the first decision for the current
1299// delivery and marks it decided. A second Ack/Preserve for the same delivery,
1300// or a decision with no outstanding delivery, is refused.
1301func (f *attachmentForeground) decideDelivery() bool {
1302	f.deliveryMu.Lock()
1303	defer f.deliveryMu.Unlock()
1304	if f.decided {
1305		return false
1306	}
1307	f.decided = true
1308	return true
1309}
1310
1311// Input leases the next authorized terminal read. It waits on the pump's ready
1312// signal exactly like the attach scanner, so a shared single reader is never
1313// duplicated.
1314func (f *attachmentForeground) Input(ctx context.Context) (AttachmentInputEvent, bool) {
1315	if ctx == nil {
1316		ctx = context.Background()
1317	}
1318	for {
1319		if !f.authority.actionAuthorized(f) || f.input == nil {
1320			return AttachmentInputEvent{}, false
1321		}
1322		result, ok := f.input.take(ctx, f.consumer)
1323		if ok {
1324			f.beginDelivery()
1325			return AttachmentInputEvent{Data: result.data, Err: result.err, actionID: result.actionID}, true
1326		}
1327		if ctx.Err() != nil {
1328			return AttachmentInputEvent{}, false
1329		}
1330		select {
1331		case <-ctx.Done():
1332			return AttachmentInputEvent{}, false
1333		case <-f.done:
1334			return AttachmentInputEvent{}, false
1335		case request := <-f.input.automation:
1336			f.input.mu.Lock()
1337			clean := f.input.consumer == f.consumer && request.consumer == f.consumer && f.input.pending == nil && len(f.input.residual) == 0 && f.input.delivering == 0
1338			f.input.mu.Unlock()
1339			clean = clean && request.ctx.Err() == nil && request.record.source == terminalInputAutomation && request.record.actionID != 0 && request.record.generation == f.uiGeneration && request.record.endBatch && len(request.record.data) > 0 && len(request.record.data) <= uiMaxInputBytes && validUIKeyBatch(request.record.data)
1340			if clean && request.owner != nil {
1341				clean = request.owner.accept(request.record.actionID, request.record.generation)
1342			}
1343			request.admitted <- clean
1344			if !clean {
1345				continue
1346			}
1347			request.dispatched <- true
1348			return AttachmentInputEvent{Data: append([]byte(nil), request.record.data...), actionID: request.record.actionID}, true
1349		case <-f.input.readyFor(f.consumer):
1350		}
1351	}
1352}
1353
1354// AckInput commits the last delivery so the shared reader may publish the next.
1355// It decides that delivery, so a duplicate Ack/Preserve for it is a no-op.
1356func (f *attachmentForeground) AckInput() {
1357	if f == nil || f.input == nil {
1358		return
1359	}
1360	if !f.decideDelivery() {
1361		return
1362	}
1363	f.authority.retain(f, func() { f.input.ack(f.consumer) })
1364}
1365
1366// PreserveInput re-queues the undecided bytes of the last delivery for the next
1367// authorized attempt. It decides that delivery, so a duplicate Ack/Preserve for
1368// it is a no-op and can never re-queue different bytes. It stays authorized
1369// while the supervisor finalizes the grant.
1370func (f *attachmentForeground) PreserveInput(data []byte) {
1371	if f == nil || f.input == nil {
1372		return
1373	}
1374	if !f.decideDelivery() {
1375		return
1376	}
1377	f.authority.retain(f, func() {
1378		f.input.ack(f.consumer)
1379		f.input.preserveResidual(f.consumer, data)
1380	})
1381}
1382
1383// Resize waits for the next authorized terminal resize.
1384func (f *attachmentForeground) Lifecycle(ctx context.Context) (AttachmentLifecycleActionKind, bool) {
1385	if ctx == nil {
1386		ctx = context.Background()
1387	}
1388	for {
1389		if !f.authority.actionAuthorized(f) {
1390			return 0, false
1391		}
1392		var internal <-chan AttachmentLifecycleAction
1393		if f.host != nil {
1394			internal = f.host.internalActions
1395		}
1396		select {
1397		case action, ok := <-f.actions:
1398			if !ok || !f.authority.actionAuthorized(f) {
1399				return 0, false
1400			}
1401			if action.Token != f.token {
1402				continue
1403			}
1404			return action.Kind, action.Kind == AttachmentDetachToPicker || action.Kind == AttachmentDetachAndExit
1405		case action := <-internal:
1406			if action.Token != f.token || !f.authority.actionAuthorized(f) {
1407				continue
1408			}
1409			return action.Kind, action.Kind == AttachmentDetachToPicker || action.Kind == AttachmentDetachAndExit
1410		case <-ctx.Done():
1411			return 0, false
1412		case <-f.done:
1413			return 0, false
1414		}
1415	}
1416}
1417
1418func (f *attachmentForeground) Resize(ctx context.Context) (domain.Geometry, bool) {
1419	if ctx == nil {
1420		ctx = context.Background()
1421	}
1422	if !f.authority.actionAuthorized(f) || f.host == nil {
1423		return domain.Geometry{}, false
1424	}
1425	if !f.host.ownerBoundary {
1426		resizes := f.resizes
1427		for {
1428			select {
1429			case geometry, ok := <-resizes:
1430				if !ok {
1431					resizes = nil
1432					continue
1433				}
1434				if !f.authority.actionAuthorized(f) {
1435					return domain.Geometry{}, false
1436				}
1437				return geometry, true
1438			case <-ctx.Done():
1439				return domain.Geometry{}, false
1440			case <-f.done:
1441				return domain.Geometry{}, false
1442			}
1443		}
1444	}
1445	for {
1446		if geometry, next, ok := f.host.geometryAfter(f.geometrySeq); ok {
1447			f.geometrySeq = next
1448			if f.authority.actionAuthorized(f) {
1449				return geometry, true
1450			}
1451			return domain.Geometry{}, false
1452		}
1453		select {
1454		case <-ctx.Done():
1455			return domain.Geometry{}, false
1456		case <-f.done:
1457			return domain.Geometry{}, false
1458		case <-f.host.geometryUpdate:
1459		}
1460	}
1461}
1462
1463// Output holds the same send lease across the entire UI observation boundary.
1464// UIOutputTransaction is an observation channel, not attachment commit
1465// authority: an unavailable or closed observation channel does not stop the
1466// terminal frame. Other publication failures abort. Successful terminal write,
1467// flush, and EndOutput(true) commit the frame; any failed or short write or
1468// failed flush leaves EndOutput(false).
1469func (f *attachmentForeground) Output(uiContext ports.UIContext, data []byte) error {
1470	return f.write(uiContext, data, false)
1471}
1472
1473// write is the shared output transaction. While an overlay owns the terminal an
1474// attachment frame (overlay=false) is accepted without being written or
1475// published: the caller still applies and acknowledges it, and the overlay's
1476// release requests an authoritative repaint. Overlay frames bypass only that
1477// suppression.
1478func (f *attachmentForeground) write(uiContext ports.UIContext, data []byte, overlay bool) error {
1479	if f == nil {
1480		return errAttachmentForegroundRevoked
1481	}
1482	var outputErr error
1483	ok := f.lease.send(func() bool {
1484		if !f.authority.actionAuthorized(f) {
1485			return false
1486		}
1487		if !overlay && f.overlayKind() != attachmentOverlayNone {
1488			// Applied, not written: remember its committed boundary so a
1489			// daemon receipt fenced on it still settles its action.
1490			if f.uiGeneration != 0 && f.host != nil && f.host.actionUI != nil {
1491				f.host.actionUI.suppressedOutput(f.uiGeneration, f.presentationContext(uiContext))
1492			}
1493			return true
1494		}
1495		if supervisorNil(f.term) {
1496			outputErr = ports.ErrUIUnavailable
1497			return true
1498		}
1499		writer := f.term.Out()
1500		if supervisorNil(writer) {
1501			outputErr = ports.ErrUIUnavailable
1502			return true
1503		}
1504		uiContext = f.presentationContext(uiContext)
1505		hasUI := !supervisorNil(f.ui)
1506		if hasUI {
1507			f.ui.BeginOutput(uiContext)
1508		}
1509		success := false
1510		defer func() {
1511			if hasUI {
1512				f.ui.EndOutput(success)
1513				if success {
1514					f.uiPublished()
1515				}
1516			}
1517		}()
1518		// An unavailable capture means the optional UI observation channel is
1519		// disabled or closed, so the terminal frame still writes and flushes.
1520		// Any other publication error aborts before the frame is written.
1521		if hasUI {
1522			if err := f.ui.PublishContext(uiContext); err != nil && !errors.Is(err, ports.ErrUIUnavailable) {
1523				outputErr = err
1524				return true
1525			}
1526		}
1527		var n int
1528		n, outputErr = writer.Write(data)
1529		if outputErr == nil && n != len(data) {
1530			outputErr = io.ErrShortWrite
1531		}
1532		if outputErr != nil {
1533			return true
1534		}
1535		outputErr = f.term.Flush()
1536		success = outputErr == nil
1537		return true
1538	})
1539	if !ok {
1540		return errAttachmentForegroundRevoked
1541	}
1542	return outputErr
1543}
1544
1545// presentationContext applies the published-context shape rule to one foreground
1546// transaction. Only an Attached transaction carries the real actionable
1547// generation and the committed session metadata; any other presentation carries
1548// the run's stable UI handle and its status with every other field zeroed. A
1549// pre-attach Connecting frame therefore never publishes an actionable
1550// generation or session identity, and no caller can fence an action against a
1551// generation the attachment has not committed.
1552func (f *attachmentForeground) presentationContext(identity ports.UIContext) ports.UIContext {
1553	handle := identity.AttachmentHandle
1554	if f.host != nil && f.host.actionUI != nil {
1555		handle = f.host.actionUI.Handle()
1556	}
1557	if identity.Status != ports.UIStatusAttached {
1558		return ports.UIContext{AttachmentHandle: handle, Status: identity.Status}
1559	}
1560	identity.Generation = f.actionableGeneration()
1561	identity.AttachmentHandle = handle
1562	return identity
1563}
1564
1565// PublishAttached publishes the committed attached presentation. It takes the
1566// same send lease and the same authorization fence as Output, so it can never
1567// race a finalization or a revoked generation, and it writes no bytes: the
1568// attached presentation describes the frame that already drained. A closed UI
1569// observation channel is tolerated exactly as Output tolerates it, while any
1570// other publication failure is reported and leaves the run unattached.
1571func (f *attachmentForeground) PublishAttached(uiContext ports.UIContext) error {
1572	if f == nil {
1573		return errAttachmentForegroundRevoked
1574	}
1575	var outputErr error
1576	ok := f.lease.send(func() bool {
1577		if !f.authority.actionAuthorized(f) {
1578			return false
1579		}
1580		if supervisorNil(f.ui) {
1581			return true
1582		}
1583		uiContext = f.presentationContext(uiContext)
1584		f.ui.BeginOutput(uiContext)
1585		publishErr := f.ui.PublishContext(uiContext)
1586		if publishErr != nil && !errors.Is(publishErr, ports.ErrUIUnavailable) {
1587			outputErr = publishErr
1588			f.ui.EndOutput(false)
1589			return true
1590		}
1591		f.ui.EndOutput(true)
1592		f.uiPublished()
1593		return true
1594	})
1595	if !ok {
1596		return errAttachmentForegroundRevoked
1597	}
1598	return outputErr
1599}
1600
1601// MarkAttached records the attached transition atomically with respect to
1602// finalization and revocation.
1603func (f *attachmentForeground) MarkAttached() bool {
1604	if f == nil {
1605		return false
1606	}
1607	set := f.authority.act(f, func() { f.attached.Store(true) })
1608	if !set {
1609		return false
1610	}
1611	if f.onAttached != nil {
1612		f.onAttached(f.token)
1613	}
1614	return true
1615}
1616
1617// Attached reports whether MarkAttached has succeeded.
1618func (f *attachmentForeground) Attached() bool {
1619	return f != nil && f.attached.Load()
1620}

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