powered by
etapx

0%

(
July 21, 2026
)

Notifications Shouldn't Need a Screen: Keeping Agent Alerts Alive in Unmounted Tabs

How Influxx redesigned hidden-tab memory savings after a same-day revert taught us that unmounting a terminal view silently kills agent-completion notifications too.
Notifications Shouldn't Need a Screen: Keeping Agent Alerts Alive in Unmounted Tabs
Notifications Shouldn't Need a Screen: Keeping Agent Alerts Alive in Unmounted Tabs
How Influxx redesigned hidden-tab memory savings after a same-day revert taught us that unmounting a terminal view silently kills agent-completion notifications too.

A hidden terminal tab should be free — no rendering cost, no wasted memory holding pixels nobody is looking at. That premise shipped, then got reverted the same day, because the code that freed the memory was also the only code watching for bell sounds, title changes, and the moment a background agent finished its work. A tab you couldn't see became a tab that couldn't tell you anything either.

The Problem: Freeing a Tab's View Also Freed Its Attention

Influxx runs a lot of terminals at once. Each worktree gets its own agent, each agent gets its own pane, and a working session can easily have a dozen or more tabs open across projects. Most of those tabs are not on screen at any given moment — they're backgrounded while you focus on one pane, waiting for their agent to either need input or finish a task. Keeping every one of those tabs fully mounted, with a live terminal view rendering rows and columns of output that nobody is reading, is wasted work. The obvious fix is to unmount the view for any tab that isn't currently visible and remount it on demand.

That's exactly what the first version of this feature did. It worked, in the narrow sense that memory usage for hidden tabs dropped. It also broke the one thing users actually rely on backgrounded agents for: knowing when they're done. The terminal view component wasn't just responsible for drawing characters to the screen. It also owned the byte parser that watched the raw PTY stream for a bell character, for escape sequences that update the terminal title, and for the patterns that told Influxx an agent had gone idle after finishing a task. Unmount the view, and all of that went with it — silently, with no error, because from the runtime's perspective a component being torn down is completely normal.

The practical effect: a user could kick off a long agent run in a background tab, alt-tab away, come back an hour later, and find the tab still showing its old title and no unread indicator, even though the agent had finished within the first ten minutes. Nothing crashed. Nothing logged an error. The feature simply stopped noticing things the moment you stopped looking at them — which is precisely backwards for a tool whose entire value proposition is letting you not watch a terminal continuously. The fix went in and out within the same day.

"The bug was almost elegant in how invisible it was. We didn't lose data, we didn't throw exceptions, we just stopped listening. Nobody writes a test called 'does the app still care about this tab' — you only find that gap when a real background agent finishes and nothing happens."

— Priya Ramaswamy, Staff Engineer, Terminal Platform at ETAPX

The Fix: Separate What Watches from What Renders

The redesign starts from a different premise: notification-worthy events should never have been coupled to the rendered view in the first place. A terminal tab has two jobs that only look like one job. It needs to paint characters on screen when it's visible, and it needs to watch the byte stream for things worth telling you about, whether it's visible or not. The first version conflated these because it was convenient — the view component was already sitting on the byte stream, so it was the easiest place to also parse for bells and titles. The redesign pulls that logic out into something that doesn't care whether a view exists at all.

The Pane-Less Watcher

The core piece of the fix is what the team calls a pane-less watcher: a background subscriber that keeps notification-relevant side effects alive for a tab even when there's no mounted terminal view backing it. When a tab gets hidden and its rendered pane is torn down to save memory, the pane-less watcher takes over. It drives the exact same notification logic a visible pane would use — same bell handling, same title-change detection, same agent-completion state machine — just without anything on screen consuming the bytes for painting. Nothing about the notification path changes based on visibility. Only the rendering path does.

Parse Once, Centrally, Into Typed Facts

Making that possible required moving parsing out of the view layer entirely. In the shipped design, every byte coming from a terminal — whether it originates locally, from the background daemon that keeps PTYs alive across app restarts, or from an SSH-remote session — is parsed exactly once, in one central place, and turned into typed facts: title changes, bell events, agent-working, agent-idle, agent-exited, command-finished, and pull-request-link-detected. Views and watchers alike don't re-parse raw terminal output; they just consume whichever of these facts they care about. A visible pane consumes them to update what's drawn. A hidden tab's pane-less watcher consumes the same facts to update unread state, tab titles, and OS notifications. Neither one holds the only copy of the parsing logic, so neither one can silently lose it by being unmounted.

"Once parsing happens in exactly one place and produces facts instead of side effects, the rendering layer becomes optional. You can throw the view away and reconstruct it later from cached state, and the facts pipeline never noticed it was gone. That's the property the first version didn't have."

— Priya Ramaswamy, Staff Engineer, Terminal Platform at ETAPX

Notification Policy: What Actually Fires and When

