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	// inPlaceResults carries the outcome of each in-place switch the
400	// supervisor requested on the current foreground.
401	inPlaceResults chan inPlaceResult
402}
403
404// newAttachmentHost builds the reusable foreground host. A nil clock falls back
405// to the system clock; a nil terminal leaves output refused but still enforces
406// authority.
407func newAttachmentHost(cfg attachmentHostConfig) *attachmentHost {
408	clock := cfg.Clock
409	if supervisorNil(clock) {
410		clock = systemClock{}
411	}
412	ui := cfg.UI
413	resizes := (<-chan domain.Geometry)(nil)
414	var initialGeometry domain.Geometry
415	var initialGeometryValid bool
416	if !supervisorNil(cfg.Terminal) {
417		if supervisorNil(ui) {
418			ui, _ = cfg.Terminal.(ports.UIOutputTransaction)
419		}
420		resizes = cfg.Terminal.ResizeEvents()
421		if geometry, err := cfg.Terminal.Geometry(); err == nil && geometry.Valid() {
422			initialGeometry = geometry.NormalizePixels()
423			initialGeometryValid = true
424		}
425	}
426	return &attachmentHost{
427		term:               cfg.Terminal,
428		clock:              clock,
429		input:              cfg.Input,
430		ui:                 ui,
431		actionUI:           cfg.ActionUI,
432		resizes:            resizes,
433		actions:            cfg.Actions,
434		onAttach:           cfg.OnAttached,
435		geometry:           initialGeometry,
436		geometryValid:      initialGeometryValid,
437		geometryUpdate:     make(chan struct{}, 1),
438		presentationUpdate: make(chan struct{}, 1),
439		navigationRequests: make(chan struct{}, 1),
440		internalActions:    make(chan AttachmentLifecycleAction, 1),
441		routeDemand:        make(chan struct{}, 1),
442		navigations:        make(chan protocol.ServerMessage, 1),
443		overlayActions:     make(chan overlayActionConsumed, 1),
444		inPlaceResults:     make(chan inPlaceResult, 1),
445	}
446}
447
448// Begin grants one generation's foreground and starts its worker on a
449// supervisor-joined goroutine. It never closes or otherwise mutates stream, so
450// on false the caller retains sole ownership of it. It returns false when:
451//
452//   - the host is nil;
453//   - the token is zero (no supervisor generation was assigned);
454//   - the worker or stream is nil, in any typed-nil shape;
455//   - another foreground is already live, or is finalizing a previous grant;
456//   - the shared input pump already has a consumer (a picker or rival
457//     foreground owns input).
458//
459// The granted token and event token always retain the caller-supplied
460// supervisor generation. When action UI is configured, its independent
461// generation is used only for UI publication and action fencing. The caller
462// must Wait or Cancel the returned run.
463func (h *attachmentHost) Begin(ctx context.Context, token AttachmentToken, worker AttachmentWorker, stream ports.BrokerLogicalConnection) (*attachmentRun, bool) {
464	if h == nil || token.IsZero() || supervisorNil(worker) || supervisorNil(stream) {
465		return nil, false
466	}
467	if ctx == nil {
468		ctx = context.Background()
469	}
470	workerCtx, cancel := context.WithCancel(ctx)
471	fg := h.newForeground(token, stream)
472	if !h.authority.grant(fg) {
473		cancel()
474		return nil, false
475	}
476	// A request or action left behind by a previous foreground never applies
477	// to this one.
478	h.drainForegroundSignals()
479	if h.actionUI != nil {
480		fg.uiGeneration = h.actionUI.bindForeground(workerCtx, fg.input, fg.consumer)
481	}
482	run := &attachmentRun{
483		host:    h,
484		token:   token,
485		stream:  stream,
486		fg:      fg,
487		cancel:  cancel,
488		outcome: make(chan AttachmentEvent, 1),
489		done:    make(chan struct{}),
490		retired: make(chan struct{}),
491	}
492	go func() {
493		defer close(run.done)
494		event := worker.Run(workerCtx, fg)
495		// The host stamps the granted token: a worker can never report under a
496		// generation it was not granted, and the settlement check is exact.
497		event.Token = token
498		run.outcome <- event
499	}()
500	return run, true
501}
502
503// Run is Begin followed immediately by Wait, for callers that join in place.
504// The second result is false when the run was superseded or cancelled, in which
505// case the worker's late event is discarded.
506func (h *attachmentHost) Run(ctx context.Context, token AttachmentToken, worker AttachmentWorker, stream ports.BrokerLogicalConnection) (AttachmentEvent, bool) {
507	run, ok := h.Begin(ctx, token, worker, stream)
508	if !ok {
509		return AttachmentEvent{}, false
510	}
511	return run.Wait(ctx)
512}
513
514// newForeground allocates a candidate; grant claims input before publishing it.
515func (h *attachmentHost) setInput(input *terminalInputPump) {
516	if h == nil {
517		return
518	}
519	h.input = input
520	h.ownerBoundary = true
521}
522
523func (h *attachmentHost) startGeometry(ctx context.Context) {
524	if h == nil {
525		return
526	}
527	if h.geometryDone != nil {
528		select {
529		case <-h.geometryDone:
530			h.geometryDone = nil
531			h.geometryCancel = nil
532		default:
533			return
534		}
535	}
536	geometryCtx, cancel := context.WithCancel(ctx)
537	h.geometryCancel = cancel
538	h.geometryDone = make(chan struct{})
539	go func() {
540		defer close(h.geometryDone)
541		for {
542			select {
543			case geometry, ok := <-h.resizes:
544				if !ok {
545					return
546				}
547				if !geometry.Valid() {
548					continue
549				}
550				h.geometryMu.Lock()
551				h.geometry = geometry.NormalizePixels()
552				h.geometryValid = true
553				h.geometrySeq++
554				h.geometryMu.Unlock()
555				select {
556				case h.geometryUpdate <- struct{}{}:
557				default:
558				}
559				// Presentation invalidation is deliberately independent from the
560				// attachment geometry sequence/wakeup. The supervisor consumes this
561				// coalesced signal and decides whether a picker presentation may be
562				// repainted; this collector never renders directly.
563				select {
564				case h.presentationUpdate <- struct{}{}:
565				default:
566				}
567			case <-geometryCtx.Done():
568				return
569			}
570		}
571	}()
572}
573
574func (h *attachmentHost) stopGeometry() {
575	if h == nil || h.geometryCancel == nil {
576		return
577	}
578	h.geometryCancel()
579	<-h.geometryDone
580	h.geometryCancel = nil
581	h.geometryDone = nil
582}
583
584func (h *attachmentHost) latestGeometry() (domain.Geometry, bool) {
585	if h == nil {
586		return domain.Geometry{}, false
587	}
588	h.geometryMu.Lock()
589	defer h.geometryMu.Unlock()
590	return h.geometry, h.geometryValid
591}
592
593func (h *attachmentHost) geometryAfter(sequence uint64) (domain.Geometry, uint64, bool) {
594	h.geometryMu.Lock()
595	defer h.geometryMu.Unlock()
596	return h.geometry, h.geometrySeq, h.geometryValid && h.geometrySeq > sequence
597}
598
599func (h *attachmentHost) newForeground(token AttachmentToken, stream ports.BrokerLogicalConnection) *attachmentForeground {
600	return &attachmentForeground{
601		authority:  &h.authority,
602		token:      token,
603		stream:     stream,
604		input:      h.input,
605		lease:      newForegroundSendLease(),
606		term:       h.term,
607		ui:         h.ui,
608		resizes:    h.resizes,
609		actions:    h.actions,
610		host:       h,
611		done:       make(chan struct{}),
612		decided:    true,
613		onAttached: h.onAttach,
614		repaint:    make(chan struct{}, 1),
615		tabSelect:  make(chan domain.TabStableID, 1),
616		routes:     make(chan protocol.RecentRouteSnapshot, 1),
617		replies:    make(chan protocol.ClientMessage, attachmentReplyCapacity),
618		inPlace:    make(chan inPlaceSwitch, 1),
619	}
620}
621
622// drainForegroundSignals drops any navigation request or internal lifecycle
623// action that belonged to an earlier foreground.
624func (h *attachmentHost) drainForegroundSignals() {
625	for {
626		select {
627		case <-h.navigationRequests:
628		case <-h.internalActions:
629		case <-h.routeDemand:
630		case <-h.navigations:
631		case <-h.overlayActions:
632		case <-h.inPlaceResults:
633		default:
634			return
635		}
636	}
637}
638
639// NavigationRequests wakes the supervisor when the attached worker asked for
640// the client picker over the live attachment.
641func (h *attachmentHost) NavigationRequests() <-chan struct{} {
642	if h == nil {
643		return nil
644	}
645	return h.navigationRequests
646}
647
648// beginNavigationOverlay composes the supervisor-owned picker over the current
649// foreground: from its return, attachment output is applied and acknowledged
650// but never written, and every authorized input delivery is handed to sink. It
651// reports false when no active foreground can host the overlay.
652func (h *attachmentHost) beginNavigationOverlay(sink pickerInputConsumer) bool {
653	if h == nil || sink == nil {
654		return false
655	}
656	fg := h.authority.foreground()
657	if fg == nil {
658		return false
659	}
660	return fg.setOverlay(attachmentOverlayNavigation, sink)
661}
662
663// endNavigationOverlay returns the terminal to the current foreground. With
664// repaint it asks the worker for an authoritative repaint over the picker box.
665func (h *attachmentHost) endNavigationOverlay(repaint bool) {
666	if h == nil {
667		return
668	}
669	if fg := h.authority.foreground(); fg != nil {
670		fg.releaseOverlay(attachmentOverlayNavigation, repaint)
671	}
672}
673
674// committedTarget reports the session the current foreground last committed
675// output for, so the supervisor can tell a commit to the attached session from
676// a commit elsewhere.
677func (h *attachmentHost) committedTarget() (protocol.ExactSessionTarget, bool) {
678	target, _, ok := h.committedView()
679	return target, ok
680}
681
682// committedView reports the session and the tab the current foreground last
683// committed output for.
684func (h *attachmentHost) committedView() (protocol.ExactSessionTarget, domain.TabStableID, bool) {
685	if h == nil {
686		return protocol.ExactSessionTarget{}, "", false
687	}
688	fg := h.authority.foreground()
689	if fg == nil {
690		return protocol.ExactSessionTarget{}, "", false
691	}
692	fg.overlayMu.Lock()
693	defer fg.overlayMu.Unlock()
694	return fg.committed, fg.committedTab, fg.committedKnown
695}
696
697// committedTargetOrZero is committedTarget with an unknown target as zero.
698func (h *attachmentHost) committedTargetOrZero() protocol.ExactSessionTarget {
699	target, ok := h.committedTarget()
700	if !ok {
701		return protocol.ExactSessionTarget{}
702	}
703	return target
704}
705
706// requestTabSelection asks the current foreground's worker to show one tab of
707// its attached session in place. The newest request replaces an unsent one.
708func (h *attachmentHost) requestTabSelection(token AttachmentToken, tab domain.TabStableID) bool {
709	if h == nil || tab == "" {
710		return false
711	}
712	fg := h.authority.foreground()
713	if fg == nil || fg.token != token {
714		return false
715	}
716	for {
717		select {
718		case fg.tabSelect <- tab:
719			return true
720		default:
721		}
722		select {
723		case <-fg.tabSelect:
724		default:
725		}
726	}
727}
728
729// requestDetach asks the current foreground's worker to detach explicitly, so
730// the daemon observes a clean Detach before the supervisor swaps attachments.
731func (h *attachmentHost) requestDetach(token AttachmentToken) {
732	if h == nil {
733		return
734	}
735	select {
736	case h.internalActions <- AttachmentLifecycleAction{Token: token, Kind: AttachmentDetachToPicker}:
737	default:
738	}
739}
740
741// attachmentOverlayKind names the client-composed overlay that owns the
742// terminal over a live attachment.
743type attachmentOverlayKind uint8
744
745const (
746	attachmentOverlayNone attachmentOverlayKind = iota
747	// attachmentOverlayNavigation is the supervisor-owned session picker.
748	attachmentOverlayNavigation
749	// attachmentOverlayMove is the worker-owned move-destination picker the
750	// serving daemon offered.
751	attachmentOverlayMove
752)
753
754// attachmentOverlayForeground is the optional overlay seam of the real
755// foreground. A worker reaches it by assertion so scripted foregrounds may
756// omit it; without it, overlays are simply never presented.
757type attachmentOverlayForeground interface {
758	requestNavigationPicker()
759	setOverlay(kind attachmentOverlayKind, sink pickerInputConsumer) bool
760	clearOverlay(kind attachmentOverlayKind)
761	overlayKind() attachmentOverlayKind
762	divertInput(data []byte, actionID uint64) bool
763	overlayRepaint() <-chan struct{}
764	overlayOutput(uiContext ports.UIContext, data []byte) error
765	noteCommitted(target protocol.ExactSessionTarget, tab domain.TabStableID)
766	tabSelections() <-chan domain.TabStableID
767	routeSnapshots() <-chan protocol.RecentRouteSnapshot
768	navigationReplies() <-chan protocol.ClientMessage
769	requestNavigation(message protocol.ServerMessage)
770	inPlaceSwitches() <-chan inPlaceSwitch
771	reportInPlace(seq uint64, outcome inPlaceOutcome)
772}
773
774var _ attachmentOverlayForeground = (*attachmentForeground)(nil)
775
776// requestNavigationPicker records the daemon's navigation offer for the
777// supervisor. The request is coalesced; the supervisor decides whether the
778// current presentation can host the overlay.
779func (f *attachmentForeground) requestNavigationPicker() {
780	if f == nil || f.host == nil || !f.authority.actionAuthorized(f) {
781		return
782	}
783	select {
784	case f.host.navigationRequests <- struct{}{}:
785	default:
786	}
787}
788
789// setOverlay installs kind under the output lease, so no attachment frame is
790// mid-write when the overlay takes the terminal. An occupied slot refuses a
791// different overlay.
792func (f *attachmentForeground) setOverlay(kind attachmentOverlayKind, sink pickerInputConsumer) bool {
793	if f == nil || kind == attachmentOverlayNone {
794		return false
795	}
796	installed := false
797	f.lease.send(func() bool {
798		if !f.authority.actionAuthorized(f) {
799			return false
800		}
801		f.overlayMu.Lock()
802		defer f.overlayMu.Unlock()
803		if f.overlay != attachmentOverlayNone && f.overlay != kind {
804			return true
805		}
806		f.overlay = kind
807		f.overlaySink = sink
808		installed = true
809		return true
810	})
811	return installed
812}
813
814// clearOverlay releases kind under the output lease and asks the worker for an
815// authoritative repaint: the terminal still shows the picker box and any
816// attachment frames it suppressed were never written.
817func (f *attachmentForeground) clearOverlay(kind attachmentOverlayKind) {
818	f.releaseOverlay(kind, true)
819}
820
821// releaseOverlay is clearOverlay with the repaint request optional.
822func (f *attachmentForeground) releaseOverlay(kind attachmentOverlayKind, repaint bool) {
823	if f == nil {
824		return
825	}
826	cleared := false
827	f.lease.send(func() bool {
828		f.overlayMu.Lock()
829		defer f.overlayMu.Unlock()
830		if f.overlay != kind {
831			return true
832		}
833		f.overlay = attachmentOverlayNone
834		f.overlaySink = nil
835		cleared = true
836		return true
837	})
838	if !cleared {
839		// The lease stops at finalization; the overlay dies with the grant.
840		f.overlayMu.Lock()
841		if f.overlay == kind {
842			f.overlay = attachmentOverlayNone
843			f.overlaySink = nil
844		}
845		f.overlayMu.Unlock()
846		return
847	}
848	if !repaint {
849		return
850	}
851	select {
852	case f.repaint <- struct{}{}:
853	default:
854	}
855}
856
857func (f *attachmentForeground) overlayKind() attachmentOverlayKind {
858	if f == nil {
859		return attachmentOverlayNone
860	}
861	f.overlayMu.Lock()
862	defer f.overlayMu.Unlock()
863	return f.overlay
864}
865
866// divertInput hands one authorized delivery to the supervisor-owned overlay.
867// It reports false when no navigation overlay owns input, so the worker keeps
868// its session path. A driver action consumed here never reaches the daemon, so
869// it is marked before the picker sees the bytes and handed to the supervisor
870// afterwards, which settles it at its own event-loop boundary (or follows it
871// across the swap a commit starts).
872func (f *attachmentForeground) divertInput(data []byte, actionID uint64) bool {
873	if f == nil {
874		return false
875	}
876	f.overlayMu.Lock()
877	sink := f.overlaySink
878	kind := f.overlay
879	f.overlayMu.Unlock()
880	if kind != attachmentOverlayNavigation || sink == nil {
881		return false
882	}
883	tracked := actionID != 0 && f.uiGeneration != 0 && f.host != nil && f.host.actionUI != nil
884	if tracked {
885		f.host.actionUI.markOverlayInput(f.uiGeneration, actionID)
886	}
887	sink.ConsumeTerminalRead(data)
888	if tracked {
889		f.host.offerOverlayAction(overlayActionConsumed{generation: f.uiGeneration, actionID: actionID})
890	}
891	return true
892}
893
894// overlayActionConsumed names one driver action the client picker overlay
895// consumed on the given UI generation.
896type overlayActionConsumed struct {
897	generation uint64
898	actionID   uint64
899}
900
901// offerOverlayAction hands the latest overlay-consumed action to the
902// supervisor. Automation admits one action at a time and the worker is the only
903// producer, so replacing an unread stale entry never drops a live one.
904func (h *attachmentHost) offerOverlayAction(consumed overlayActionConsumed) {
905	for {
906		select {
907		case h.overlayActions <- consumed:
908			return
909		default:
910		}
911		select {
912		case <-h.overlayActions:
913		default:
914		}
915	}
916}
917
918// OverlayActions wakes the supervisor for each driver action the picker
919// overlay consumed.
920func (h *attachmentHost) OverlayActions() <-chan overlayActionConsumed {
921	if h == nil {
922		return nil
923	}
924	return h.overlayActions
925}
926
927func (f *attachmentForeground) overlayRepaint() <-chan struct{} {
928	if f == nil {
929		return nil
930	}
931	return f.repaint
932}
933
934// committedSnapshot reports the session and tab this foreground last
935// committed output for. It stays readable after the foreground is finished,
936// so a settled attachment can still name what it was showing.
937func (f *attachmentForeground) committedSnapshot() (protocol.ExactSessionTarget, domain.TabStableID, bool) {
938	if f == nil {
939		return protocol.ExactSessionTarget{}, "", false
940	}
941	f.overlayMu.Lock()
942	defer f.overlayMu.Unlock()
943	return f.committed, f.committedTab, f.committedKnown
944}
945
946func (f *attachmentForeground) noteCommitted(target protocol.ExactSessionTarget, tab domain.TabStableID) {
947	if f == nil {
948		return
949	}
950	f.overlayMu.Lock()
951	changed := !f.committedKnown || f.committed != target
952	f.committed = target
953	f.committedTab = tab
954	f.committedKnown = true
955	f.overlayMu.Unlock()
956	if changed {
957		f.demandRoutes()
958	}
959}
960
961// tabSelections carries the supervisor's in-place tab switches to the worker.
962func (f *attachmentForeground) tabSelections() <-chan domain.TabStableID {
963	if f == nil {
964		return nil
965	}
966	return f.tabSelect
967}
968
969// overlayOutput writes one overlay frame through the same lease and UI
970// transaction as attachment output, bypassing only the overlay suppression.
971func (f *attachmentForeground) overlayOutput(uiContext ports.UIContext, data []byte) error {
972	return f.write(uiContext, data, true)
973}
974
975// attachmentQueryForeground is the optional terminal-query seam of the real
976// foreground. Scripted foregrounds may omit it; the worker then skips palette
977// detection but still strips terminal replies from input.
978type attachmentQueryForeground interface {
979	writeTerminalQuery(data []byte) error
980}
981
982var _ attachmentQueryForeground = (*attachmentForeground)(nil)
983
984// writeTerminalQuery writes and flushes terminal query bytes (palette probes)
985// under the output lease. They draw nothing, so neither an overlay nor the UI
986// observation channel is involved.
987func (f *attachmentForeground) writeTerminalQuery(data []byte) error {
988	if f == nil {
989		return errAttachmentForegroundRevoked
990	}
991	var err error
992	ok := f.lease.send(func() bool {
993		if !f.authority.actionAuthorized(f) {
994			return false
995		}
996		if supervisorNil(f.term) || supervisorNil(f.term.Out()) {
997			err = ports.ErrUIUnavailable
998			return true
999		}
1000		var n int
1001		n, err = f.term.Out().Write(data)
1002		if err == nil && n != len(data) {
1003			err = io.ErrShortWrite
1004		}
1005		if err == nil {
1006			err = f.term.Flush()
1007		}
1008		return true
1009	})
1010	if !ok {
1011		return errAttachmentForegroundRevoked
1012	}
1013	return err
1014}
1015
1016// attachmentPredictionForeground is the optional predictive-echo seam of the
1017// real foreground. Scripted foregrounds may omit it; the worker then never
1018// predicts.
1019type attachmentPredictionForeground interface {
1020	writePrediction(data []byte) (bool, error)
1021}
1022
1023var _ attachmentPredictionForeground = (*attachmentForeground)(nil)
1024
1025// writePrediction writes and flushes predicted echo under the output lease.
1026// It reports false without writing while an overlay owns the terminal. The
1027// guesses are client-local drawing, so the UI observation channel is not
1028// involved.
1029func (f *attachmentForeground) writePrediction(data []byte) (bool, error) {
1030	if f == nil {
1031		return false, errAttachmentForegroundRevoked
1032	}
1033	var err error
1034	written := false
1035	ok := f.lease.send(func() bool {
1036		if !f.authority.actionAuthorized(f) {
1037			return false
1038		}
1039		if f.overlayKind() != attachmentOverlayNone {
1040			return true
1041		}
1042		if supervisorNil(f.term) || supervisorNil(f.term.Out()) {
1043			err = ports.ErrUIUnavailable
1044			return true
1045		}
1046		var n int
1047		n, err = f.term.Out().Write(data)
1048		if err == nil && n != len(data) {
1049			err = io.ErrShortWrite
1050		}
1051		if err == nil {
1052			err = f.term.Flush()
1053		}
1054		written = err == nil
1055		return true
1056	})
1057	if !ok {
1058		return false, errAttachmentForegroundRevoked
1059	}
1060	return written, err
1061}
1062
1063// attachmentRun is one in-flight worker generation owned by the supervisor.
1064type attachmentRun struct {
1065	host   *attachmentHost
1066	token  AttachmentToken
1067	stream ports.BrokerLogicalConnection
1068	fg     *attachmentForeground
1069	cancel context.CancelFunc
1070	// outcome is capacity one so a worker that settles after the supervisor has
1071	// already moved on can never block on its own return.
1072	outcome chan AttachmentEvent
1073	// done closes when the worker goroutine has returned.
1074	done      chan struct{}
1075	joinOnce  sync.Once
1076	stateMu   sync.Mutex
1077	retired   chan struct{}
1078	isRetired bool
1079}
1080
1081// Wait joins the run. It returns the worker's typed event with adopted=true
1082// only when the worker settled on its own and the supervisor had not cancelled;
1083// a run that was cancelled or superseded is joined and its late event discarded
1084// (adopted=false). The caller's ctx stops waiting for an outcome; retirement
1085// then uses the separate bounded worker-join budget.
1086func (r *attachmentRun) Wait(ctx context.Context) (AttachmentEvent, bool) {
1087	if ctx == nil {
1088		ctx = context.Background()
1089	}
1090	select {
1091	case event := <-r.outcome:
1092		// Settlement and external retirement compete at one state boundary.
1093		adopted := r.retire() && ctx.Err() == nil && event.Token == r.token
1094		r.cleanup()
1095		if adopted {
1096			return event, true
1097		}
1098	case <-r.retired:
1099		r.cleanup()
1100	case <-ctx.Done():
1101		r.Cancel()
1102	}
1103	return AttachmentEvent{}, false
1104}
1105
1106// retire is authoritative even when outcome and cancellation are both ready.
1107func (r *attachmentRun) retire() bool {
1108	r.stateMu.Lock()
1109	defer r.stateMu.Unlock()
1110	if r.isRetired {
1111		return false
1112	}
1113	r.isRetired = true
1114	close(r.retired)
1115	return true
1116}
1117
1118// Cancel marks retirement, runs the deterministic final input window, and
1119// returns once the worker is joined or the join bound expires and the retained
1120// consumer and stream are released.
1121func (r *attachmentRun) Cancel() {
1122	r.retire()
1123	r.cleanup()
1124}
1125
1126// cleanup is the ordered teardown. It refuses further output/UI/resize/new input
1127// and closes Done, wakes the worker, then joins within the bound while the pump
1128// consumer stays authorized for Ack/Preserve, so a worker woken by Done can
1129// still save the read it holds. Only after the join (or its timeout) does it
1130// revoke the consumer and foreground slot, then close the owned stream.
1131func (r *attachmentRun) cleanup() {
1132	r.joinOnce.Do(func() {
1133		r.fg.finalize()
1134		r.cancel()
1135		r.join()
1136		r.fg.finish()
1137		_ = r.stream.Close()
1138	})
1139}
1140
1141// join waits for the worker goroutine within the shared join bound. It allocates
1142// a timer only when the worker has not already returned.
1143func (r *attachmentRun) join() {
1144	select {
1145	case <-r.done:
1146		return
1147	default:
1148	}
1149	timer := r.host.clock.NewTimer(attachmentWorkerJoinTimeout)
1150	if supervisorNil(timer) {
1151		timer = systemClock{}.NewTimer(attachmentWorkerJoinTimeout)
1152	}
1153	defer stopSupervisorTimer(timer)
1154	select {
1155	case <-r.done:
1156	case <-timer.C():
1157	}
1158}
1159
1160// attachmentForeground is the supervisor-granted implementation of
1161// AttachmentForeground. It reuses the existing terminal input pump for input
1162// and the existing foreground send lease for output serialization.
1163type attachmentForeground struct {
1164	authority    *attachmentAuthority
1165	token        AttachmentToken
1166	stream       ports.BrokerLogicalConnection
1167	input        *terminalInputPump
1168	consumer     uint64
1169	lease        *foregroundSendLease
1170	term         ports.Terminal
1171	ui           ports.UIOutputTransaction
1172	uiGeneration uint64
1173	resizes      <-chan domain.Geometry
1174	actions      <-chan AttachmentLifecycleAction
1175	host         *attachmentHost
1176	done         chan struct{}
1177	// finalizedOnce and finishedOnce split the single teardown into the
1178	// deterministic final input window: finalize closes Done and stops output
1179	// first, finish revokes the retained consumer only after the join.
1180	finalizedOnce sync.Once
1181	finishedOnce  sync.Once
1182	// deliveryMu guards the at-most-once input decision. Input opens the window
1183	// by marking a freshly received delivery undecided; AckInput or PreserveInput
1184	// decides it at most once. decided starts true, so a decision with no
1185	// outstanding delivery is refused instead of silently committing nothing.
1186	deliveryMu  sync.Mutex
1187	decided     bool
1188	geometrySeq uint64
1189	attached    atomic.Bool
1190	onAttached  func(AttachmentToken)
1191
1192	// overlayMu guards the overlay slot and the committed target. The slot is
1193	// changed only inside the output lease, so an overlay never takes the
1194	// terminal while an attachment frame is mid-write.
1195	overlayMu      sync.Mutex
1196	overlay        attachmentOverlayKind
1197	overlaySink    pickerInputConsumer
1198	committed      protocol.ExactSessionTarget
1199	committedTab   domain.TabStableID
1200	committedKnown bool
1201	// repaint wakes the worker to request an authoritative repaint once an
1202	// overlay released the terminal.
1203	repaint chan struct{}
1204	// tabSelect carries one pending in-place tab switch to the worker.
1205	tabSelect chan domain.TabStableID
1206	// routes carries the newest client route snapshot to the worker; replies
1207	// carries the supervisor's navigation failures.
1208	routes  chan protocol.RecentRouteSnapshot
1209	replies chan protocol.ClientMessage
1210	// inPlace carries one pending supervisor in-place switch to the worker.
1211	inPlace chan inPlaceSwitch
1212}
1213
1214// Token is the generation/attempt identity of this grant.
1215func (f *attachmentForeground) Token() AttachmentToken { return f.token }
1216
1217func (f *attachmentForeground) actionableGeneration() uint64 {
1218	if f.uiGeneration != 0 {
1219		return f.uiGeneration
1220	}
1221	return f.token.Generation
1222}
1223
1224func (f *attachmentForeground) uiPublished() {
1225	if f.uiGeneration == 0 || f.host == nil || f.host.actionUI == nil {
1226		return
1227	}
1228	f.host.actionUI.published(f.uiGeneration)
1229}
1230
1231func (f *attachmentForeground) uiReceipt(receipt protocol.UIReceipt) {
1232	if f.uiGeneration != 0 && f.host != nil && f.host.actionUI != nil {
1233		f.host.actionUI.receipt(f.uiGeneration, receipt)
1234	}
1235}
1236
1237// uiFollowSamePeer keeps the driver action that caused a same-peer offer
1238// pending until the in-place destination commits.
1239func (f *attachmentForeground) uiFollowSamePeer(actionID uint64) {
1240	if actionID != 0 && f.uiGeneration != 0 && f.host != nil && f.host.actionUI != nil {
1241		f.host.actionUI.follow(f.uiGeneration, actionID)
1242	}
1243}
1244
1245// uiSamePeerArrived runs before the destination's first output is published.
1246func (f *attachmentForeground) uiSamePeerArrived() {
1247	if f.uiGeneration != 0 && f.host != nil && f.host.actionUI != nil {
1248		f.host.actionUI.arriveInPlace(f.uiGeneration)
1249	}
1250}
1251
1252// uiSamePeerFailed fails the followed action of a refused same-peer switch.
1253func (f *attachmentForeground) uiSamePeerFailed() {
1254	if f.uiGeneration != 0 && f.host != nil && f.host.actionUI != nil {
1255		f.host.actionUI.failInPlace(f.uiGeneration)
1256	}
1257}
1258
1259// Stream is the supervisor-admitted logical stream this worker owns.
1260func (f *attachmentForeground) Stream() ports.BrokerLogicalConnection { return f.stream }
1261
1262// Done is closed when the supervisor starts finalizing this grant. It stops
1263// Input and Resize and wakes a worker so it can still Ack/Preserve the read it
1264// holds; the output gate is stopped at the same time.
1265func (f *attachmentForeground) Done() <-chan struct{} { return f.done }
1266
1267// finalize opens the final input window exactly once: it moves the grant into
1268// the finalizing authority state, stops the output gate (waiting for any
1269// in-flight output transaction to end), and closes Done so Input/Resize refuse
1270// new work and the worker wakes. The pump consumer and foreground slot stay
1271// owned, so the worker may still Ack/Preserve the read it holds.
1272// Lock order is authority -> send lease; beginFinalize releases authority before
1273// waiting on the lease, so output never observes authority while it holds the
1274// lease.
1275func (f *attachmentForeground) finalize() {
1276	if f == nil {
1277		return
1278	}
1279	f.finalizedOnce.Do(func() {
1280		f.authority.beginFinalize(f)
1281		if f.lease != nil {
1282			f.lease.stop()
1283		}
1284		if f.uiGeneration != 0 && f.host != nil && f.host.actionUI != nil {
1285			f.host.actionUI.releaseForeground(f.uiGeneration)
1286		}
1287		close(f.done)
1288	})
1289}
1290
1291// finish revokes the retained consumer and clears the foreground slot after the
1292// bounded join. It runs before the stream is closed, so undecided input either
1293// reached the pump during finalization or is dropped with the revocation.
1294func (f *attachmentForeground) finish() {
1295	if f == nil {
1296		return
1297	}
1298	f.finishedOnce.Do(func() {
1299		// Finish is lossless: input kept for a resume must survive it. The
1300		// supervisor drops unclaimed bytes (dropUnclaimed) when the terminal
1301		// returns to the picker, the owner-class boundary no attachment byte
1302		// may cross.
1303		f.authority.revoke(f)
1304	})
1305}
1306
1307// beginDelivery opens the decision window for the delivery the worker just
1308// received: the next AckInput or PreserveInput decides it, and a further
1309// decision is refused until Input receives another delivery.
1310func (f *attachmentForeground) beginDelivery() {
1311	f.deliveryMu.Lock()
1312	f.decided = false
1313	f.deliveryMu.Unlock()
1314}
1315
1316// decideDelivery reports whether this is the first decision for the current
1317// delivery and marks it decided. A second Ack/Preserve for the same delivery,
1318// or a decision with no outstanding delivery, is refused.
1319func (f *attachmentForeground) decideDelivery() bool {
1320	f.deliveryMu.Lock()
1321	defer f.deliveryMu.Unlock()
1322	if f.decided {
1323		return false
1324	}
1325	f.decided = true
1326	return true
1327}
1328
1329// Input leases the next authorized terminal read. It waits on the pump's ready
1330// signal exactly like the attach scanner, so a shared single reader is never
1331// duplicated.
1332func (f *attachmentForeground) Input(ctx context.Context) (AttachmentInputEvent, bool) {
1333	if ctx == nil {
1334		ctx = context.Background()
1335	}
1336	for {
1337		if !f.authority.actionAuthorized(f) || f.input == nil {
1338			return AttachmentInputEvent{}, false
1339		}
1340		result, ok := f.input.take(ctx, f.consumer)
1341		if ok {
1342			f.beginDelivery()
1343			return AttachmentInputEvent{Data: result.data, Err: result.err, actionID: result.actionID}, true
1344		}
1345		if ctx.Err() != nil {
1346			return AttachmentInputEvent{}, false
1347		}
1348		select {
1349		case <-ctx.Done():
1350			return AttachmentInputEvent{}, false
1351		case <-f.done:
1352			return AttachmentInputEvent{}, false
1353		case request := <-f.input.automation:
1354			f.input.mu.Lock()
1355			clean := f.input.consumer == f.consumer && request.consumer == f.consumer && f.input.pending == nil && len(f.input.residual) == 0 && f.input.delivering == 0
1356			f.input.mu.Unlock()
1357			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)
1358			if clean && request.owner != nil {
1359				clean = request.owner.accept(request.record.actionID, request.record.generation)
1360			}
1361			request.admitted <- clean
1362			if !clean {
1363				continue
1364			}
1365			request.dispatched <- true
1366			return AttachmentInputEvent{Data: append([]byte(nil), request.record.data...), actionID: request.record.actionID}, true
1367		case <-f.input.readyFor(f.consumer):
1368		}
1369	}
1370}
1371
1372// AckInput commits the last delivery so the shared reader may publish the next.
1373// It decides that delivery, so a duplicate Ack/Preserve for it is a no-op.
1374func (f *attachmentForeground) AckInput() {
1375	if f == nil || f.input == nil {
1376		return
1377	}
1378	if !f.decideDelivery() {
1379		return
1380	}
1381	f.authority.retain(f, func() { f.input.ack(f.consumer) })
1382}
1383
1384// PreserveInput re-queues the undecided bytes of the last delivery for the next
1385// authorized attempt. It decides that delivery, so a duplicate Ack/Preserve for
1386// it is a no-op and can never re-queue different bytes. It stays authorized
1387// while the supervisor finalizes the grant.
1388func (f *attachmentForeground) PreserveInput(data []byte) {
1389	if f == nil || f.input == nil {
1390		return
1391	}
1392	if !f.decideDelivery() {
1393		return
1394	}
1395	f.authority.retain(f, func() {
1396		f.input.ack(f.consumer)
1397		f.input.preserveResidual(f.consumer, data)
1398	})
1399}
1400
1401// Resize waits for the next authorized terminal resize.
1402func (f *attachmentForeground) Lifecycle(ctx context.Context) (AttachmentLifecycleActionKind, bool) {
1403	if ctx == nil {
1404		ctx = context.Background()
1405	}
1406	for {
1407		if !f.authority.actionAuthorized(f) {
1408			return 0, false
1409		}
1410		var internal <-chan AttachmentLifecycleAction
1411		if f.host != nil {
1412			internal = f.host.internalActions
1413		}
1414		select {
1415		case action, ok := <-f.actions:
1416			if !ok || !f.authority.actionAuthorized(f) {
1417				return 0, false
1418			}
1419			if action.Token != f.token {
1420				continue
1421			}
1422			return action.Kind, action.Kind == AttachmentDetachToPicker || action.Kind == AttachmentDetachAndExit
1423		case action := <-internal:
1424			if action.Token != f.token || !f.authority.actionAuthorized(f) {
1425				continue
1426			}
1427			return action.Kind, action.Kind == AttachmentDetachToPicker || action.Kind == AttachmentDetachAndExit
1428		case <-ctx.Done():
1429			return 0, false
1430		case <-f.done:
1431			return 0, false
1432		}
1433	}
1434}
1435
1436func (f *attachmentForeground) Resize(ctx context.Context) (domain.Geometry, bool) {
1437	if ctx == nil {
1438		ctx = context.Background()
1439	}
1440	if !f.authority.actionAuthorized(f) || f.host == nil {
1441		return domain.Geometry{}, false
1442	}
1443	if !f.host.ownerBoundary {
1444		resizes := f.resizes
1445		for {
1446			select {
1447			case geometry, ok := <-resizes:
1448				if !ok {
1449					resizes = nil
1450					continue
1451				}
1452				if !f.authority.actionAuthorized(f) {
1453					return domain.Geometry{}, false
1454				}
1455				return geometry, true
1456			case <-ctx.Done():
1457				return domain.Geometry{}, false
1458			case <-f.done:
1459				return domain.Geometry{}, false
1460			}
1461		}
1462	}
1463	for {
1464		if geometry, next, ok := f.host.geometryAfter(f.geometrySeq); ok {
1465			f.geometrySeq = next
1466			if f.authority.actionAuthorized(f) {
1467				return geometry, true
1468			}
1469			return domain.Geometry{}, false
1470		}
1471		select {
1472		case <-ctx.Done():
1473			return domain.Geometry{}, false
1474		case <-f.done:
1475			return domain.Geometry{}, false
1476		case <-f.host.geometryUpdate:
1477		}
1478	}
1479}
1480
1481// Output holds the same send lease across the entire UI observation boundary.
1482// UIOutputTransaction is an observation channel, not attachment commit
1483// authority: an unavailable or closed observation channel does not stop the
1484// terminal frame. Other publication failures abort. Successful terminal write,
1485// flush, and EndOutput(true) commit the frame; any failed or short write or
1486// failed flush leaves EndOutput(false).
1487func (f *attachmentForeground) Output(uiContext ports.UIContext, data []byte) error {
1488	return f.write(uiContext, data, false)
1489}
1490
1491// write is the shared output transaction. While an overlay owns the terminal an
1492// attachment frame (overlay=false) is accepted without being written or
1493// published: the caller still applies and acknowledges it, and the overlay's
1494// release requests an authoritative repaint. Overlay frames bypass only that
1495// suppression.
1496func (f *attachmentForeground) write(uiContext ports.UIContext, data []byte, overlay bool) error {
1497	if f == nil {
1498		return errAttachmentForegroundRevoked
1499	}
1500	var outputErr error
1501	ok := f.lease.send(func() bool {
1502		if !f.authority.actionAuthorized(f) {
1503			return false
1504		}
1505		if !overlay && f.overlayKind() != attachmentOverlayNone {
1506			// Applied, not written: remember its committed boundary so a
1507			// daemon receipt fenced on it still settles its action.
1508			if f.uiGeneration != 0 && f.host != nil && f.host.actionUI != nil {
1509				f.host.actionUI.suppressedOutput(f.uiGeneration, f.presentationContext(uiContext))
1510			}
1511			return true
1512		}
1513		if supervisorNil(f.term) {
1514			outputErr = ports.ErrUIUnavailable
1515			return true
1516		}
1517		writer := f.term.Out()
1518		if supervisorNil(writer) {
1519			outputErr = ports.ErrUIUnavailable
1520			return true
1521		}
1522		uiContext = f.presentationContext(uiContext)
1523		hasUI := !supervisorNil(f.ui)
1524		if hasUI {
1525			f.ui.BeginOutput(uiContext)
1526		}
1527		success := false
1528		defer func() {
1529			if hasUI {
1530				f.ui.EndOutput(success)
1531				if success {
1532					f.uiPublished()
1533				}
1534			}
1535		}()
1536		// An unavailable capture means the optional UI observation channel is
1537		// disabled or closed, so the terminal frame still writes and flushes.
1538		// Any other publication error aborts before the frame is written.
1539		if hasUI {
1540			if err := f.ui.PublishContext(uiContext); err != nil && !errors.Is(err, ports.ErrUIUnavailable) {
1541				outputErr = err
1542				return true
1543			}
1544		}
1545		var n int
1546		n, outputErr = writer.Write(data)
1547		if outputErr == nil && n != len(data) {
1548			outputErr = io.ErrShortWrite
1549		}
1550		if outputErr != nil {
1551			return true
1552		}
1553		outputErr = f.term.Flush()
1554		success = outputErr == nil
1555		return true
1556	})
1557	if !ok {
1558		return errAttachmentForegroundRevoked
1559	}
1560	return outputErr
1561}
1562
1563// presentationContext applies the published-context shape rule to one foreground
1564// transaction. Only an Attached transaction carries the real actionable
1565// generation and the committed session metadata; any other presentation carries
1566// the run's stable UI handle and its status with every other field zeroed. A
1567// pre-attach Connecting frame therefore never publishes an actionable
1568// generation or session identity, and no caller can fence an action against a
1569// generation the attachment has not committed.
1570func (f *attachmentForeground) presentationContext(identity ports.UIContext) ports.UIContext {
1571	handle := identity.AttachmentHandle
1572	if f.host != nil && f.host.actionUI != nil {
1573		handle = f.host.actionUI.Handle()
1574	}
1575	if identity.Status != ports.UIStatusAttached {
1576		return ports.UIContext{AttachmentHandle: handle, Status: identity.Status}
1577	}
1578	identity.Generation = f.actionableGeneration()
1579	identity.AttachmentHandle = handle
1580	return identity
1581}
1582
1583// PublishAttached publishes the committed attached presentation. It takes the
1584// same send lease and the same authorization fence as Output, so it can never
1585// race a finalization or a revoked generation, and it writes no bytes: the
1586// attached presentation describes the frame that already drained. A closed UI
1587// observation channel is tolerated exactly as Output tolerates it, while any
1588// other publication failure is reported and leaves the run unattached.
1589func (f *attachmentForeground) PublishAttached(uiContext ports.UIContext) error {
1590	if f == nil {
1591		return errAttachmentForegroundRevoked
1592	}
1593	var outputErr error
1594	ok := f.lease.send(func() bool {
1595		if !f.authority.actionAuthorized(f) {
1596			return false
1597		}
1598		if supervisorNil(f.ui) {
1599			return true
1600		}
1601		uiContext = f.presentationContext(uiContext)
1602		f.ui.BeginOutput(uiContext)
1603		publishErr := f.ui.PublishContext(uiContext)
1604		if publishErr != nil && !errors.Is(publishErr, ports.ErrUIUnavailable) {
1605			outputErr = publishErr
1606			f.ui.EndOutput(false)
1607			return true
1608		}
1609		f.ui.EndOutput(true)
1610		f.uiPublished()
1611		return true
1612	})
1613	if !ok {
1614		return errAttachmentForegroundRevoked
1615	}
1616	return outputErr
1617}
1618
1619// MarkAttached records the attached transition atomically with respect to
1620// finalization and revocation.
1621func (f *attachmentForeground) MarkAttached() bool {
1622	if f == nil {
1623		return false
1624	}
1625	set := f.authority.act(f, func() { f.attached.Store(true) })
1626	if !set {
1627		return false
1628	}
1629	if f.onAttached != nil {
1630		f.onAttached(f.token)
1631	}
1632	return true
1633}
1634
1635// Attached reports whether MarkAttached has succeeded.
1636func (f *attachmentForeground) Attached() bool {
1637	return f != nil && f.attached.Load()
1638}

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