Render on/off-task split in the reflection block
This commit is contained in:
@@ -263,9 +263,10 @@ func (c *Controller) startReflectionFetchLocked() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildReflectionFinishedLocked renders the just-finished session as a compact
|
// buildReflectionFinishedLocked renders the just-finished session as a compact
|
||||||
// block for the reviewer. Caller holds mu; c.stats/c.commitment are still set
|
// block for the reviewer: the commitment, the outcome, on/off/unclassified time
|
||||||
// (End clears them, but enterReview runs before End). Reuses bucketViews for the
|
// totals, and a top-N on-task list and top-N off-task list. The split fields are
|
||||||
// per-window totals, already sorted desc by seconds.
|
// populated live by creditLocked. Caller holds mu; c.stats/c.commitment are still
|
||||||
|
// set (End clears them, but enterReview runs before End).
|
||||||
func (c *Controller) buildReflectionFinishedLocked() string {
|
func (c *Controller) buildReflectionFinishedLocked() string {
|
||||||
var na, sc string
|
var na, sc string
|
||||||
if c.commitment != nil {
|
if c.commitment != nil {
|
||||||
@@ -280,15 +281,41 @@ func (c *Controller) buildReflectionFinishedLocked() string {
|
|||||||
fmt.Fprintf(&b, "Success condition: %s\n", sc)
|
fmt.Fprintf(&b, "Success condition: %s\n", sc)
|
||||||
fmt.Fprintf(&b, "Outcome: %s\n", outcome)
|
fmt.Fprintf(&b, "Outcome: %s\n", outcome)
|
||||||
if c.stats != nil {
|
if c.stats != nil {
|
||||||
|
onMin := int64(sumDurations(c.stats.OnTask).Seconds()) / 60
|
||||||
|
offMin := int64(sumDurations(c.stats.OffTask).Seconds()) / 60
|
||||||
|
unclMin := int64(c.stats.unclassified.Seconds()) / 60
|
||||||
|
fmt.Fprintf(&b, "On-task %dm / Off-task %dm / Unclassified %dm\n", onMin, offMin, unclMin)
|
||||||
fmt.Fprintf(&b, "Context switches: %d\n", c.stats.SwitchCount)
|
fmt.Fprintf(&b, "Context switches: %d\n", c.stats.SwitchCount)
|
||||||
for i, bv := range bucketViews(c.stats.Buckets) {
|
writeBucketList(&b, "On-task", c.stats.OnTask)
|
||||||
|
writeBucketList(&b, "Off-task", c.stats.OffTask)
|
||||||
|
}
|
||||||
|
return strings.TrimRight(b.String(), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeBucketList renders a labeled, time-descending list of buckets capped at
|
||||||
|
// reflectionTopBuckets. It writes nothing — not even the label — when the map is
|
||||||
|
// empty, so a single-sided session shows only the list that has time.
|
||||||
|
func writeBucketList(b *strings.Builder, label string, m map[bucketKey]time.Duration) {
|
||||||
|
views := bucketViews(m)
|
||||||
|
if len(views) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(b, "%s:\n", label)
|
||||||
|
for i, bv := range views {
|
||||||
if i >= reflectionTopBuckets {
|
if i >= reflectionTopBuckets {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&b, "- %s · %s: %dm\n", bv.Class, bv.Title, bv.Seconds/60)
|
fmt.Fprintf(b, "- %s · %s: %dm\n", bv.Class, bv.Title, bv.Seconds/60)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sumDurations totals the durations in a bucket map.
|
||||||
|
func sumDurations(m map[bucketKey]time.Duration) time.Duration {
|
||||||
|
var total time.Duration
|
||||||
|
for _, d := range m {
|
||||||
|
total += d
|
||||||
}
|
}
|
||||||
return strings.TrimRight(b.String(), "\n")
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildReflectionHistory renders the last few prior sessions as compact lines.
|
// buildReflectionHistory renders the last few prior sessions as compact lines.
|
||||||
|
|||||||
@@ -1408,3 +1408,60 @@ func TestCreditLockedSplitsByDriftStatus(t *testing.T) {
|
|||||||
t.Errorf("Buckets total = %v, want 20s", got)
|
t.Errorf("Buckets total = %v, want 20s", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReflectionBlockShowsOnOffSplit(t *testing.T) {
|
||||||
|
c, _ := newTestController(t)
|
||||||
|
startActive(t, c, nil) // sets commitment ("write report" / "report drafted") and stats
|
||||||
|
|
||||||
|
c.stats.SwitchCount = 2
|
||||||
|
c.stats.OnTask = map[bucketKey]time.Duration{
|
||||||
|
{Class: "code", Title: "main.go"}: 20 * time.Minute,
|
||||||
|
}
|
||||||
|
c.stats.OffTask = map[bucketKey]time.Duration{
|
||||||
|
{Class: "firefox", Title: "YouTube"}: 8 * time.Minute,
|
||||||
|
{Class: "firefox", Title: "Reddit"}: 4 * time.Minute,
|
||||||
|
}
|
||||||
|
c.stats.unclassified = 5 * time.Minute
|
||||||
|
c.outcomePending = "completed"
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
got := c.buildReflectionFinishedLocked()
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
want := "Next action: write report\n" +
|
||||||
|
"Success condition: report drafted\n" +
|
||||||
|
"Outcome: completed\n" +
|
||||||
|
"On-task 20m / Off-task 12m / Unclassified 5m\n" +
|
||||||
|
"Context switches: 2\n" +
|
||||||
|
"On-task:\n" +
|
||||||
|
"- code · main.go: 20m\n" +
|
||||||
|
"Off-task:\n" +
|
||||||
|
"- firefox · YouTube: 8m\n" +
|
||||||
|
"- firefox · Reddit: 4m"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("reflection block mismatch:\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReflectionBlockOmitsEmptyOffTaskList(t *testing.T) {
|
||||||
|
c, _ := newTestController(t)
|
||||||
|
startActive(t, c, nil)
|
||||||
|
|
||||||
|
c.stats.OnTask = map[bucketKey]time.Duration{
|
||||||
|
{Class: "code", Title: "main.go"}: 20 * time.Minute,
|
||||||
|
}
|
||||||
|
c.stats.OffTask = map[bucketKey]time.Duration{} // none
|
||||||
|
c.stats.unclassified = 0
|
||||||
|
c.outcomePending = "completed"
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
got := c.buildReflectionFinishedLocked()
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if strings.Contains(got, "Off-task:") {
|
||||||
|
t.Errorf("fully on-task session must omit the Off-task list, got:\n%s", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "On-task 20m / Off-task 0m / Unclassified 0m") {
|
||||||
|
t.Errorf("totals line missing or wrong, got:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user