Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions internal/session/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,29 @@ func (inst *Instance) GetLastActivityTime() time.Time {
return inst.CreatedAt
}

// DisplayLastActivityTime returns the timestamp to show as "last active" in
// the UI. It intentionally differs from GetLastActivityTime: that method
// returns the tmux tracker's raw lastChangeTime (which is seeded with
// time.Now() when the tracker is lazily created, so it leaks ~ the TUI load
// time for sessions that never confirm real activity — e.g. error/idle/
// stopped panes) and also feeds OpenCode rotation windows, so its semantics
// must not change.
//
// Here we consult only CONFIRMED activity (LastObservedActivity guards on
// realActivityConfirmed). When none has been observed we fall back to the
// persisted last-accessed time — matching what the web serves — and finally
// to CreatedAt. This keeps the TUI "⏱ last active" line in agreement with
// the web instead of resetting to the most recent TUI load.
func (inst *Instance) DisplayLastActivityTime() time.Time {
if ts, ok := inst.LastObservedActivity(); ok {
return ts
}
if !inst.LastAccessedAt.IsZero() {
return inst.LastAccessedAt
}
return inst.CreatedAt
}

// LastObservedActivity returns the last time the tmux tracker confirmed a
// real busy spike for this session, and a bool that is false when no
// confirmation has happened (the instance has no tmux session, or the
Expand Down
54 changes: 54 additions & 0 deletions internal/session/instance_display_activity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package session

import (
"testing"
"time"
)

// DisplayLastActivityTime is the UI-facing "last active" timestamp. Unlike
// GetLastActivityTime (which feeds OpenCode rotation windows and returns the
// tmux tracker's raw lastChangeTime), it must never leak the tracker's
// initialization timestamp (~ TUI load time) for a session that has no
// CONFIRMED activity. Error/idle/stopped sessions with a live-but-inactive
// tmux pane fall into exactly that case: the web shows the persisted
// last-accessed time, and the TUI must match it rather than jumping to the
// most recent TUI load. See the last-used regression.

func TestDisplayLastActivityTime_NoConfirmedActivity_UsesLastAccessed(t *testing.T) {
accessed := time.Now().Add(-24 * time.Hour)
created := time.Now().Add(-72 * time.Hour)
// nil tmuxSession => LastObservedActivity() reports (zero,false), i.e. no
// confirmed activity — the error-session case.
inst := &Instance{ID: "err-sess", CreatedAt: created, LastAccessedAt: accessed}

got := inst.DisplayLastActivityTime()
if !got.Equal(accessed) {
t.Errorf("with no confirmed activity, want LastAccessedAt %v, got %v", accessed, got)
}
// Must NOT report ~now (the load-time leak this fix targets).
if time.Since(got) < time.Hour {
t.Errorf("display time %v is suspiciously recent — the load-time leak is back", got)
}
}

func TestDisplayLastActivityTime_NoAccess_FallsBackToCreated(t *testing.T) {
created := time.Now().Add(-72 * time.Hour)
inst := &Instance{ID: "err-sess-2", CreatedAt: created} // LastAccessedAt zero

got := inst.DisplayLastActivityTime()
if !got.Equal(created) {
t.Errorf("with no access and no activity, want CreatedAt %v, got %v", created, got)
}
}

// The zero-data tail: very old rows persist last_accessed AND created_at as the
// Go zero sentinel (-62135596800 unix). Both fall through to a zero time, which
// the UI renders as "unknown" — an honest blank, never the ~load-time leak.
func TestDisplayLastActivityTime_AllZero_ReturnsZeroNotLoadTime(t *testing.T) {
inst := &Instance{ID: "err-sess-3"} // CreatedAt and LastAccessedAt both zero

got := inst.DisplayLastActivityTime()
if !got.IsZero() {
t.Errorf("with no access, activity, or creation time, want zero time, got %v", got)
}
}
7 changes: 5 additions & 2 deletions internal/ui/home.go
Original file line number Diff line number Diff line change
Expand Up @@ -15776,8 +15776,11 @@ func (h *Home) renderPreviewPane(width, height int) string {
b.WriteString(infoStyle.Render("📁 " + pathStr))
b.WriteString("\n")

// Activity time - shows when session was last active
activityTime := selected.GetLastActivityTime()
// Activity time - shows when session was last active. Uses the display-
// oriented accessor so sessions with no confirmed activity (error/idle/
// stopped) fall back to the persisted last-accessed time — matching the
// web — instead of leaking the tmux tracker's ~load-time seed.
activityTime := selected.DisplayLastActivityTime()
activityStr := formatRelativeTime(activityTime)
if selectedStatus == session.StatusRunning {
activityStr = "active now"
Expand Down
Loading