Skip to content

Commit d31b343

Browse files
andyrewleeclaude
andcommitted
Simplify amux: eliminate tmuxService wrapper, dedup, and reduce boilerplate
Remove the 154-line pure-passthrough tmuxService wrapper layer by changing App.tmuxService from *tmuxService to the TmuxOps interface directly. Loop-ify appendSessionTags, consolidate SetSessionTagValue as a delegate to SetSessionTagValues, unify runSendWait/runRunWait into runAgentWait, and unexport PanePIDs (only used within package). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6bac489 commit d31b343

19 files changed

Lines changed: 103 additions & 208 deletions

internal/app/activity/fetch.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ func FetchTaggedSessions(svc SessionFetcher, infoBySession map[string]SessionInf
5151
if !ok && !hasInput {
5252
// Lease is refreshed on both input and output events; treat it as a
5353
// compatibility fallback when explicit output tags are absent.
54-
// TODO: retire this fallback after all active sessions reliably write
55-
// explicit input/output tags.
54+
// Still needed (2026-03): CLI-created sessions lack output/input tags
55+
// until the first PTY activity event writes them.
5656
if leaseAt, leaseOK := ParseLastOutputAtTag(row.Tags[tmux.TagSessionLeaseAt]); leaseOK {
5757
lastOutputAt = leaseAt
5858
ok = true

internal/app/app_core.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type App struct {
4646
config *config.Config
4747
workspaceService *workspaceService
4848
gitStatus GitStatusService
49-
tmuxService *tmuxService
49+
tmuxService TmuxOps
5050
updateService UpdateService
5151

5252
// Limits

internal/app/app_init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func New(version, commit, date string) (*App, error) {
4848
statusManager := git.NewStatusManager(nil)
4949
gitStatus := newGitStatusService(statusManager)
5050

51-
tmuxSvc := newTmuxService(nil)
51+
var tmuxSvc TmuxOps = tmuxOps{}
5252
updateSvc := newUpdateService(version, commit, date)
5353

5454
// Create file watcher event channel

internal/app/app_tmux_activity.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func (a *App) runTmuxActivityScan(
117117
infoBySession map[string]activity.SessionInfo,
118118
statesSnapshot map[string]*activity.SessionState,
119119
opts tmux.Options,
120-
svc *tmuxService,
120+
svc TmuxOps,
121121
) tmuxActivityResult {
122122
if svc == nil {
123123
return tmuxActivityResult{Token: scanToken, Err: activity.ErrTmuxUnavailable}
@@ -232,7 +232,7 @@ func (a *App) runTmuxActivityScan(
232232
func (a *App) fetchAndSyncActivitySessionStates(
233233
infoBySession map[string]activity.SessionInfo,
234234
opts tmux.Options,
235-
svc *tmuxService,
235+
svc TmuxOps,
236236
) ([]activity.TaggedSession, []messages.TabSessionStatus, error) {
237237
sessions, err := activity.FetchTaggedSessions(svc, infoBySession, opts)
238238
if err != nil {
@@ -321,7 +321,7 @@ func (a *App) tabSessionInfoByName() map[string]activity.SessionInfo {
321321
func syncActivitySessionStates(
322322
infoBySession map[string]activity.SessionInfo,
323323
sessions []activity.TaggedSession,
324-
svc *tmuxService,
324+
svc TmuxOps,
325325
opts tmux.Options,
326326
) []messages.TabSessionStatus {
327327
stoppedTabs := make([]messages.TabSessionStatus, 0)

internal/app/app_tmux_activity_prefilter_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import (
1010

1111
func TestTmuxActivityScan_PrefilterErrorStillAllowsStaleFallback(t *testing.T) {
1212
app, wsID := newActivityTestAppWithScriptedTmux([]string{"new pane content"})
13-
ops, ok := app.tmuxService.ops.(*scriptedActivityTmuxOps)
13+
ops, ok := app.tmuxService.(*scriptedActivityTmuxOps)
1414
if !ok {
15-
t.Fatalf("expected scripted tmux ops, got %T", app.tmuxService.ops)
15+
t.Fatalf("expected scripted tmux ops, got %T", app.tmuxService)
1616
}
1717
ops.prefilterErr = errors.New("prefilter unavailable")
1818
ops.lastOutputAge = activity.OutputWindow + time.Second

internal/app/app_tmux_activity_shared_scan_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ func TestRunTmuxActivityScan_FollowerReconcilesStoppedTabsFromSharedSnapshot(t *
4343

4444
app := &App{
4545
instanceID: "shared-follower",
46-
tmuxService: newTmuxService(sessionsWithTagsStubTmuxOps{
46+
tmuxService: sessionsWithTagsStubTmuxOps{
4747
stubTmuxOps: stubTmuxOps{
4848
allStates: map[string]tmux.SessionState{},
4949
},
5050
rows: []tmux.SessionTagValues{{
5151
Name: "session-a",
5252
Tags: map[string]string{},
5353
}},
54-
}),
54+
},
5555
}
5656

5757
infoBySession := map[string]activity.SessionInfo{
@@ -92,9 +92,9 @@ func TestRunTmuxActivityScan_ScanErrorIncludesResolvedOwnerMetadata(t *testing.T
9292
scanErr := errors.New("fetch tagged sessions failed")
9393
app := &App{
9494
instanceID: "shared-owner",
95-
tmuxService: newTmuxService(sessionsWithTagsStubTmuxOps{
95+
tmuxService: sessionsWithTagsStubTmuxOps{
9696
err: scanErr,
97-
}),
97+
},
9898
}
9999

100100
result := app.runTmuxActivityScan(1, map[string]activity.SessionInfo{}, map[string]*activity.SessionState{}, opts, app.tmuxService)
@@ -126,7 +126,7 @@ func TestRunTmuxActivityScan_OwnerLeaseRevalidatedBeforePublish(t *testing.T) {
126126

127127
app := &App{
128128
instanceID: "owner-a",
129-
tmuxService: newTmuxService(sessionsWithTagsStubTmuxOps{
129+
tmuxService: sessionsWithTagsStubTmuxOps{
130130
stubTmuxOps: stubTmuxOps{
131131
allStates: map[string]tmux.SessionState{},
132132
},
@@ -138,7 +138,7 @@ func TestRunTmuxActivityScan_OwnerLeaseRevalidatedBeforePublish(t *testing.T) {
138138
t.Fatalf("write takeover snapshot: %v", err)
139139
}
140140
},
141-
}),
141+
},
142142
}
143143

144144
result := app.runTmuxActivityScan(42, map[string]activity.SessionInfo{}, map[string]*activity.SessionState{}, opts, app.tmuxService)
@@ -187,7 +187,7 @@ func TestRunTmuxActivityScan_LeaseRevalidationErrorBeforePublishSkipsApply(t *te
187187

188188
app := &App{
189189
instanceID: "owner-a",
190-
tmuxService: newTmuxService(sessionsWithTagsStubTmuxOps{
190+
tmuxService: sessionsWithTagsStubTmuxOps{
191191
stubTmuxOps: stubTmuxOps{
192192
allStates: map[string]tmux.SessionState{},
193193
},
@@ -196,7 +196,7 @@ func TestRunTmuxActivityScan_LeaseRevalidationErrorBeforePublishSkipsApply(t *te
196196
cmd := exec.Command("tmux", gcTmuxArgs(opts, "kill-server")...)
197197
_ = cmd.Run()
198198
},
199-
}),
199+
},
200200
}
201201

202202
result := app.runTmuxActivityScan(99, map[string]activity.SessionInfo{}, map[string]*activity.SessionState{}, opts, app.tmuxService)
@@ -221,12 +221,12 @@ func TestRunTmuxActivityScan_OwnerResolutionErrorLeavesRoleUnknown(t *testing.T)
221221

222222
app := &App{
223223
instanceID: "owner-a",
224-
tmuxService: newTmuxService(sessionsWithTagsStubTmuxOps{
224+
tmuxService: sessionsWithTagsStubTmuxOps{
225225
stubTmuxOps: stubTmuxOps{
226226
allStates: map[string]tmux.SessionState{},
227227
},
228228
rows: []tmux.SessionTagValues{},
229-
}),
229+
},
230230
}
231231

232232
result := app.runTmuxActivityScan(11, map[string]activity.SessionInfo{}, map[string]*activity.SessionState{}, opts, app.tmuxService)

internal/app/app_tmux_activity_test.go

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,11 @@ func TestSyncActivitySessionStates_NilSvc(t *testing.T) {
125125
}
126126

127127
func TestSyncActivitySessionStates_EmptyInfoBySession(t *testing.T) {
128-
svc := newTmuxService(stubTmuxOps{
128+
var svc TmuxOps = stubTmuxOps{
129129
allStates: map[string]tmux.SessionState{
130130
"s": {Exists: true, HasLivePane: true},
131131
},
132-
})
132+
}
133133
result := syncActivitySessionStates(
134134
map[string]activity.SessionInfo{},
135135
[]activity.TaggedSession{{Session: tmux.SessionActivity{Name: "s"}}},
@@ -142,9 +142,9 @@ func TestSyncActivitySessionStates_EmptyInfoBySession(t *testing.T) {
142142
}
143143

144144
func TestSyncActivitySessionStates_AllSessionStatesError(t *testing.T) {
145-
svc := newTmuxService(stubTmuxOps{
145+
var svc TmuxOps = stubTmuxOps{
146146
allStatesErr: errors.New("tmux failed"),
147-
})
147+
}
148148
info := map[string]activity.SessionInfo{
149149
"s": {Status: "running", WorkspaceID: "ws1"},
150150
}
@@ -164,11 +164,11 @@ func TestSyncActivitySessionStates_AllSessionStatesError(t *testing.T) {
164164
}
165165

166166
func TestSyncActivitySessionStates_RunningSessionDeadPane(t *testing.T) {
167-
svc := newTmuxService(stubTmuxOps{
167+
var svc TmuxOps = stubTmuxOps{
168168
allStates: map[string]tmux.SessionState{
169169
"s": {Exists: true, HasLivePane: false},
170170
},
171-
})
171+
}
172172
info := map[string]activity.SessionInfo{
173173
"s": {Status: "running", WorkspaceID: "ws1"},
174174
}
@@ -191,9 +191,9 @@ func TestSyncActivitySessionStates_RunningSessionDeadPane(t *testing.T) {
191191

192192
func TestSyncActivitySessionStates_RunningSessionDisappeared(t *testing.T) {
193193
// Session appears in tagged list but not in AllSessionStates (disappeared).
194-
svc := newTmuxService(stubTmuxOps{
194+
var svc TmuxOps = stubTmuxOps{
195195
allStates: map[string]tmux.SessionState{}, // empty: session gone
196-
})
196+
}
197197
info := map[string]activity.SessionInfo{
198198
"s": {Status: "running", WorkspaceID: "ws1"},
199199
}
@@ -215,11 +215,11 @@ func TestSyncActivitySessionStates_RunningSessionDisappeared(t *testing.T) {
215215
}
216216

217217
func TestSyncActivitySessionStates_StoppedSessionRevived(t *testing.T) {
218-
svc := newTmuxService(stubTmuxOps{
218+
var svc TmuxOps = stubTmuxOps{
219219
allStates: map[string]tmux.SessionState{
220220
"s": {Exists: true, HasLivePane: true},
221221
},
222-
})
222+
}
223223
info := map[string]activity.SessionInfo{
224224
"s": {Status: "stopped", WorkspaceID: "ws1"},
225225
}
@@ -239,9 +239,9 @@ func TestSyncActivitySessionStates_StoppedSessionRevived(t *testing.T) {
239239

240240
func TestSyncActivitySessionStates_AlreadyStoppedDisappeared(t *testing.T) {
241241
// A session already marked stopped that also disappeared should not emit a duplicate.
242-
svc := newTmuxService(stubTmuxOps{
242+
var svc TmuxOps = stubTmuxOps{
243243
allStates: map[string]tmux.SessionState{},
244-
})
244+
}
245245
info := map[string]activity.SessionInfo{
246246
"s": {Status: "stopped", WorkspaceID: "ws1"},
247247
}
@@ -258,11 +258,11 @@ func TestSyncActivitySessionStates_AlreadyStoppedDisappeared(t *testing.T) {
258258

259259
func TestSyncActivitySessionStates_TaggedNotInInfo(t *testing.T) {
260260
// Session in tagged list but not in infoBySession should be skipped.
261-
svc := newTmuxService(stubTmuxOps{
261+
var svc TmuxOps = stubTmuxOps{
262262
allStates: map[string]tmux.SessionState{
263263
"unknown": {Exists: true, HasLivePane: false},
264264
},
265-
})
265+
}
266266
info := map[string]activity.SessionInfo{}
267267
result := syncActivitySessionStates(
268268
info,
@@ -277,9 +277,9 @@ func TestSyncActivitySessionStates_TaggedNotInInfo(t *testing.T) {
277277

278278
func TestSyncActivitySessionStates_InfoNotInTaggedRunning(t *testing.T) {
279279
// Session in infoBySession but not in tagged list, with running status → emits stopped (second loop).
280-
svc := newTmuxService(stubTmuxOps{
280+
var svc TmuxOps = stubTmuxOps{
281281
allStates: map[string]tmux.SessionState{},
282-
})
282+
}
283283
info := map[string]activity.SessionInfo{
284284
"orphan": {Status: "running", WorkspaceID: "ws1"},
285285
}
@@ -423,7 +423,7 @@ func newActivityTestAppWithScriptedTmux(contentByScan []string) (*App, string) {
423423
},
424424
},
425425
projects: []data.Project{project},
426-
tmuxService: newTmuxService(tmuxOps),
426+
tmuxService: tmuxOps,
427427
tmuxOptions: tmux.Options{},
428428
tmuxAvailable: true,
429429
dashboard: dashboard.New(),

internal/app/app_tmux_gc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func (a *App) gcStaleDetachedAgentSessions() tea.Cmd {
107107
// Bulk client listing is an optional fast path on the default tmux ops.
108108
// Keep a per-session fallback for stubs/custom ops that only expose
109109
// SessionHasClients so detached-session GC remains correct everywhere.
110-
if lister, ok := svc.ops.(sessionClientsLister); ok {
110+
if lister, ok := svc.(sessionClientsLister); ok {
111111
clientNames, clientsErr := lister.SessionNamesWithClients(opts)
112112
if clientsErr != nil {
113113
logging.Warn("detached agent GC: failed to list attached clients in bulk: %v", clientsErr)

internal/app/app_tmux_gc_detached_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func TestGcStaleDetachedAgentSessions_RunsWhenFollower(t *testing.T) {
7878
instanceID: "instance-a",
7979
tmuxActivityOwnershipSet: true,
8080
tmuxActivityScannerOwner: false,
81-
tmuxService: newTmuxService(ops),
81+
tmuxService: ops,
8282
}
8383
cmd := app.gcStaleDetachedAgentSessions()
8484
if cmd == nil {
@@ -111,7 +111,7 @@ func TestGcStaleDetachedAgentSessions_FiltersByInstanceID(t *testing.T) {
111111
app := &App{
112112
tmuxAvailable: true,
113113
instanceID: "instance-a",
114-
tmuxService: newTmuxService(ops),
114+
tmuxService: ops,
115115
}
116116
msg := app.gcStaleDetachedAgentSessions()()
117117
result, ok := msg.(staleDetachedAgentGCResult)
@@ -165,7 +165,7 @@ func TestGcStaleDetachedAgentSessions_KillsStaleDetachedNoLivePane(t *testing.T)
165165

166166
app := &App{
167167
tmuxAvailable: true,
168-
tmuxService: newTmuxService(ops),
168+
tmuxService: ops,
169169
}
170170
cmd := app.gcStaleDetachedAgentSessions()
171171
if cmd == nil {
@@ -210,7 +210,7 @@ func TestGcStaleDetachedAgentSessions_RespectsLivePaneThreshold(t *testing.T) {
210210

211211
app := &App{
212212
tmuxAvailable: true,
213-
tmuxService: newTmuxService(ops),
213+
tmuxService: ops,
214214
}
215215
msg := app.gcStaleDetachedAgentSessions()()
216216
result, ok := msg.(staleDetachedAgentGCResult)
@@ -257,7 +257,7 @@ func TestGcStaleDetachedAgentSessions_SkipsFreshAndAttached(t *testing.T) {
257257

258258
app := &App{
259259
tmuxAvailable: true,
260-
tmuxService: newTmuxService(ops),
260+
tmuxService: ops,
261261
}
262262
msg := app.gcStaleDetachedAgentSessions()()
263263
result, ok := msg.(staleDetachedAgentGCResult)
@@ -309,7 +309,7 @@ func TestGcStaleDetachedAgentSessions_UsesBulkClientListWhenAvailable(t *testing
309309

310310
app := &App{
311311
tmuxAvailable: true,
312-
tmuxService: newTmuxService(ops),
312+
tmuxService: ops,
313313
}
314314
msg := app.gcStaleDetachedAgentSessions()()
315315
result, ok := msg.(staleDetachedAgentGCResult)
@@ -361,7 +361,7 @@ func TestGcStaleDetachedAgentSessions_UsesSessionActivityFallback(t *testing.T)
361361

362362
app := &App{
363363
tmuxAvailable: true,
364-
tmuxService: newTmuxService(ops),
364+
tmuxService: ops,
365365
}
366366
msg := app.gcStaleDetachedAgentSessions()()
367367
result, ok := msg.(staleDetachedAgentGCResult)

internal/app/app_tmux_gc_safety_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func newGCTestApp(ops *gcOrphanOps) *App {
4848
return &App{
4949
tmuxAvailable: true,
5050
projectsLoaded: true,
51-
tmuxService: newTmuxService(ops),
51+
tmuxService: ops,
5252
}
5353
}
5454

0 commit comments

Comments
 (0)