105 lines
2.6 KiB
Go
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))
|
|
}
|
|
}
|