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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ The list nests three tiers — **day → project → session** — each folding

The preview always shows **what that scope produced** — PRs, Jira issues, artifacts and plans, one row each. Selecting a date row shows the day's outputs; selecting a project row narrows to that project on that day. The sessions themselves are not listed in the pane: they are one row below in the list.

Rows read as a **timeline**: every output in the order it first appeared, stamped with that time, kinds interleaved — a day is lived in time, and "what happened after the PR went up" is the question the pane is usually asked. An output whose first mention falls on another date (a long-lived session carrying a ref in) shows its full date rather than a bare time that would belong to the wrong day, and a `~` marks a time taken from the producing session because the output records no entry of its own (plan slugs, and refs extracted by an older build).

To read one kind at a time, the pane is **tabbed**: `All` plus a tab for each kind the scope produced (`PRs`, `Jira`, `Artifacts`, `Plans`), each carrying its own count so the bar doubles as the day's rollup. `tab`/`shift+tab` switches — the same keys that rotate preview modes on a session row, since a day row has no preview modes to rotate. Kinds are tabs rather than sections in one list because a busy day produces 500+ outputs, and stacked sections put the later kinds hundreds of lines below the fold. Every tab keeps the one chronology, and the selected tab is sticky as you walk between dates, so "what PRs did each day produce" stays a single keypress per day; a day with none of that kind says so rather than silently falling back to `All`.

Every output row carries the session that produced it as an anchor (`a1b2c3 · ~/src/repo`). Focus the preview and press `Enter` on a row to land in that conversation **at the message where the output first appeared** — the digest tells you *what* came out, and the anchor is how you get to *how*. `o` opens the output itself (a PR, Jira issue or artifact in the browser), `y` copies its URL or path, and `x` lists every action that applies to the row (see [Output Row Actions](#output-row-actions-x)). Outputs referenced from several sessions collapse to one row with a `+N` spread marker, anchored to the earliest session (where the work happened, not where it was later quoted) — and the jump lands in *that* session, at *its* first mention.

Sessions are bucketed by the calendar day of their **last** activity. A session that spans midnight appears once, under the day it was last active — it is never duplicated across dates.
Expand Down
23 changes: 18 additions & 5 deletions internal/tui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ type App struct {
dayOutputRows []dayOutputRow // outputs shown in the daily view's day pane, in cursor order
dayOutputsCursor int // cursor within the day pane's output list
dayOutputsCacheID string // day key the cursor currently tracks
dayOutputTabKind session.OutputKind // day pane's active kind tab ("" = the All timeline)
preDailyGroupMode int // grouping to restore when the daily view is toggled back off
dailyPreviewMode sessPreview // preview mode remembered for the daily view
browserPreviewMode sessPreview // preview mode remembered for every other grouping
Expand Down Expand Up @@ -2323,6 +2324,14 @@ func (a *App) handleSessionKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
sp.Focus = false
return a, a.updateSessionPreview()
}
// A row that owns the day pane has no preview modes to rotate — its pane
// is that scope's outputs whatever sessPreviewMode says (the same reason
// rowSupportsPreviewModes blocks the digits there). Tab switches the
// pane's KIND instead, which is the only axis it actually has.
if a.selectedOwnsDayPane() {
a.cycleDayOutputTab(+1)
return a, nil
}
a.cycleSessionPreviewMode()
return a, a.updateSessionPreview()
case km.Session.PreviewBack:
Expand All @@ -2331,6 +2340,10 @@ func (a *App) handleSessionKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
sp.Focus = false
return a, a.updateSessionPreview()
}
if a.selectedOwnsDayPane() {
a.cycleDayOutputTab(-1)
return a, nil
}
a.cycleSessionPreviewModeReverse()
return a, a.updateSessionPreview()
}
Expand Down Expand Up @@ -5391,9 +5404,9 @@ func (a *App) updateSessionPreview() tea.Cmd {
// produced — rather than an arbitrary child's detail. Drilling into a
// child session is what opens the per-session Outputs digest.
// The pane is the day's outputs regardless of preview mode, so the mode
// is not part of the key; the cursor and focus are, so moving the
// highlight re-renders.
cacheKey := fmt.Sprintf("day:%s:%d:%d:%t", di.dayKey, len(di.sessions), a.dayOutputsCursor, a.sessSplit.Focus)
// is not part of the key; the cursor, focus and KIND TAB are, so moving
// the highlight or switching tabs re-renders.
cacheKey := fmt.Sprintf("day:%s:%d:%d:%t:%s", di.dayKey, len(di.sessions), a.dayOutputsCursor, a.sessSplit.Focus, a.dayOutputTabKind)
if cacheKey == a.sessSplit.CacheKey {
return nil
}
Expand All @@ -5407,8 +5420,8 @@ func (a *App) updateSessionPreview() tea.Cmd {
// day's work in one project, so its pane is that slice's outputs — not a
// representative session's, and not the generic project summary.
if pi.dayKey != "" {
cacheKey := fmt.Sprintf("dayproj:%s:%s:%d:%d:%t", pi.dayKey, pi.basePath,
len(pi.sessions), a.dayOutputsCursor, a.sessSplit.Focus)
cacheKey := fmt.Sprintf("dayproj:%s:%s:%d:%d:%t:%s", pi.dayKey, pi.basePath,
len(pi.sessions), a.dayOutputsCursor, a.sessSplit.Focus, a.dayOutputTabKind)
if cacheKey == a.sessSplit.CacheKey {
return nil
}
Expand Down
32 changes: 24 additions & 8 deletions internal/tui/daily_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,10 @@ func TestDayPreviewCollapsesRepeatedOutputs(t *testing.T) {
}
}

func TestDayPreviewOrdersResultsBeforePlans(t *testing.T) {
// TestDayPreviewTabsCoverEveryKindProduced replaces the old kind-ordering test:
// kinds are no longer sections in one list, they are tabs, and the bar must
// offer exactly the kinds the scope produced (All plus those).
func TestDayPreviewTabsCoverEveryKindProduced(t *testing.T) {
sessions := []session.Session{{
ID: "a1", ShortID: "a1", ProjectPath: "/tmp/repo-a", ModTime: dayOf(0),
PlanSlugs: []string{"a-plan"},
Expand All @@ -546,13 +549,26 @@ func TestDayPreviewOrdersResultsBeforePlans(t *testing.T) {
di := buildDailyItems(sessions, nil)[0].(dayItem)
rows := buildDayOutputRows(di)

want := []session.OutputKind{session.OutputPR, session.OutputArtifact, session.OutputPlan}
if len(rows) != len(want) {
t.Fatalf("expected %d rows, got %d", len(want), len(rows))
}
for i, k := range want {
if rows[i].out.Kind != k {
t.Fatalf("row %d: got %s, want %s", i, rows[i].out.Kind, k)
tabs := dayOutputTabsFor(rows, "")
var got []string
for _, tb := range tabs {
got = append(got, tb.label)
}
// All first, then the produced kinds in outputKindRank order. Jira is absent
// because the day produced none — a tab that can only ever be empty would
// make the bar say more than the day does.
want := []string{"All", "PRs", "Artifacts", "Plans"}
if strings.Join(got, ",") != strings.Join(want, ",") {
t.Errorf("tabs = %v, want %v", got, want)
}
for _, tb := range tabs {
if tb.kind == "" {
continue
}
for _, r := range filterDayOutputRows(rows, tb) {
if r.out.Kind != tb.kind {
t.Errorf("tab %q leaked a %s row", tb.label, r.out.Kind)
}
}
}
}
Expand Down
Loading
Loading