Skip to content
Merged
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
20 changes: 12 additions & 8 deletions cmd/codeaf/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ func runTelemetry(args []string) error {
return runTelemetryStatus(nil)
}
switch args[0] {
case "-h", "-help", "--help":
// ASKING IS NEVER A FAILURE. This door took the flag for a sixth
// verb and left with 1, the one door in the binary that did.
return commandHelp("telemetry")
case "status":
return runTelemetryStatus(args[1:])
case "info":
Expand All @@ -42,11 +46,11 @@ func runTelemetry(args []string) error {
}

// telemetryFlags holds the one flag every verb accepts so a --help reader and
// the tests share one parser.
// the tests share one parser. It is the binary's own seam, named for the whole
// line a person typed, so `codeaf telemetry status --help` prints the usage and
// leaves with 0 like every other verb instead of `flag: help requested` and 1.
func telemetryFlags(name string) *flag.FlagSet {
flags := flag.NewFlagSet(name, flag.ContinueOnError)
flags.SetOutput(os.Stderr)
return flags
return commandFlags("telemetry " + name)
}

// runTelemetryStatus prints the pipe's whole answer: on or off, why it is off,
Expand All @@ -55,7 +59,7 @@ func telemetryFlags(name string) *flag.FlagSet {
// recognise, far too little to be a person.
func runTelemetryStatus(args []string) error {
flags := telemetryFlags("status")
if err := flags.Parse(args); err != nil {
if err := parseCommandFlags(flags, args); err != nil {
return err
}
// The config answer goes through the package's single door so the row
Expand Down Expand Up @@ -110,7 +114,7 @@ func telemetryInstallPrefix() string {
// here, each under a line naming where it goes or why it does not.
func runTelemetryInfo(args []string) error {
flags := telemetryFlags("info")
if err := flags.Parse(args); err != nil {
if err := parseCommandFlags(flags, args); err != nil {
return err
}
profileDir := config.ProfileDir()
Expand All @@ -127,7 +131,7 @@ func runTelemetryInfo(args []string) error {
// it, not piping it; a pipe reads indented JSON just as well.
func runTelemetryShow(args []string) error {
flags := telemetryFlags("show")
if err := flags.Parse(args); err != nil {
if err := parseCommandFlags(flags, args); err != nil {
return err
}
profileDir := config.ProfileDir()
Expand Down Expand Up @@ -371,7 +375,7 @@ func poolRowsWaiting(poolDir string) []json.RawMessage {
// nobody could audit.
func runTelemetrySet(word string, args []string) error {
flags := telemetryFlags(word)
if err := flags.Parse(args); err != nil {
if err := parseCommandFlags(flags, args); err != nil {
return err
}
profileDir := config.ProfileDir()
Expand Down
34 changes: 34 additions & 0 deletions cmd/codeaf/usage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"bytes"
"errors"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
Expand Down Expand Up @@ -67,6 +69,15 @@ func TestAskingForHelpIsNotAFailure(t *testing.T) {
{"plan run", func(args []string) error { return runGraph("plan run", args) }},
{"services", runServices},
{"models", runModels},
// The telemetry group and each of its verbs. The group took `--help`
// for a sixth verb and every verb answered `flag: help requested`,
// all with 1, while the manual says help on any verb exits 0.
{"telemetry", runTelemetry},
{"telemetry status", telemetryVerb("status")},
{"telemetry info", telemetryVerb("info")},
{"telemetry show", telemetryVerb("show")},
{"telemetry on", telemetryVerb("on")},
{"telemetry off", telemetryVerb("off")},
// The two old top-level spellings. They still open, and asking one for
// help says NOTHING on stderr: `--help` runs nothing, so there is no run
// for the rename notice to be about, and a Makefile that probes the
Expand Down Expand Up @@ -112,6 +123,29 @@ func TestAskingForHelpIsNotAFailure(t *testing.T) {
}
}

// telemetryVerb is `codeaf telemetry <verb>` as the dispatch reaches it.
func telemetryVerb(verb string) func([]string) error {
return func(args []string) error { return runTelemetry(append([]string{verb}, args...)) }
}

// ASKING `off` FOR HELP TURNS NOTHING OFF. `--help` runs nothing, so a person
// reading what `codeaf telemetry off` does has not yet chosen to do it.
func TestAskingTelemetryOffForHelpChangesNothing(t *testing.T) {
home := t.TempDir()
t.Setenv("CODEAF_HOME", home)
before, _ := os.ReadFile(filepath.Join(config.ProfileDir(), "config.json"))
captureUsage(t)
for _, verb := range []string{"off", "on"} {
if code := exitCodeOf(runTelemetry([]string{verb, "--help"})); code != 0 {
t.Fatalf("`codeaf telemetry %s --help` left with %d, want 0", verb, code)
}
}
after, _ := os.ReadFile(filepath.Join(config.ProfileDir(), "config.json"))
if string(before) != string(after) {
t.Fatalf("asking for help rewrote the setting:\nbefore %q\nafter %q", before, after)
}
}

// A REAL FLAG ERROR IS STILL AN ERROR, and it is one fact said once.
//
// It used to be said twice: the flag package printed `flag provided but not
Expand Down
10 changes: 10 additions & 0 deletions docs/changes/unreleased/1533-happy-path-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
kind: fixed
title: telemetry --help exits 0, the manual names six --help groups, a wall tile shows its first age
pr: 1533
surface: [chat, docs]
invalidates:
- "On c34a3a76d, `codeaf telemetry --help` answered `telemetry takes one of: status, info, show, on, off` and exited 1, and each telemetry verb's `--help` printed `flag: help requested` and exited 1. The group and every verb now print their usage and exit 0, and `off --help` changes nothing."
- "The manual said `codeaf --help` prints five groups. Since senior-dev (#1488) it prints six: Talk to it, Hand it work, Hand it a whole task, Look at what happened, Housekeeping, Plan work by hand. A build that carries no program prints five."
- "On c34a3a76d, the wall drew an idle conversation's tile with a blank activity line on its first read after launch. The tile now says `updated … ago` from the transcript's last write."
---
9 changes: 6 additions & 3 deletions internal/manual/chat/running-from-the-terminal.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,13 @@ through the same code path the tool on the belt runs, so the two cannot drift:
fetch on a keyed provider, and every `image` call the model and are billed like any
other call.

## What codeaf --help prints — the five groups, and where the environment table went
## What codeaf --help prints — the six groups, and where the environment table went

`codeaf help`, `--help` and `-h` all print the same thing: every command under those five
headings, in that order, then five worked examples.
`codeaf help`, `--help` and `-h` all print the same thing: every command under six
headings, in this order — **Talk to it**, **Hand it work**, **Hand it a whole task**,
**Look at what happened**, **Housekeeping** and **Plan work by hand** — then five worked
examples. **Hand it a whole task** lists the programs this build carries, such as
`codeaf senior-dev`; a build that carries none prints the other five.

**The environment table is not on that page**: it is `codeaf help env`, because it is a
reference somebody consults and it used to be more than half of what `--help` printed.
Expand Down
2 changes: 1 addition & 1 deletion internal/tui3/wall.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ func (a *app) wallFrame(width, height int) []string {
tiles[i].rows = a.wallMiniRows(tail, wallInnerW(tileW))
if tail != nil {
tiles[i].doing = wallDoing(tail.recent, tiles[i].signal)
tiles[i].moved = tail.freshAt
tiles[i].moved = tail.moved
} else {
tiles[i].doing = wallDoing(nil, tiles[i].signal)
}
Expand Down
39 changes: 39 additions & 0 deletions internal/tui3/wallactivity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package tui3

import (
"os"
"path/filepath"
"strings"
"testing"
"time"

"github.com/Agent-Field/codeaf/internal/session"
)

// A finished conversation's first wall reading uses the conversation's own
// last write, so opening the wall does not claim that old work happened now.
func TestWallFirstReadShowsConversationFileActivity(t *testing.T) {
now := time.Date(2026, 9, 25, 12, 0, 0, 0, time.UTC)
file := filepath.Join(t.TempDir(), "transcript.jsonl")
if err := os.WriteFile(file, []byte("saved conversation\n"), 0600); err != nil {
t.Fatal(err)
}
last := now.Add(-5 * time.Minute)
if err := os.Chtimes(file, last, last); err != nil {
t.Fatal(err)
}
agent := &fakeAgent{model: "m", past: []session.DisplayEntry{{Role: "user", Text: "hello"}, {Role: "assistant", Text: "done"}}}
a := newTestApp(agent)
a.file, a.workspace, a.title = file, filepath.Dir(file), "A finished conversation"
a.width, a.height = 120, 40
a.clock = func() time.Time { return now }
_ = a.openWall()
drive(t, a, runCmd(a.wallReadCmd(a.frontTabKey()))...)
if got := wallTileRowsFor(t, a, a.frontTabKey()); !strings.Contains(got, "updated 5m ago") {
t.Fatalf("the first idle tile did not show its last activity:\n%s", got)
}
now = now.Add(2 * time.Minute)
if got := wallTileRowsFor(t, a, a.frontTabKey()); !strings.Contains(got, "updated 7m ago") {
t.Fatalf("the idle tile did not age with the wall clock:\n%s", got)
}
}
3 changes: 3 additions & 0 deletions internal/tui3/wallcontract.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,9 @@ type wallTail struct {
seen time.Time
fresh int
freshAt time.Time
// moved is the conversation's own last activity, including its file time
// when the first reading has no new lines to measure.
moved time.Time
// spark is a ring of per-second activity, newest at sparkAt.
spark [wallSparkLen]uint8
sparkAt time.Time
Expand Down
20 changes: 17 additions & 3 deletions internal/tui3/walltail.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tui3

import (
"os"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -60,6 +61,9 @@ type wallReadMsg struct {
// books is what the conversation's books said on the same trip
// ([Agent.Usage]), which takes the same lock the transcript does.
books float64
// modified is the transcript file's last write, read beside its contents
// off the loop so the first idle tile has a real activity time.
modified time.Time
}

// wallTickMsg is the wall's clock.
Expand Down Expand Up @@ -351,7 +355,12 @@ func (a *app) wallReadCmd(keys ...string) tea.Cmd {
key := key
cmds = append(cmds, func() tea.Msg {
at := time.Now()
return wallReadMsg{key: key, entries: agent.Transcript(), at: at, live: live, books: agent.Usage().CostUSD}
entries := agent.Transcript()
var modified time.Time
if info, err := os.Stat(key); err == nil {
modified = info.ModTime()
}
return wallReadMsg{key: key, entries: entries, at: at, live: live, books: agent.Usage().CostUSD, modified: modified}
})
}
switch len(cmds) {
Expand Down Expand Up @@ -390,6 +399,11 @@ func (a *app) wallTakeRead(msg wallReadMsg) {
}
tail.books = max(tail.books, msg.books)
tail.take(msg.entries, msg.at, msg.live)
if !tail.freshAt.IsZero() {
tail.moved = tail.freshAt
} else if tail.moved.IsZero() {
tail.moved = msg.modified
}
}

// wallStir is a held conversation's stir, as the wall hears it: a read of that
Expand Down Expand Up @@ -504,8 +518,8 @@ func (a *app) wallTiles(now time.Time) []wallTile {
tile.lines = tail.lines
tile.fresh = tail.fresh
tile.freshAt = tail.freshAt
if !tail.freshAt.IsZero() {
tile.age = wallAge(now.Sub(tail.freshAt))
if !tail.moved.IsZero() {
tile.age = wallAge(now.Sub(tail.moved))
}
if live {
tile.spark = tail.sparkline(now)
Expand Down
Loading