Skip to content

Commit 2aa4192

Browse files
committed
fix: discover custom tmux sockets and fallback to alt-screen capture
1 parent cad02fa commit 2aa4192

4 files changed

Lines changed: 150 additions & 33 deletions

File tree

docs/changelog/260222.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,28 @@ Made Lisa socket discovery resilient to older `lisa` binaries that reject `--wit
4545
- Verify tmux-visualiser no longer shows `unknown flag: --with-next-action` in discovery errors.
4646
- Verify Lisa sessions still appear when using a newer Lisa build that supports `--with-next-action`.
4747
- Verify fallback path still returns zero errors when `lisa session list --all-sockets --json` succeeds but has no `items`.
48+
49+
## 260222-22:44:19 - Improve Lisa socket coverage and alt-screen capture fallback
50+
51+
### Summary
52+
Made tmux-visualiser reliably detect Lisa sessions across custom sockets and show pane output that lives on alternate screen buffers.
53+
54+
### Changed
55+
- Changed Lisa discovery payload parsing to consume `items[].socketPath` when provided, with project-root fallback for older payloads.
56+
- Changed process-table socket discovery to include all active `tmux -S` sockets rather than filtering to `lisa-*` names.
57+
58+
### Fixed
59+
- Fixed missed Lisa sessions when runtime socket naming/location does not match canonical `lisa-tmux-*` patterns.
60+
- Fixed blank captures for sessions that render primarily in alternate screen buffers by falling back to `capture-pane -a`.
61+
62+
### Files
63+
- `src/sockets.go`
64+
- `src/state.go`
65+
- `src/socket_test.go`
66+
- `docs/changelog/260222.md`
67+
68+
### QA Notes
69+
- Verify sessions on custom socket names still appear in the visualiser.
70+
- Verify sessions discovered via `lisa session list --all-sockets --with-next-action --json` use explicit `socketPath` when present.
71+
- Verify panes that previously looked empty now render via alternate-screen fallback.
72+
- Verify regression suite: `go test ./src`.

src/socket_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,47 @@ func TestListLisaSocketPathsFromLISAOldPayloadWithoutItemsReturnsEmpty(t *testin
441441
}
442442
}
443443

444+
func TestListLisaSocketPathsFromLISAUsesSocketPathFieldWhenPresent(t *testing.T) {
445+
cfg := config{cmdTimeout: 2 * time.Second}
446+
stubLisaSessionList(t, func(_ context.Context, _ bool) ([]byte, error) {
447+
return []byte(`{"items":[{"projectRoot":"/tmp/proj-a","socketPath":"/tmp/custom-a.sock"},{"projectRoot":"/tmp/proj-b"}]}`), nil
448+
})
449+
got, err := listLisaSocketPathsFromLISA(cfg)
450+
if err != nil {
451+
t.Fatalf("listLisaSocketPathsFromLISA err: %v", err)
452+
}
453+
rootB := canonicalProjectRoot("/tmp/proj-b")
454+
want := []string{
455+
"/tmp/custom-a.sock",
456+
tmuxSocketPathForProjectRoot(rootB),
457+
tmuxLegacySocketPathForProjectRoot(rootB),
458+
}
459+
if !reflect.DeepEqual(got, want) {
460+
t.Fatalf("got = %v, want %v", got, want)
461+
}
462+
}
463+
464+
func TestListLisaSocketPathsFromProcessTableIncludesNonLisaNamedSockets(t *testing.T) {
465+
origList := listProcessCommandsFn
466+
t.Cleanup(func() {
467+
listProcessCommandsFn = origList
468+
})
469+
listProcessCommandsFn = func() ([]string, error) {
470+
return []string{
471+
"tmux -S /tmp/custom.sock list-sessions",
472+
"tmux -S /tmp/lisa-a.sock list-sessions",
473+
}, nil
474+
}
475+
got, err := listLisaSocketPathsFromProcessTable()
476+
if err != nil {
477+
t.Fatalf("listLisaSocketPathsFromProcessTable err: %v", err)
478+
}
479+
want := []string{"/tmp/custom.sock", "/tmp/lisa-a.sock"}
480+
if !reflect.DeepEqual(got, want) {
481+
t.Fatalf("got = %v, want %v", got, want)
482+
}
483+
}
484+
444485
func TestExtractTmuxSocketPathsFromCommands(t *testing.T) {
445486
commands := []string{
446487
"/opt/homebrew/bin/tmux -S /tmp/lisa-a.sock new -d",
@@ -608,6 +649,40 @@ func TestPaneQualifiedKey(t *testing.T) {
608649
}
609650
}
610651

652+
func TestCapturePaneFallsBackToAlternateScreen(t *testing.T) {
653+
origRun := runTmuxOnSocketFn
654+
t.Cleanup(func() {
655+
runTmuxOnSocketFn = origRun
656+
})
657+
658+
calls := make([]string, 0, 2)
659+
runTmuxOnSocketFn = func(_ context.Context, _ config, socket string, args ...string) (string, error) {
660+
calls = append(calls, socket+"|"+strings.Join(args, " "))
661+
if len(args) == 0 || args[0] != "capture-pane" {
662+
return "", errors.New("unexpected command")
663+
}
664+
if len(args) > 1 && args[1] == "-a" {
665+
return "alt-line-1\nalt-line-2\n", nil
666+
}
667+
return "\n", nil
668+
}
669+
670+
lines, err := capturePane(context.Background(), config{}, "/tmp/test.sock", "%1", 80)
671+
if err != nil {
672+
t.Fatalf("capturePane err: %v", err)
673+
}
674+
want := []string{"alt-line-1", "alt-line-2"}
675+
if !reflect.DeepEqual(lines, want) {
676+
t.Fatalf("lines = %v, want %v", lines, want)
677+
}
678+
if len(calls) != 2 {
679+
t.Fatalf("calls len = %d", len(calls))
680+
}
681+
if !strings.Contains(calls[1], "capture-pane -a -t %1 -p -e -S -80") {
682+
t.Fatalf("expected alternate-screen capture fallback, got %q", calls[1])
683+
}
684+
}
685+
611686
func TestListSessionsOnSocketAllPanes(t *testing.T) {
612687
socketPath := "/tmp/test-all-panes.sock"
613688

src/sockets.go

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import (
77
"encoding/json"
88
"errors"
99
"fmt"
10-
"os/exec"
1110
"os"
11+
"os/exec"
1212
"path/filepath"
1313
"sort"
1414
"strings"
@@ -193,14 +193,7 @@ func listLisaSocketPathsFromProcessTable() ([]string, error) {
193193
if err != nil {
194194
return nil, err
195195
}
196-
paths := extractTmuxSocketPathsFromCommands(commands)
197-
out := make([]string, 0, len(paths))
198-
for _, path := range paths {
199-
if isLikelyLisaSocketPath(path) {
200-
out = append(out, path)
201-
}
202-
}
203-
return out, nil
196+
return extractTmuxSocketPathsFromCommands(commands), nil
204197
}
205198

206199
func listProcessCommands() ([]string, error) {
@@ -274,19 +267,11 @@ func listLisaSocketPathsFromLISA(cfg config) ([]string, error) {
274267
return nil, fmt.Errorf("lisa list failed: %s", strings.TrimSpace(string(out)))
275268
}
276269

277-
projectRoots, err := lisaProjectRootsFromListPayload(out)
270+
socketPaths, err := lisaSocketPathsFromListPayload(out)
278271
if err != nil {
279272
return nil, fmt.Errorf("lisa list invalid json")
280273
}
281-
paths := make([]string, 0, len(projectRoots)*2)
282-
for _, root := range projectRoots {
283-
paths = append(paths, tmuxSocketPathForProjectRoot(root))
284-
legacy := tmuxLegacySocketPathForProjectRoot(root)
285-
if legacy != "" && legacy != paths[len(paths)-1] {
286-
paths = append(paths, legacy)
287-
}
288-
}
289-
return dedupePaths(paths), nil
274+
return socketPaths, nil
290275
}
291276

292277
func runLisaSessionList(ctx context.Context, withNextAction bool) ([]byte, error) {
@@ -317,24 +302,34 @@ func lisaUnknownWithNextActionError(out []byte) bool {
317302
return strings.Contains(lower, "unknown flag") && strings.Contains(lower, "--with-next-action")
318303
}
319304

320-
func lisaProjectRootsFromListPayload(out []byte) ([]string, error) {
305+
func lisaSocketPathsFromListPayload(out []byte) ([]string, error) {
321306
var payload struct {
322307
Items []struct {
323308
ProjectRoot string `json:"projectRoot"`
309+
SocketPath string `json:"socketPath"`
324310
} `json:"items"`
325311
}
326312
if err := json.Unmarshal(out, &payload); err != nil {
327313
return nil, err
328314
}
329-
roots := make([]string, 0, len(payload.Items))
315+
paths := make([]string, 0, len(payload.Items)*2)
330316
for _, item := range payload.Items {
317+
socketPath := strings.TrimSpace(item.SocketPath)
318+
if socketPath != "" {
319+
paths = append(paths, filepath.Clean(socketPath))
320+
continue
321+
}
331322
root := canonicalProjectRoot(item.ProjectRoot)
332323
if root == "" {
333324
continue
334325
}
335-
roots = append(roots, root)
326+
paths = append(paths, tmuxSocketPathForProjectRoot(root))
327+
legacy := tmuxLegacySocketPathForProjectRoot(root)
328+
if legacy != "" {
329+
paths = append(paths, legacy)
330+
}
336331
}
337-
return dedupePaths(roots), nil
332+
return dedupePaths(paths), nil
338333
}
339334

340335
func canonicalProjectRoot(projectRoot string) string {
@@ -408,14 +403,6 @@ func preferredTmuxSocketDir() string {
408403
return filepath.Clean(tmp)
409404
}
410405

411-
func isLikelyLisaSocketPath(path string) bool {
412-
base := strings.ToLower(filepath.Base(strings.TrimSpace(path)))
413-
if base == "lisa-codex-nosb.sock" {
414-
return true
415-
}
416-
return strings.HasPrefix(base, "lisa-") && strings.HasSuffix(base, ".sock")
417-
}
418-
419406
func makeSocketTarget(path string) socketTarget {
420407
return socketTarget{
421408
path: path,

src/state.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,12 +301,42 @@ func capturePane(ctx context.Context, cfg config, socketPath string, paneID stri
301301
if err != nil {
302302
return nil, err
303303
}
304-
if out == "" {
304+
305+
primary := normalizeCaptureOutput(out)
306+
if hasVisibleCapture(primary) {
307+
return primary, nil
308+
}
309+
310+
altOut, altErr := runTmuxOnSocketFn(ctx, cfg, socketPath, "capture-pane", "-a", "-t", paneID, "-p", "-e", "-S", rangeArg)
311+
if altErr == nil {
312+
alt := normalizeCaptureOutput(altOut)
313+
if len(alt) > 0 {
314+
return alt, nil
315+
}
316+
}
317+
318+
if len(primary) == 0 {
305319
return []string{"(empty)"}, nil
306320
}
321+
return primary, nil
322+
}
323+
324+
func normalizeCaptureOutput(out string) []string {
325+
if out == "" {
326+
return []string{}
327+
}
307328
result := strings.Split(out, "\n")
308329
if len(result) > 0 && result[len(result)-1] == "" {
309330
result = result[:len(result)-1]
310331
}
311-
return result, nil
332+
return result
333+
}
334+
335+
func hasVisibleCapture(lines []string) bool {
336+
for _, line := range lines {
337+
if strings.TrimSpace(line) != "" {
338+
return true
339+
}
340+
}
341+
return false
312342
}

0 commit comments

Comments
 (0)