Skip to content

fix: honor inject_status_line=false on the watcher/reconnect path - #1780

Open
hfreire wants to merge 1 commit into
asheshgoplani:mainfrom
hfreire:up/fix-inject-status
Open

fix: honor inject_status_line=false on the watcher/reconnect path#1780
hfreire wants to merge 1 commit into
asheshgoplani:mainfrom
hfreire:up/fix-inject-status

Conversation

@hfreire

@hfreire hfreire commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What problem does this solve?

[tmux].inject_status_line = false doesn't actually remove the status bar. buildStatusBarArgs returns nil when injection is disabled, so ConfigureStatusBar becomes a no-op and agent-deck never emits status off. But tmux defaults every session to status on, so a bar — from that default, the user's own tmux config, or an earlier run that had injection enabled — survives untouched. Turning the option off stops us setting a bar but never removes one, so the opt-out silently does nothing on any session that already shows a bar (#687).

Why this change

When injection is disabled, emit set-option -t <session> status off instead of returning nil, so disabling the option actually turns the bar off. An explicit user status entry in [tmux].options still wins (returns nil, we don't override it), so a user who wants to manage the bar themselves can.

This supersedes my earlier approach on this PR (seeding a process-wide injectStatusLine default for the reconnect constructors). That was a no-op: all four production construction sites already call SetInjectStatusLine(GetTmuxSettings()...) explicitly right after building the session, and tmux.ReconnectSession has no non-test callers — so the constructor default never changed observable behavior. The real gap was the missing turn-off, not the constructor default.

User impact

inject_status_line = false now removes the agent-deck status bar on every session — fresh or reconnected — rather than silently leaving a pre-existing one on. No change for the default (inject_status_line = true). A user who sets status explicitly under [tmux].options keeps control of it.

Evidence

The bug is invisible to a unit test that asserts on the struct field or the argv it just built — which is exactly how #687 shipped (see tests/eval/README.md). So the guard is a real-tmux eval.

TestEval_Session_BarOff_RealTmux (tests/eval/session) writes inject_status_line = false, starts a session under a real tmux server (which defaults to status on), and asks tmux itself:

$ tmux display-message -p -t <session> '#{status}'
off

Revert-sensitive: with the tmux.go hunk reverted (back to return nil), the same eval fails because the default bar survives —

inject_status_off_test.go:86: inject_status_line=false did not turn the status bar off.
    session="agentdeck_inj_e76f8143"
    #{status}="on" (want "off")

Unit coverage of the arg builder both ways (internal/tmux): TestBuildStatusBarArgs_InjectDisabled asserts set-option ... status off is emitted; TestBuildStatusBarArgs_InjectDisabled_UserStatusOverride asserts an explicit user status override is left alone.

Sandboxed (production package):

$ HOME=$(mktemp -d) XDG_CONFIG_HOME= XDG_DATA_HOME= XDG_CACHE_HOME= go test ./internal/tmux/
ok  github.com/asheshgoplani/agent-deck/internal/tmux

The eval runs under its build tag, matching the existing TestEval_Session_InjectStatusLine_RealTmux:

$ go test -tags eval_smoke -run TestEval_Session_BarOff_RealTmux ./tests/eval/session/
ok  github.com/asheshgoplani/agent-deck/tests/eval/session

AI disclosure

  • AI-authored (a model wrote most of it)

Model(s), if AI helped: claude-opus-4-8

Prompt / session log (optional):

What actually bothered you

My human, running agent-deck with the status bar disabled, asked, verbatim: "there is a bottom ribbon (maybe from TMUX) that can be removed so it doesn't take place (both TUI and iOS)". They had set inject_status_line = false, but the bar kept showing because disabling the option never actually turned tmux's status off.

Checklist

  • Targeted diff: one problem, no unrelated changes
  • Tests added or updated for new behavior
  • Test suite passes sandboxed: HOME=$(mktemp -d) XDG_CONFIG_HOME= XDG_DATA_HOME= XDG_CACHE_HOME= go test ./...
  • If this touches a hot path (list, status, session output, startup, tmux layer): buildStatusBarArgs runs from ConfigureStatusBar on configure/reconnect, not per status poll — the change adds one set-option in the already-existing bounded batch, no measurable cost
  • CHANGELOG.md untouched (entries are added at landing)
  • AI-assisted? Disclosed above, with validation evidence, and I can answer questions about the code
  • "Allow edits from maintainers" is enabled

Summary by CodeRabbit

  • Bug Fixes

    • Disabling the injected status line now reliably turns off the tmux status bar.
    • Existing user-defined status settings are preserved when explicitly configured.
  • Tests

    • Added coverage verifying status bar removal and user override behavior in real tmux sessions.

@hfreire
hfreire requested a review from asheshgoplani as a code owner July 27, 2026 07:58
@github-actions github-actions Bot added intake:clean PR/issue passed the intake contract ai-authored Primarily authored by an AI agent labels Jul 27, 2026
@github-actions

Copy link
Copy Markdown

👋 Thanks for the contribution — intake looks complete.

Your PR body carries everything the maintainer's validation pipeline reads first: the problem, the reasoning, the human intent behind it, and an AI-disclosure. It will be applied, built, and tested against main, and you'll get a structured result within about a day. Merges are always human.

gate marker read: ai=authored model=claude-opus-4-8 intent=yes

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Disabled status-line injection now explicitly turns the tmux status bar off unless the user overrides status. Unit and real-tmux eval tests verify both the disable behavior and override handling.

Changes

Tmux status-bar disabling

Layer / File(s) Summary
Status-bar disable behavior
internal/tmux/tmux.go
buildStatusBarArgs() emits status off when injection is disabled, except when status is explicitly overridden.
Override and disabled-state unit coverage
internal/tmux/tmux_test.go
Unit tests verify forced status-bar disabling and preservation of user overrides.
Real tmux status validation
tests/eval/session/inject_status_off_test.go
An eval test starts a session with status injection disabled, queries tmux for the resolved status, and tears down the session.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: asheshgoplani

🚥 Pre-merge checks | ✅ 7
✅ Passed checks (7 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Remote_parity ✅ Passed PASS: The commit only changes internal/tmux and tests; no files under internal/ui/ or cmd/agent-deck/ were touched, so the RemoteSession parity check is not applicable.
Test_coverage_per_surface ✅ Passed The change is covered by tmux unit tests and a real-tmux eval via session start; no web/remote touchpoints were found, and the fix lives in the shared tmux path.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title uses Conventional Commits format and accurately reflects the fix for disabled inject_status_line behavior on reconnect-related paths.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/tmux/inject_status_default_test.go`:
- Around line 9-21: Extend TestReconnectSessionLazyHonorsInjectDefault to also
construct sessions through NewSession and ReconnectSession, asserting
injectStatusLine is false when the default is disabled and true when it is
enabled. Keep the existing default restoration and ReconnectSessionLazy coverage
intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b8d7347-e7ad-49e1-ae0c-d76b46e7f15b

📥 Commits

Reviewing files that changed from the base of the PR and between 0fc83cd and fd64bf7.

📒 Files selected for processing (4)
  • cmd/agent-deck/main.go
  • internal/tmux/inject_status_default_test.go
  • internal/tmux/socket.go
  • internal/tmux/tmux.go

Comment on lines +9 to +21
func TestReconnectSessionLazyHonorsInjectDefault(t *testing.T) {
orig := getDefaultInjectStatusLine()
defer SetDefaultInjectStatusLine(orig)

SetDefaultInjectStatusLine(false)
if s := ReconnectSessionLazy("no-such-session", "d", "/tmp", "", ""); s.injectStatusLine {
t.Fatalf("expected injectStatusLine=false when default is off")
}

SetDefaultInjectStatusLine(true)
if s := ReconnectSessionLazy("no-such-session", "d", "/tmp", "", ""); !s.injectStatusLine {
t.Fatalf("expected injectStatusLine=true when default is on")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cover all constructors changed by this PR.

This test only exercises ReconnectSessionLazy; a regression in NewSession or ReconnectSession would still pass. Add assertions for both constructors under false and true defaults.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tmux/inject_status_default_test.go` around lines 9 - 21, Extend
TestReconnectSessionLazyHonorsInjectDefault to also construct sessions through
NewSession and ReconnectSession, asserting injectStatusLine is false when the
default is disabled and true when it is enabled. Keep the existing default
restoration and ReconnectSessionLazy coverage intact.

@asheshgoplani

Copy link
Copy Markdown
Owner

Thank you for this one, and for the honest revert-limitation note in the body — but I want to push back on the premise before we land it, because I think merging as-is would close the ticket without actually killing your status bar.

On main today, the reconnect path already honors the config: internal/session/storage.go calls ReconnectSessionLazy at :1318 and then SetInjectStatusLine(GetTmuxSettings()...) 27 lines later at :1345, and the same pairing exists after NewSession at instance.go:783, :862 and :5545. tmux.ReconnectSession itself has zero non-test callers. So the constructor default this PR adds should produce no user-visible delta — which makes me think the bar you are seeing survives for a different reason.

My hypothesis for the real gap: buildStatusBarArgs (internal/tmux/tmux.go:2649) returns nil when inject_status_line is false, so agent-deck never emits set-option status off. Turning the option off therefore stops us from SETTING a bar, but never REMOVES one that an earlier run (or the user's own tmux config) already set server-side — so it persists forever on existing sessions. If that matches what you observe, the fix is to emit the off/restore explicitly rather than to change a default.

Three asks:

  1. The repro: config contents, tmux -L agent-deck show-options -g status before/after, and whether the ribbon persists on an EXISTING session versus a freshly created one. Your fix: force UTF-8 on the terminal-bridge tmux attach (-u) #1779 shipped raw before/after bytes, so I know you have the tooling — that level of evidence here would settle it immediately.
  2. Re-anchor the fix on the turn-off path (emit set-option -t <session> status off, or restore the user's prior value).
  3. Replace the unit test with a revert-sensitive eval beside TestEval_Session_InjectStatusLine_RealTmux (tests/eval/session/lifecycle_test.go:31) asserting display-message -p '#{status}' after a reconnect. Worth noting tests/eval/README.md:13 cites this exact option (Add ability to run on separate tmux server #687) as the reason that lane exists — a test that only asserts the field it just set cannot catch this class. If the eval passes without the process-wide global, drop the global too, since all four production sites already call SetInjectStatusLine explicitly.

#1779 merged, by the way — clean mechanism, clean evidence. Thanks for both.

buildStatusBarArgs returned nil when injection was disabled, so agent-deck
never emitted `status off`. Since tmux defaults every session to `status on`,
a bar from that default (or the user's config, or a prior enabled run) survived
untouched — the opt-out silently did nothing on any session that already showed
a bar (asheshgoplani#687). Emit `set-option -t <session> status off` instead, unless the
user set `status` explicitly in [tmux].options.

Guarded by a revert-sensitive real-tmux eval that asserts #{status}==off after
a session starts with injection disabled; a struct/argv unit test can't see
this, which is how asheshgoplani#687 shipped.
@hfreire
hfreire force-pushed the up/fix-inject-status branch from fd64bf7 to 66aa9f5 Compare July 27, 2026 09:52
@hfreire

hfreire commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

You're right, and thanks for the careful read — I pushed a rework that drops my original premise entirely.

I checked the four production construction sites and you're correct: they each call SetInjectStatusLine(GetTmuxSettings()...) right after building the session (instance.go:783,862,5545, storage.go:1383), and tmux.ReconnectSession has no non-test callers. So the constructor default I was seeding never changed observable behavior — it would have closed the ticket without touching the bar. I've dropped the process-wide global, the main.go seeding, and the constructor changes.

Your hypothesis was the actual bug: buildStatusBarArgs returns nil when injection is disabled, so ConfigureStatusBar no-ops and nothing ever emits status off. Since tmux defaults every session to status on, the bar survives regardless of the config. The fix re-anchors on the turn-off path — when injection is disabled it now emits set-option -t <session> status off, unless the user set status explicitly in [tmux].options (then we leave it alone).

On your ask for real evidence over argv assertions: the guard is now a revert-sensitive eval beside the existing one, asserting what tmux actually reports:

$ go test -tags eval_smoke -run TestEval_Session_BarOff_RealTmux ./tests/eval/session/
ok

# with the tmux.go hunk reverted to `return nil`:
inject_status_off_test.go: inject_status_line=false did not turn the status bar off.
    #{status}="on" (want "off")

It starts a session with inject_status_line=false under real tmux (which defaults to status on) and asserts display-message -p '#{status}' comes back off. Reverting the fix flips it back to on — a struct/argv unit test can't see that, which is how #687 got here. I also corrected the unit test that had been asserting the buggy nil return, and added one for the user-override path.

Diff is down to +127/−2 across internal/tmux/tmux.go, its unit test, and the new eval. The eval runs under -tags eval_smoke, same as the existing one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-authored Primarily authored by an AI agent intake:clean PR/issue passed the intake contract

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants