Split session views into views.go
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -166,102 +166,6 @@ type Controller struct {
|
|||||||
lastNudgedAt time.Time
|
lastNudgedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitmentView is the UI projection of the active commitment.
|
|
||||||
type CommitmentView struct {
|
|
||||||
NextAction string `json:"next_action"`
|
|
||||||
SuccessCondition string `json:"success_condition"`
|
|
||||||
TimeboxSecs int64 `json:"timebox_secs"`
|
|
||||||
DeadlineUnixSecs int64 `json:"deadline_unix_secs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProposalView / CoachView project the ephemeral planning-coach state.
|
|
||||||
type ProposalView struct {
|
|
||||||
NextAction string `json:"next_action"`
|
|
||||||
SuccessCondition string `json:"success_condition"`
|
|
||||||
TimeboxSecs int64 `json:"timebox_secs"`
|
|
||||||
|
|
||||||
AllowedWindowClasses []string `json:"allowed_window_classes,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DriftView is the active-only drift projection. Nudge is a separate axis from
|
|
||||||
// Status: it is populated precisely when Status is "ontask" (semantic drift
|
|
||||||
// inside an allowed app), so it cannot be folded into the status enum.
|
|
||||||
type DriftView struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Reason string `json:"reason,omitempty"`
|
|
||||||
Nudge string `json:"nudge,omitempty"`
|
|
||||||
Enforced bool `json:"enforced,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CoachView struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Proposal *ProposalView `json:"proposal,omitempty"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TaskView is one to-do item in the planning tasks list.
|
|
||||||
type TaskView struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Day string `json:"day,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TasksView projects the ephemeral planning tasks state (Marvin's today list).
|
|
||||||
type TasksView struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Tasks []TaskView `json:"tasks,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// KnowledgeView projects the ephemeral planning knowledge state (the standing
|
|
||||||
// profile that grounds the coach). The profile text is intentionally omitted —
|
|
||||||
// only its presence, source path, and size cross the wire.
|
|
||||||
type KnowledgeView struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Path string `json:"path,omitempty"`
|
|
||||||
Chars int `json:"chars,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReflectionView projects the reviewer's output. Recap is shown on Review;
|
|
||||||
// CarryForward is shown on the next Planning screen. Unlike the knowledge
|
|
||||||
// profile, these short lines exist to be displayed, so they cross the wire.
|
|
||||||
type ReflectionView struct {
|
|
||||||
Status string `json:"status,omitempty"`
|
|
||||||
Recap string `json:"recap,omitempty"`
|
|
||||||
CarryForward string `json:"carry_forward,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// WindowView / BucketView / EvidenceView are the evidence projection.
|
|
||||||
type WindowView struct {
|
|
||||||
Class string `json:"class"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BucketView struct {
|
|
||||||
Class string `json:"class"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Seconds int64 `json:"seconds"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EvidenceView struct {
|
|
||||||
Available bool `json:"available"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
Current WindowView `json:"current"`
|
|
||||||
SwitchCount int `json:"switch_count"`
|
|
||||||
Buckets []BucketView `json:"buckets"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// State is the broadcastable view of the controller.
|
|
||||||
type State struct {
|
|
||||||
RuntimeState domain.RuntimeState `json:"runtime_state"`
|
|
||||||
Commitment *CommitmentView `json:"commitment"`
|
|
||||||
Evidence *EvidenceView `json:"evidence"`
|
|
||||||
Coach *CoachView `json:"coach,omitempty"`
|
|
||||||
Tasks *TasksView `json:"tasks,omitempty"`
|
|
||||||
Knowledge *KnowledgeView `json:"knowledge,omitempty"`
|
|
||||||
Reflection *ReflectionView `json:"reflection,omitempty"`
|
|
||||||
Drift *DriftView `json:"drift,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// New loads any persisted snapshot, prunes stale session logs, and rebuilds
|
// New loads any persisted snapshot, prunes stale session logs, and rebuilds
|
||||||
// in-memory stats from the raw log if a live session was interrupted.
|
// in-memory stats from the raw log if a live session was interrupted.
|
||||||
func New(snapshotPath string) (*Controller, error) {
|
func New(snapshotPath string) (*Controller, error) {
|
||||||
@@ -340,92 +244,6 @@ func (c *Controller) Deadline() time.Time {
|
|||||||
return c.deadline
|
return c.deadline
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) stateLocked() State {
|
|
||||||
st := State{RuntimeState: c.runtimeState}
|
|
||||||
if c.commitment != nil {
|
|
||||||
view := &CommitmentView{
|
|
||||||
NextAction: c.commitment.NextAction,
|
|
||||||
SuccessCondition: c.commitment.SuccessCondition,
|
|
||||||
TimeboxSecs: c.commitment.TimeboxSecs,
|
|
||||||
}
|
|
||||||
if !c.deadline.IsZero() {
|
|
||||||
view.DeadlineUnixSecs = c.deadline.Unix()
|
|
||||||
}
|
|
||||||
st.Commitment = view
|
|
||||||
}
|
|
||||||
if c.stats != nil {
|
|
||||||
st.Evidence = &EvidenceView{
|
|
||||||
Available: c.stats.Current.Health.Available,
|
|
||||||
Reason: c.stats.Current.Health.Reason,
|
|
||||||
Current: WindowView{Class: c.stats.Current.Class, Title: c.stats.Current.Title},
|
|
||||||
SwitchCount: c.stats.SwitchCount,
|
|
||||||
Buckets: bucketViews(c.stats.Buckets),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c.runtimeState == domain.RuntimePlanning {
|
|
||||||
status := c.coachStatus
|
|
||||||
if status == "" {
|
|
||||||
status = coachIdle
|
|
||||||
}
|
|
||||||
cv := &CoachView{Status: status, Error: c.coachErr}
|
|
||||||
if c.coachProposal != nil {
|
|
||||||
cv.Proposal = &ProposalView{
|
|
||||||
NextAction: c.coachProposal.NextAction,
|
|
||||||
SuccessCondition: c.coachProposal.SuccessCondition,
|
|
||||||
TimeboxSecs: c.coachProposal.TimeboxSecs,
|
|
||||||
AllowedWindowClasses: c.coachProposal.AllowedWindowClasses,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
st.Coach = cv
|
|
||||||
if c.tasksProvider != nil {
|
|
||||||
tstatus := c.tasksStatus
|
|
||||||
if tstatus == "" {
|
|
||||||
tstatus = tasksIdle
|
|
||||||
}
|
|
||||||
tv := &TasksView{Status: tstatus}
|
|
||||||
for _, t := range c.tasksList {
|
|
||||||
tv.Tasks = append(tv.Tasks, TaskView{ID: t.ID, Title: t.Title, Day: t.Day})
|
|
||||||
}
|
|
||||||
st.Tasks = tv
|
|
||||||
}
|
|
||||||
if c.knowledgeSrc != nil {
|
|
||||||
kstatus := c.knowledgeStatus
|
|
||||||
if kstatus == "" {
|
|
||||||
kstatus = knowledgeIdle
|
|
||||||
}
|
|
||||||
st.Knowledge = &KnowledgeView{Status: kstatus, Path: c.knowledgePath, Chars: c.knowledgeChars}
|
|
||||||
}
|
|
||||||
if c.carryForward != "" {
|
|
||||||
st.Reflection = &ReflectionView{CarryForward: c.carryForward}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c.runtimeState == domain.RuntimeReview {
|
|
||||||
rstatus := c.reflectionStatus
|
|
||||||
if rstatus == "" {
|
|
||||||
rstatus = reflectionIdle
|
|
||||||
}
|
|
||||||
st.Reflection = &ReflectionView{Status: rstatus, Recap: c.reflectionRecap}
|
|
||||||
}
|
|
||||||
if c.runtimeState == domain.RuntimeActive {
|
|
||||||
status := c.driftStatus
|
|
||||||
if status == "" {
|
|
||||||
status = driftIdle
|
|
||||||
}
|
|
||||||
enforced := c.enforcementLevel == domain.EnforcementBlock && c.driftStatus == driftDrifting
|
|
||||||
st.Drift = &DriftView{Status: status, Reason: c.driftReason, Nudge: c.nudgeMessage, Enforced: enforced}
|
|
||||||
}
|
|
||||||
return st
|
|
||||||
}
|
|
||||||
|
|
||||||
func bucketViews(buckets map[bucketKey]time.Duration) []BucketView {
|
|
||||||
out := make([]BucketView, 0, len(buckets))
|
|
||||||
for k, d := range buckets {
|
|
||||||
out = append(out, BucketView{Class: k.Class, Title: k.Title, Seconds: int64(d.Seconds())})
|
|
||||||
}
|
|
||||||
sort.Slice(out, func(i, j int) bool { return out[i].Seconds > out[j].Seconds })
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) persistLocked() error {
|
func (c *Controller) persistLocked() error {
|
||||||
snap := store.Snapshot{
|
snap := store.Snapshot{
|
||||||
RuntimeState: c.runtimeState,
|
RuntimeState: c.runtimeState,
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"antidrift/internal/domain"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommitmentView is the UI projection of the active commitment.
|
||||||
|
type CommitmentView struct {
|
||||||
|
NextAction string `json:"next_action"`
|
||||||
|
SuccessCondition string `json:"success_condition"`
|
||||||
|
TimeboxSecs int64 `json:"timebox_secs"`
|
||||||
|
DeadlineUnixSecs int64 `json:"deadline_unix_secs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProposalView / CoachView project the ephemeral planning-coach state.
|
||||||
|
type ProposalView struct {
|
||||||
|
NextAction string `json:"next_action"`
|
||||||
|
SuccessCondition string `json:"success_condition"`
|
||||||
|
TimeboxSecs int64 `json:"timebox_secs"`
|
||||||
|
|
||||||
|
AllowedWindowClasses []string `json:"allowed_window_classes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DriftView is the active-only drift projection. Nudge is a separate axis from
|
||||||
|
// Status: it is populated precisely when Status is "ontask" (semantic drift
|
||||||
|
// inside an allowed app), so it cannot be folded into the status enum.
|
||||||
|
type DriftView struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Nudge string `json:"nudge,omitempty"`
|
||||||
|
Enforced bool `json:"enforced,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CoachView struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Proposal *ProposalView `json:"proposal,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskView is one to-do item in the planning tasks list.
|
||||||
|
type TaskView struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Day string `json:"day,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TasksView projects the ephemeral planning tasks state (Marvin's today list).
|
||||||
|
type TasksView struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Tasks []TaskView `json:"tasks,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KnowledgeView projects the ephemeral planning knowledge state (the standing
|
||||||
|
// profile that grounds the coach). The profile text is intentionally omitted —
|
||||||
|
// only its presence, source path, and size cross the wire.
|
||||||
|
type KnowledgeView struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
Chars int `json:"chars,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReflectionView projects the reviewer's output. Recap is shown on Review;
|
||||||
|
// CarryForward is shown on the next Planning screen. Unlike the knowledge
|
||||||
|
// profile, these short lines exist to be displayed, so they cross the wire.
|
||||||
|
type ReflectionView struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Recap string `json:"recap,omitempty"`
|
||||||
|
CarryForward string `json:"carry_forward,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowView / BucketView / EvidenceView are the evidence projection.
|
||||||
|
type WindowView struct {
|
||||||
|
Class string `json:"class"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BucketView struct {
|
||||||
|
Class string `json:"class"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Seconds int64 `json:"seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EvidenceView struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Current WindowView `json:"current"`
|
||||||
|
SwitchCount int `json:"switch_count"`
|
||||||
|
Buckets []BucketView `json:"buckets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// State is the broadcastable view of the controller.
|
||||||
|
type State struct {
|
||||||
|
RuntimeState domain.RuntimeState `json:"runtime_state"`
|
||||||
|
Commitment *CommitmentView `json:"commitment"`
|
||||||
|
Evidence *EvidenceView `json:"evidence"`
|
||||||
|
Coach *CoachView `json:"coach,omitempty"`
|
||||||
|
Tasks *TasksView `json:"tasks,omitempty"`
|
||||||
|
Knowledge *KnowledgeView `json:"knowledge,omitempty"`
|
||||||
|
Reflection *ReflectionView `json:"reflection,omitempty"`
|
||||||
|
Drift *DriftView `json:"drift,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) stateLocked() State {
|
||||||
|
st := State{RuntimeState: c.runtimeState}
|
||||||
|
if c.commitment != nil {
|
||||||
|
view := &CommitmentView{
|
||||||
|
NextAction: c.commitment.NextAction,
|
||||||
|
SuccessCondition: c.commitment.SuccessCondition,
|
||||||
|
TimeboxSecs: c.commitment.TimeboxSecs,
|
||||||
|
}
|
||||||
|
if !c.deadline.IsZero() {
|
||||||
|
view.DeadlineUnixSecs = c.deadline.Unix()
|
||||||
|
}
|
||||||
|
st.Commitment = view
|
||||||
|
}
|
||||||
|
if c.stats != nil {
|
||||||
|
st.Evidence = &EvidenceView{
|
||||||
|
Available: c.stats.Current.Health.Available,
|
||||||
|
Reason: c.stats.Current.Health.Reason,
|
||||||
|
Current: WindowView{Class: c.stats.Current.Class, Title: c.stats.Current.Title},
|
||||||
|
SwitchCount: c.stats.SwitchCount,
|
||||||
|
Buckets: bucketViews(c.stats.Buckets),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.runtimeState == domain.RuntimePlanning {
|
||||||
|
status := c.coachStatus
|
||||||
|
if status == "" {
|
||||||
|
status = coachIdle
|
||||||
|
}
|
||||||
|
cv := &CoachView{Status: status, Error: c.coachErr}
|
||||||
|
if c.coachProposal != nil {
|
||||||
|
cv.Proposal = &ProposalView{
|
||||||
|
NextAction: c.coachProposal.NextAction,
|
||||||
|
SuccessCondition: c.coachProposal.SuccessCondition,
|
||||||
|
TimeboxSecs: c.coachProposal.TimeboxSecs,
|
||||||
|
AllowedWindowClasses: c.coachProposal.AllowedWindowClasses,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st.Coach = cv
|
||||||
|
if c.tasksProvider != nil {
|
||||||
|
tstatus := c.tasksStatus
|
||||||
|
if tstatus == "" {
|
||||||
|
tstatus = tasksIdle
|
||||||
|
}
|
||||||
|
tv := &TasksView{Status: tstatus}
|
||||||
|
for _, t := range c.tasksList {
|
||||||
|
tv.Tasks = append(tv.Tasks, TaskView{ID: t.ID, Title: t.Title, Day: t.Day})
|
||||||
|
}
|
||||||
|
st.Tasks = tv
|
||||||
|
}
|
||||||
|
if c.knowledgeSrc != nil {
|
||||||
|
kstatus := c.knowledgeStatus
|
||||||
|
if kstatus == "" {
|
||||||
|
kstatus = knowledgeIdle
|
||||||
|
}
|
||||||
|
st.Knowledge = &KnowledgeView{Status: kstatus, Path: c.knowledgePath, Chars: c.knowledgeChars}
|
||||||
|
}
|
||||||
|
if c.carryForward != "" {
|
||||||
|
st.Reflection = &ReflectionView{CarryForward: c.carryForward}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.runtimeState == domain.RuntimeReview {
|
||||||
|
rstatus := c.reflectionStatus
|
||||||
|
if rstatus == "" {
|
||||||
|
rstatus = reflectionIdle
|
||||||
|
}
|
||||||
|
st.Reflection = &ReflectionView{Status: rstatus, Recap: c.reflectionRecap}
|
||||||
|
}
|
||||||
|
if c.runtimeState == domain.RuntimeActive {
|
||||||
|
status := c.driftStatus
|
||||||
|
if status == "" {
|
||||||
|
status = driftIdle
|
||||||
|
}
|
||||||
|
enforced := c.enforcementLevel == domain.EnforcementBlock && c.driftStatus == driftDrifting
|
||||||
|
st.Drift = &DriftView{Status: status, Reason: c.driftReason, Nudge: c.nudgeMessage, Enforced: enforced}
|
||||||
|
}
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
|
||||||
|
func bucketViews(buckets map[bucketKey]time.Duration) []BucketView {
|
||||||
|
out := make([]BucketView, 0, len(buckets))
|
||||||
|
for k, d := range buckets {
|
||||||
|
out = append(out, BucketView{Class: k.Class, Title: k.Title, Seconds: int64(d.Seconds())})
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i].Seconds > out[j].Seconds })
|
||||||
|
return out
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user