Decoupling the parser from the view solved the "the tab went silent" problem, but it also gave the team a clean place to formalize notification behavior that had previously been implicit in whatever the view happened to do. The policy that survived the redesign:

  • A bell always marks unread. Even if the tab is currently focused on screen, a bell character sets the worktree/tab to unread. Focus doesn't suppress it — the assumption is that a bell means "something happened," and losing that signal because you happened to be looking elsewhere in a split view isn't acceptable.
  • OS notifications are deliberately delayed. Rather than firing the instant a candidate event is detected, the app holds briefly and yields to a pending agent-completion notification if one is about to fire for the same tab. This avoids sending a generic "something happened" push right before the more useful "your agent finished" push arrives a beat later.
  • Agent completion uses a grace period plus a maximum wait. Agents don't always go idle cleanly — there can be a short pause mid-task that looks like completion but isn't. The detector waits a short grace period to avoid false positives, but caps that wait with a maximum so a genuinely finished agent doesn't sit unreported indefinitely.
  • A per-worktree cooldown gates every OS notification. As a final check before anything actually reaches the OS notification center, a cooldown of a few seconds per worktree has to have elapsed since the last one. A chatty agent that bells repeatedly in a tight loop doesn't turn into a burst of desktop notifications.

Why Side Effects Run Ahead of Pixels

One of the more counterintuitive decisions in the redesign is that notification state is explicitly not synchronized with what's visibly painted on screen. A completion-title update can reach the app's state before the visible terminal has even finished painting its final lines of output. That sounds like a race condition, but it's deliberate: attention and title state are treated as out-of-band signals that must keep advancing on their own schedule, separate from the byte-delivery pipeline that feeds the visible view.

This matters most under backpressure. If a hidden or slow-to-render view is intentionally paused — because nobody's watching it, or because it can't keep up with a firehose of output — that pause should never be allowed to stall the facts pipeline that decides whether your agent is done. The old, coupled design would have made that stall happen by construction, because the parser lived inside the thing being paused. In the current design, the parser and the pane-less watcher sit upstream of rendering, so a backpressured or hidden view can lag as much as it needs to while title updates, unread flags, and completion detection keep moving in real time.

"I run four or five agents in parallel across different repos and I basically never watch the terminals directly anymore — I just wait for the tab to tell me it's done. Before, I caught myself manually clicking through backgrounded tabs every ten minutes just to check, which defeats the entire point of running things in the background."

— Marcus Feld, platform engineer at a logistics software company, Influxx user

Testing a Feature That's Defined by What Doesn't Happen

The hardest part of validating this kind of fix is that the failure mode is an absence — nothing crashes, nothing logs, a notification simply never fires. That's not the kind of bug a smoke test catches by accident. The feature is now covered by a dedicated end-to-end suite that specifically targets terminal attention state, agent-completion notifications, and the hidden-view-parking behavior together, rather than testing memory savings and notifications as separate concerns that happen to touch the same code.

Part of that suite is a parked-tab/focused-pane-suppression test matrix: a set of cases that cross tab visibility state (parked and unmounted vs. mounted and focused) against notification triggers (bell, title change, agent completion) to confirm the right thing happens in every combination — including confirming that a currently-focused pane correctly suppresses redundant OS notifications for events the user is already looking at, while a parked tab still fires them. That matrix is what actually catches a regression like the original one: it would have failed a parked-tab test case the moment the view's teardown started silently taking the parser down with it.

"We shipped the memory win and reverted it in the same day, and I think that's the right outcome to be proud of, not embarrassed by. A background agent that finishes and never tells you is a worse product than one that uses a bit more memory. Getting that trade-off backwards for even a few hours was worth catching immediately rather than living with it."

— Elena Vasquez, VP of Engineering at ETAPX

Frequently Asked Questions

Does hiding a tab still save memory the way the original feature intended?

Yes — the memory-saving behavior of unmounting a hidden tab's rendered terminal view is preserved. The redesign didn't roll that part back; it added the pane-less watcher so unmounting no longer costs you notifications.

Will I get a notification for every single bell character an agent emits?

No. Bells always mark a tab unread, but actual OS-level notifications are subject to a grace period for agent-completion detection and a per-worktree cooldown of a few seconds, specifically to prevent a chatty or looping agent from spamming your notification center.

Does this behave the same way for daemon-backed and SSH-remote terminals, not just local ones?

Yes. Byte parsing into typed facts happens centrally regardless of whether the terminal is local, running through the background daemon that survives app restarts, or connected over SSH to a remote host — the pane-less watcher and notification policy operate on those facts, not on the transport.

If a tab is focused and visible, will I get redundant notifications for things I can already see happening?

No — the test matrix specifically covers focused-pane suppression, so a currently-focused tab does not fire the same OS-level notification a parked, hidden tab would fire for the same event, since you're presumably already watching it.

What happens to the tab's title if I never bring it back into focus?

It still updates. Title changes are driven by the same typed facts pipeline as notifications, independent of whether a view is mounted, so a hidden tab's title reflects agent state changes even if you never look at it again until the notification arrives.

Could the completion notification fire before the terminal has actually finished displaying output?

It's possible for the completion-title update to reach app state slightly before a visible pane finishes painting its last output, because attention state is intentionally treated as out-of-band from rendering — this is a deliberate design choice, not a bug, to keep notification timing decoupled from render performance or backpressure.

How was this actually caught and fixed so quickly?

The original coupling was caught the same day it shipped, and the fix — separating parsing into a single central pipeline of typed facts that both views and a new pane-less watcher can consume — is now locked in by a dedicated end-to-end test suite covering attention state, completion notifications, and hidden-tab parking together.

The real lesson here wasn't about memory or about notifications specifically — it was that "does the user still get told" is a property that has to be designed for explicitly, because nothing about a rendering optimization will remind you it's also deleting your alerting logic on the way out.