Skip to content

feat(log): add a rotating log file and show its path in the TUI - #14

Merged
hoaitan merged 2 commits into
hoaitan:mainfrom
nwebbot:feat/rotating-log-file
Aug 8, 2026
Merged

feat(log): add a rotating log file and show its path in the TUI#14
hoaitan merged 2 commits into
hoaitan:mainfrom
nwebbot:feat/rotating-log-file

Conversation

@nwebbot

@nwebbot nwebbot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Why

LogBuffer keeps the process log stream in memory for the TUI panel, but nothing reaches disk. Once the TUI exits the run leaves no record — there is nothing to go back to when debugging a reconnect, a bootstrap, or a crash that happened twenty minutes ago.

Downstream, retask-cli wants exactly this for retask sandbox connect: keep the TUI panel as-is, and additionally append every line to retask.log in the working folder, with the active path visible on the Logs divider.

What

LogFileConfig + OpenLogFile (new logfile.go)

The library still never installs a logger of its own. OpenLogFile hands back an io.Writer the caller tees into whatever handler already feeds the TUI:

cfg := agentfleet.DefaultConfig()
cfg.LogFile.Path = "retask.log"

logFile, err := agentfleet.OpenLogFile(cfg.LogFile)
defer logFile.Close()

logBuf := agentfleet.NewLogBuffer(500)
cfg.TUI.Log     = logBuf
cfg.TUI.LogPath = logFile.Path()

logger := slog.New(slog.NewTextHandler(io.MultiWriter(logBuf, logFile), nil))
Setting Default Meaning
LogFile.Enabled true Write the log stream to a file.
LogFile.Path <cwd>/agentfleet.log Live log file; relative paths resolve against the working directory.
LogFile.MaxBytes 10MB Rotate once the live file exceeds this. 0 disables rotation.
LogFile.Backups 5 Rotated generations kept. 0 truncates instead.
TUI.ShowLogPath true Render TUI.LogPath on the log panel divider.

Rotation follows the Unix convention — the live file keeps its name, older generations shift down through <path>.1, <path>.2, … up to Backups, and the oldest is discarded. A single Write is never split across two generations, so a log line always lands whole in one file. Reopening an existing file appends rather than truncating.

*LogFile is nil-safe on every method and OpenLogFile returns nil, nil when disabled, so turning the file off needs no branching at the call site.

TUI divider

TUIConfig.LogPath / ShowLogPath render the path on the Logs divider:

─ Logs (/Users/bot/retask-sessions/nweb/session-18dcdfe8/retask.log) ───────────────

The path elides from the left as the terminal narrows, and drops entirely when there is no room, so the divider always occupies exactly one row:

termW=44   ─ Logs (…nweb/session-18dcdfe8/retask.log) ─
termW=20   ─ Logs (…task.log) ─
termW=16   ─ Logs ─────────────

Compatibility

Purely additive. Existing callers keep their current behaviour: TUIConfig.LogPath is empty by default, so the divider still renders Logs, and nothing opens a file unless OpenLogFile is called. DefaultConfig() gains the LogFile block and TUI.ShowLogPath: true, following the existing pattern for AutoOpen.

Testing

go vet ./..., gofmt -l, go build ./... and go test -race ./... all pass locally.

New coverage: rotation at the threshold, generation shifting, discard beyond Backups, Backups: 0 truncation, MaxBytes: 0 never rotating, oversized single writes, append-across-opens, absolute path resolution, write-after-close, concurrent writes under -race, nil-writer no-op, plus divider label rendering at wide, narrow and too-narrow terminal widths.

Follow-up

A tagged release is needed before nwebxyz/retask-cli can consume this.

🤖 Generated with Claude Code

The library captures the process log stream in a LogBuffer for the TUI
panel, but nothing reaches disk — once the TUI exits the run leaves no
record to debug from.

Add LogFileConfig and OpenLogFile, which return an io.Writer callers tee
into their existing slog handler so the same lines land in both places.
Rotation follows the Unix convention: the live file keeps its name and
older generations shift down through <path>.1 … <path>.N before being
discarded. A single write is never split across two generations, so a log
line always lands whole in one file.

*LogFile is nil-safe on every method and OpenLogFile returns nil when the
file is disabled, so callers need no branching to turn it off.

TUIConfig gains LogPath and ShowLogPath (default true), rendering
" Logs (/path/to/file.log) " on the log panel divider. The path elides
from the left and drops entirely on a terminal too narrow to hold it, so
the divider always fits one row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
govulncheck fails the pipeline on GO-2026-5856 (Encrypted Client Hello
privacy leak in crypto/tls), which is fixed in go1.26.5. CI installs the
toolchain from the go directive, so bumping it clears the finding.

Same fix as db264ab, which bumped to 1.26.4 for GO-2026-5039.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hoaitan
hoaitan merged commit 6c61e4d into hoaitan:main Aug 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants