Files

105 lines
2.6 KiB
Go

// internal/mode/offscreen/collect.go
package offscreen
import (
"context"
"fmt"
"strings"
"time"
"keel/internal/frame"
)
const (
recentProposals = 5 // how many recent proposals to show
outcomeScan = 50 // how many outcome events to scan for correlation
recentWindow = 7 * 24 * time.Hour
)
// assembleBrief builds the off-screen prompt context: the shared frame brief
// (today's tasks + ~/owc goals and life-domains) plus off-screen's own recent
// proposal history. Best-effort: never panics, never errors, tolerates nil
// ports and missing files.
func (m *Mode) assembleBrief() string {
base := frame.Assemble(context.Background(), m.know, m.tasks)
hist := m.briefHistory()
if hist == "" {
return base
}
var b strings.Builder
b.WriteString(base)
if !strings.HasSuffix(base, "\n") {
b.WriteString("\n")
}
b.WriteString("\n## Recently proposed\n")
b.WriteString(hist)
return frame.Truncate(b.String(), frame.MaxBriefBytes)
}
// briefHistory renders recent proposals with their outcome, newest first and
// within recentWindow. Returns "" when there is no memory or no recent history.
func (m *Mode) briefHistory() string {
if m.mem == nil {
return ""
}
ctx := context.Background()
proposals, err := m.mem.Recent(ctx, kindProposalMade, recentProposals)
if err != nil || len(proposals) == 0 {
return ""
}
taken := m.idSet(ctx, kindActionTaken)
dismissed := m.idSet(ctx, kindProposalDismissed)
now := m.now()
var b strings.Builder
for _, ev := range proposals {
if now.Sub(ev.At) > recentWindow {
continue
}
id, _ := ev.Data["proposal_id"].(string)
action, _ := ev.Data["next_action"].(string)
if action == "" {
continue
}
outcome := "unactioned"
if taken[id] {
outcome = "confirmed"
} else if dismissed[id] {
outcome = "dismissed"
}
fmt.Fprintf(&b, "- %q (%s, %s)\n", action, outcome, relativeAge(now.Sub(ev.At)))
}
return b.String()
}
// idSet reads recent events of a kind and returns their proposal_ids as a set.
func (m *Mode) idSet(ctx context.Context, kind string) map[string]bool {
out := map[string]bool{}
evs, err := m.mem.Recent(ctx, kind, outcomeScan)
if err != nil {
return out
}
for _, e := range evs {
if id, _ := e.Data["proposal_id"].(string); id != "" {
out[id] = true
}
}
return out
}
// relativeAge renders a coarse human age for a duration.
func relativeAge(d time.Duration) string {
switch {
case d < time.Minute:
return "just now"
case d < time.Hour:
return fmt.Sprintf("%dm ago", int(d.Minutes()))
case d < 24*time.Hour:
return fmt.Sprintf("%dh ago", int(d.Hours()))
default:
return fmt.Sprintf("%dd ago", int(d.Hours()/24))
}
}