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
|
||||
}
|
||||
|
||||
// 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
|
||||
// in-memory stats from the raw log if a live session was interrupted.
|
||||
func New(snapshotPath string) (*Controller, error) {
|
||||
@@ -340,92 +244,6 @@ func (c *Controller) Deadline() time.Time {
|
||||
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 {
|
||||
snap := store.Snapshot{
|
||||
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