fix(daemon): publish headless preview invalidations
- commit
- 5d071d643b119125edfa37d89d2d5a788fa03c85
- author
- brice · · 2026-07-11 21:44 UTC
- parent
- 1a1a903
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +11 -5
M internal/usecase/daemon/render_coordinator.go +11 -5
| @@ -252,9 +252,11 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) { | |||
| 252 | 252 | func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv renderInvalidation) { | |
| 253 | 253 | c.mu.Lock() | |
| 254 | 254 | // An unbound coordinator is the manual/headless harness state; there is no | |
| 255 | - | // published production attachment to protect there. Once an attachment has | |
| 256 | - | // been bound (or detached), identity must match exactly. | |
| 257 | - | if c.torndown || c.detached || (source != nil && c.attachment != nil && c.attachment != source) { | |
| 255 | + | // published production attachment to protect there. A parked target may | |
| 256 | + | // still publish its own PTY/session mutations to picker observers, but no | |
| 257 | + | // attachment-owned callback may revive that target's render path. | |
| 258 | + | detachedPreviewOnly := c.detached && source == nil && (c.previewWake != nil || len(c.previewWakes) != 0) | |
| 259 | + | if c.torndown || (c.detached && !detachedPreviewOnly) || (source != nil && c.attachment != nil && c.attachment != source) { | |
| 258 | 260 | c.mu.Unlock() | |
| 259 | 261 | return | |
| 260 | 262 | } | |
| @@ -797,7 +799,11 @@ func (c *renderCoordinator) fire(gen uint64, watchdog, deadline bool) { | |||
| 797 | 799 | attachmentEpoch = c.attachmentEpoch | |
| 798 | 800 | } | |
| 799 | 801 | w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog, attachment: c.attachment, attachmentEpoch: attachmentEpoch} | |
| 800 | - | if !ready || (c.attachment != nil && !c.attachmentReady) { | |
| 802 | + | // A detached target has no primary transport or ACK window. Its pending | |
| 803 | + | // state belongs exclusively to picker previews and must be consumed after | |
| 804 | + | // notification rather than accumulating ackDeferred forever. | |
| 805 | + | headlessPreviewOnly := c.detached && c.attachment == nil | |
| 806 | + | if !headlessPreviewOnly && (!ready || (c.attachment != nil && !c.attachmentReady)) { | |
| 801 | 807 | if !ready && deadline { | |
| 802 | 808 | c.ackDeferred = true | |
| 803 | 809 | } | |
| @@ -824,7 +830,7 @@ func (c *renderCoordinator) fire(gen uint64, watchdog, deadline bool) { | |||
| 824 | 830 | wake := c.opts.wake | |
| 825 | 831 | c.mu.Unlock() | |
| 826 | 832 | stopTimer(timer) | |
| 827 | - | if wake != nil { | |
| 833 | + | if wake != nil && !headlessPreviewOnly { | |
| 828 | 834 | wake(w) | |
| 829 | 835 | } | |
| 830 | 836 | c.notifyPreviews(w, preview, previews) | |
| @@ -252,9 +252,11 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) { | |||||
| 252 | func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv renderInvalidation) { | 252 | func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv renderInvalidation) { | ||
| 253 | c.mu.Lock() | 253 | c.mu.Lock() | ||
| 254 | // An unbound coordinator is the manual/headless harness state; there is no | 254 | // An unbound coordinator is the manual/headless harness state; there is no | ||
| 255 | - | // published production attachment to protect there. Once an attachment has | 255 | + | // published production attachment to protect there. A parked target may |
| 256 | - | // been bound (or detached), identity must match exactly. | 256 | + | // still publish its own PTY/session mutations to picker observers, but no |
| 257 | - | if c.torndown || c.detached || (source != nil && c.attachment != nil && c.attachment != source) { | 257 | + | // attachment-owned callback may revive that target's render path. |
| 258 | + | detachedPreviewOnly := c.detached && source == nil && (c.previewWake != nil || len(c.previewWakes) != 0) | |||
| 259 | + | if c.torndown || (c.detached && !detachedPreviewOnly) || (source != nil && c.attachment != nil && c.attachment != source) { | |||
| 258 | c.mu.Unlock() | 260 | c.mu.Unlock() | ||
| 259 | return | 261 | return | ||
| 260 | } | 262 | } | ||
| @@ -797,7 +799,11 @@ func (c *renderCoordinator) fire(gen uint64, watchdog, deadline bool) { | |||||
| 797 | attachmentEpoch = c.attachmentEpoch | 799 | attachmentEpoch = c.attachmentEpoch | ||
| 798 | } | 800 | } | ||
| 799 | w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog, attachment: c.attachment, attachmentEpoch: attachmentEpoch} | 801 | w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog, attachment: c.attachment, attachmentEpoch: attachmentEpoch} | ||
| 800 | - | if !ready || (c.attachment != nil && !c.attachmentReady) { | 802 | + | // A detached target has no primary transport or ACK window. Its pending |
| 803 | + | // state belongs exclusively to picker previews and must be consumed after | |||
| 804 | + | // notification rather than accumulating ackDeferred forever. | |||
| 805 | + | headlessPreviewOnly := c.detached && c.attachment == nil | |||
| 806 | + | if !headlessPreviewOnly && (!ready || (c.attachment != nil && !c.attachmentReady)) { | |||
| 801 | if !ready && deadline { | 807 | if !ready && deadline { | ||
| 802 | c.ackDeferred = true | 808 | c.ackDeferred = true | ||
| 803 | } | 809 | } | ||
| @@ -824,7 +830,7 @@ func (c *renderCoordinator) fire(gen uint64, watchdog, deadline bool) { | |||||
| 824 | wake := c.opts.wake | 830 | wake := c.opts.wake | ||
| 825 | c.mu.Unlock() | 831 | c.mu.Unlock() | ||
| 826 | stopTimer(timer) | 832 | stopTimer(timer) | ||
| 827 | - | if wake != nil { | 833 | + | if wake != nil && !headlessPreviewOnly { |
| 828 | wake(w) | 834 | wake(w) | ||
| 829 | } | 835 | } | ||
| 830 | c.notifyPreviews(w, preview, previews) | 836 | c.notifyPreviews(w, preview, previews) | ||