Skip to content

refactor(app): expose session state as value snapshots (#101) - #104

Merged
ezynda3 merged 2 commits into
masterfrom
feat/101-session-snapshot
Jul 30, 2026
Merged

refactor(app): expose session state as value snapshots (#101)#104
ezynda3 merged 2 commits into
masterfrom
feat/101-session-snapshot

Conversation

@ezynda3

@ezynda3 ezynda3 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Step (b) of the #101 refactor. Follows #102 (lazy terminal capability detection) and #103 (sealed app.Event union).

Problem

internal/ui reached through AppController.GetTreeSession() to get the live *session.TreeManager and drove it directly — reading the header, walking the branch, appending entries, forking sessions — and type-switched on the concrete persistence types (*session.MessageEntry, *session.ModelChangeEntry, *session.BranchSummaryEntry, ...) to decide how to filter and render the session tree.

That made the on-disk entry schema part of the UI's contract. Changing how entries are persisted broke tree rendering, and none of the tree/session UI could be exercised without a real session on disk.

Change

Session state is projected into immutable value types owned by the app layer (internal/app/session_view.go):

Type Carries
SessionSnapshot id, name, file path, cwd, created, leaf id, entry/message counts, persisted
TreeNodeView id, parent id, EntryKind, role, text payload, label, children
EntryKind value enum replacing the concrete-type switches

Plus SessionHistory() []message.Message for transcript replay, so the UI no longer decodes entries itself.

Mutations move behind intent-revealing methods rather than the UI building sessions and swapping them in:

SetSessionName(name string) error
NewSession(cwd string) error
ForkSession(cwd, targetID string) error
SessionSystemPromptEntry(systemPrompt, fallbackModelID string) ([]byte, error)

GetTreeSession / SwitchTreeSession are dropped from AppController. App keeps both — cmd/root.go owns session lifecycle and legitimately needs the manager.

TreeNodeSelectedMsg now carries ParentID instead of the raw Entry any, removing the entry lookup + type assertion the fork path used to find its branch point. This also keeps internal/ui/core free of any internal/app dependency.

Notes on scope

  • Behaviour is unchanged. Filters, display strings, role colours, export/share filenames and fork semantics all match the previous implementation.
  • EntryKindUnknown means a new entry type degrades to the (unknown entry) fallback instead of breaking the build or rendering blank.
  • Two latent papercuts fixed in passing because the code moved anyway: the duplicated filename-sanitising closure is now sanitizeFileName, and snap.ID[:12] is now shortSessionID (the old GetSessionID()[:12] would panic on a short id).
  • MessageEntry.Text() moved the parts-JSON extraction into internal/session, where the format is defined — it was previously duplicated in the UI as extractTextFromParts.
  • SessionSummary was originally sketched for this step but moved to step (c): its only producer is ListSessions/ListAllSessions, so introducing it here would mean touching session_selector.go twice.

Remaining for step (c)

session_selector.go still calls session.ListSessions / ListAllSessions / DeleteSession directly, and /export + /share still do their own os.ReadFile/os.WriteFile. Moving those out removes the internal/session import from internal/ui entirely — that PR carries Fixes #101.

Testing

  • internal/app/session_view_test.go — snapshot fields, detachment (a held snapshot/projection is not mutated by later session writes), entry-kind projection, unknown-entry fallback, history filtering to messages only, ErrNoSession on every mutation without a session, system-prompt entry model resolution + fallback.
  • internal/ui/tree_selector_test.go — display text for every kind, truncation, untruncated user text for the editor, a full filter matrix across all 5 modes × 9 node shapes, tree flattening/depth, and cursor placement for /tree and /fork. No tree-selector tests existed before.
  • go build, go vet, gofmt -l, go test -race ./... all clean; golangci-lint run reports 0 issues.
  • Manual TUI smoke test against a seeded session: /session, /tree (all 5 filter modes, labels, active marker, nesting), /fork (branches at the parent, replays history, repopulates the editor), /name (read + write), /export (filename sanitising), /new. Empty stderr throughout.

Refs #101

Summary by CodeRabbit

  • New Features

    • Added point-in-time session snapshots plus a projected session tree with entry kinds, roles, labels, and message previews.
    • Updated session history rendering to use the active branch’s messages, and improved sharing/export with system-prompt details.
    • Enhanced session controls: create, rename, and fork using parent/target entry metadata.
    • Improved tree selection, filtering, indentation, and fork targeting based on the projected tree.
  • Bug Fixes

    • Fixed history/transcript updates when there is no active session or when history has no renderable text.
    • Improved robustness when message content is missing or malformed.
  • Tests

    • Added/updated coverage for session views, UI tree selection, and session history rendering.

The TUI reached through AppController.GetTreeSession() to grab the live
*session.TreeManager and drove it directly: reading the header, walking
the branch, appending entries, forking, and type-switching on the
concrete *session.MessageEntry / *session.ModelChangeEntry / ... entry
types to decide how to filter and render the tree. That made the session
storage schema part of the UI's contract — any change to how entries are
persisted broke rendering, and the UI could not be exercised or reused
without a real session on disk.

Session state is now projected into immutable value types owned by the
app layer:

  - SessionSnapshot carries session metadata (id, name, file path, cwd,
    created, leaf, counts, persisted).
  - TreeNodeView carries a node's id, parent, kind, role, text payload,
    label and children. EntryKind replaces the concrete-type switches,
    so an unrecognised entry degrades to a fallback instead of breaking.
  - SessionHistory returns the current branch as []message.Message, so
    transcript replay no longer decodes entries itself.

Mutations move behind intent-revealing methods (SetSessionName,
NewSession, ForkSession, SessionSystemPromptEntry) instead of the UI
constructing sessions and swapping them in via SwitchTreeSession.

GetTreeSession and SwitchTreeSession are dropped from AppController;
App keeps both for cmd/root.go wiring, which owns session lifecycle.

TreeNodeSelectedMsg now carries ParentID directly rather than the raw
entry, removing the entry lookup and type assertion the fork path did to
find where to branch from.

Behaviour is unchanged: filters, display strings, colours, export/share
filenames and fork semantics all match the previous implementation, now
covered by tests for the projections and for tree filtering/rendering.

The session selector still calls session.ListSessions/DeleteSession
directly; moving session discovery and the export/share file I/O out of
the UI is the next step.

Refs #101
@mark-iii-labs-huly

Copy link
Copy Markdown

Connected to Huly®: KIT-105

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c33bf73b-e2c2-48e8-9647-f3a97425ab2f

📥 Commits

Reviewing files that changed from the base of the PR and between bbf6fc7 and 61a94eb.

📒 Files selected for processing (4)
  • internal/app/session_view.go
  • internal/ui/model.go
  • internal/ui/model_test.go
  • internal/ui/render_session_history_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/ui/model_test.go

📝 Walkthrough

Walkthrough

The change moves session metadata, tree projection, history decoding, and session mutations into internal/app. UI components now consume value-based session APIs and projected tree nodes, with updated selection events, command flows, rendering, and tests.

Changes

Session state decoupling

Layer / File(s) Summary
App-layer session projections and mutations
internal/app/session_view.go, internal/session/entry.go
Adds session snapshots, projected tree nodes, entry classification, history extraction, lifecycle mutations, system-prompt serialization, and plain-text message projection.
Session API behavior coverage
internal/app/session_view_test.go
Tests no-session behavior, detached snapshots, tree projections, history filtering, mutations, and system-prompt model selection.
Controller contract and command integration
internal/ui/model.go, internal/ui/model_test.go, internal/ui/render_session_history_test.go
Replaces direct tree-manager access with app-layer APIs for tree, fork, new, name, export, share, history, and session information flows.
Projected tree selection and event payloads
internal/ui/core/events.go, internal/ui/tree_selector.go, internal/ui/tree_selector_test.go
Updates tree selection to use TreeNodeView, derives fork metadata from projected nodes, and tests display formatting, filtering, flattening, and cursor behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UIModel
  participant AppController
  participant TreeManager
  participant SessionFile
  UIModel->>AppController: request session snapshot or tree
  AppController->>TreeManager: read or mutate session state
  TreeManager->>SessionFile: persist session changes
  AppController-->>UIModel: return projected session data
Loading

Possibly related PRs

  • mark3labs/kit#58: Both changes modify session context and system-prompt handling in the share flow.
  • mark3labs/kit#98: Both changes modify session history rendering in internal/ui/model.go.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main refactor: exposing session state as value snapshots.
Linked Issues check ✅ Passed The PR implements the core decoupling work for [#101]: value snapshots, tree projections, and app-owned session mutations.
Out of Scope Changes check ✅ Passed The added helpers and tests appear directly tied to the session-state refactor, with no obvious unrelated scope creep.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/101-session-snapshot

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ezynda3

ezynda3 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/ui/model.go (1)

104-128: 📐 Maintainability & Code Quality | 🔵 Trivial

New NewSession/ForkSession/SessionSystemPromptEntry interface methods lack context.Context.

Per the app-layer implementations (session_view.go), NewSession and ForkSession call session.CreateTreeSession/tm.ForkToNewSession, which perform disk I/O to create/write session files. These are brand-new interface methods introduced in this PR, so this is the cheapest point to add a context.Context first parameter for cancellation/timeout support, per the repo's Go guideline.

As per coding guidelines, "Use Context as the first parameter for blocking operations in Go."

♻️ Proposed signature changes
-	NewSession(cwd string) error
+	NewSession(ctx context.Context, cwd string) error
 	// ForkSession creates a new session in cwd holding the history up to
 	// targetID and switches to it. Used by /fork and tree-node selection.
-	ForkSession(cwd, targetID string) error
+	ForkSession(ctx context.Context, cwd, targetID string) error
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/ui/model.go` around lines 104 - 128, Add context.Context as the
first parameter to NewSession, ForkSession, and SessionSystemPromptEntry in the
app-layer interface and corresponding implementations. Update all callers to
pass the active context, and propagate it through session_view.go to the
underlying session creation, fork, and serialization operations without changing
their existing behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/app/session_view.go`:
- Around line 228-229: Wrap the propagated errors in
internal/app/session_view.go at lines 228-229, 243-245, 259-261, and 283-283:
add contextual fmt.Errorf wrapping for failures from AppendSessionInfo,
CreateTreeSession, ForkToNewSession, and MarshalEntry respectively, preserving
the original errors with %w.

In `@internal/ui/model.go`:
- Around line 5813-5817: Update AppModel.renderSessionHistory so an empty
SessionHistory still clears the current message list and runs refreshContent,
marks layoutDirty, and sets pendingGotoBottom before returning or completing.
Preserve the existing history-rendering behavior for non-empty sessions,
ensuring SessionSelectedMsg and performFork cannot leave the previous transcript
visible.

---

Nitpick comments:
In `@internal/ui/model.go`:
- Around line 104-128: Add context.Context as the first parameter to NewSession,
ForkSession, and SessionSystemPromptEntry in the app-layer interface and
corresponding implementations. Update all callers to pass the active context,
and propagate it through session_view.go to the underlying session creation,
fork, and serialization operations without changing their existing behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 47e33840-0d11-455b-9a57-84c4fd85fea5

📥 Commits

Reviewing files that changed from the base of the PR and between b03a41b and bbf6fc7.

📒 Files selected for processing (8)
  • internal/app/session_view.go
  • internal/app/session_view_test.go
  • internal/session/entry.go
  • internal/ui/core/events.go
  • internal/ui/model.go
  • internal/ui/model_test.go
  • internal/ui/tree_selector.go
  • internal/ui/tree_selector_test.go

Comment thread internal/app/session_view.go Outdated
Comment thread internal/ui/model.go
- renderSessionHistory no longer returns early on an empty history, which
  left the previous conversation on screen. The old code returned when the
  branch had no *entries*; the refactor returned when it had no *messages*,
  so a branch holding only non-message entries stopped clearing. This is
  reachable from /retry and /undo (both pop the last user message and can
  empty the branch), and from forking or resuming into an empty session.
  The no-session guard is preserved via SessionSnapshot so an in-memory
  run's transcript is still left untouched.

- Wrap propagated errors in SetSessionName, NewSession, ForkSession and
  SessionSystemPromptEntry with fmt.Errorf/%w so failures are diagnosable
  across the app/UI boundary.

Skipped: adding context.Context to NewSession/ForkSession/
SessionSystemPromptEntry. Nothing beneath them accepts a context —
internal/session performs synchronous file I/O with no cancellation — and
no method on AppController takes one, so the parameter would be ignored
and would advertise cancellation that does not exist. Threading context
through the session layer is its own change.

Refs #101
@ezynda3

ezynda3 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — addressed the two actionable findings in 61a94eb.

Fixed 🟠 renderSessionHistory early return. Confirmed as a real regression introduced by this PR, and worth recording why: the pre-refactor code returned early when the branch had no entries, whereas the refactored version returned when it had no messages. A branch holding only non-message entries therefore stopped being cleared. Beyond the SessionSelectedMsg / performFork paths you flagged, it is also reachable from /retry and /undo — both call PopLastUserMessage and then renderSessionHistory, and popping the only user message empties the branch, so the failed turn would stay on screen contrary to the documented intent at the call site.

One deviation from the proposed diff: it clears unconditionally, which would drop the ts == nil guard the original had and blank the transcript when no session exists (reachable in in-memory runs). I kept that guard via SessionSnapshot and removed only the empty-history return, letting the existing tail handle refreshContent/layoutDirty/pendingGotoBottom rather than duplicating those three statements. Covered by four regression tests, verified to fail against the old code.

Fixed 🟡 error wrapping at all four sites, applied individually rather than from the concatenated suggestion block.

Skipped 🔵 context.Context on NewSession/ForkSession/SessionSystemPromptEntry. The guideline is right in general, but nothing beneath these accepts a context: internal/session has no context.Context in its API at all and does synchronous file I/O with no cancellation path. No method on AppController takes one either — the interface is uniformly context-free because the UI is single-goroutine BubbleTea. Adding it to three of ~30 methods would be an ignored parameter advertising cancellation that does not exist, which is worse than omitting it. Making it real means threading context through the session layer’s I/O, which is a separate change from this refactor.

@ezynda3
ezynda3 merged commit 37ed878 into master Jul 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant