Skip to content

Edit any session by index: update_session_at#7

Open
yncyrydybyl wants to merge 19 commits into
faffhub:mainfrom
yncyrydybyl:feat/update-session-at
Open

Edit any session by index: update_session_at#7
yncyrydybyl wants to merge 19 commits into
faffhub:mainfrom
yncyrydybyl:feat/update-session-at

Conversation

@yncyrydybyl
Copy link
Copy Markdown

Summary

Extends the edit API beyond the currently-active session. Callers can now edit the semantic fields of any session in any log by (date, index), preserving start/end times and any reflection.

First consumer is yncyrydybyl/faff-tui, whose edit form now targets any session by cursor position on the dashboard rather than only the currently-active one.

What's new

Core (core/src/)

  • Log::update_session_at(index, new_session) — immutable index-based timeline replacement with bounds checking.
  • LogError::IndexOutOfRange { index, len } — new variant.
  • LogManager::update_session_at(date, index, title, role, impact, mode, subject, trackers, note) — resolves the plan for the target date (not today), validates trackers, writes the updated log. Uses the existing Session::with_fields helper so times and reflection survive untouched.

Bindings (bindings-uniffi/)

  • FaffWorkspace::update_session_at exposed via UDL + lib.rs. Takes date (YYYY-MM-DD string) + u32 index.
  • Errors map through anyhow to FaffError::Other, consistent with update_active_session.

Tests

  • test_update_session_at_replaces_fields — field replacement preserves start/end, leaves other sessions untouched.
  • test_update_session_at_invalid_index — out-of-range index errors cleanly.
  • test_update_session_at_preserves_reflection — reflection survives semantic edits.
  • All 169 existing tests still pass.

Test plan

  • cargo test -p faff-core — 169/169 pass including 3 new
  • cargo build -p faff-core-uniffi — clean debug + release
  • make regen in faff-tui picks up UpdateSessionAt in generated Go bindings
  • End-to-end: faff-tui dashboard with j/k cursor + e key successfully edits a past session and persists to the TOML file with time fields preserved

🤖 Generated with Claude Code

lampholder and others added 19 commits December 7, 2025 11:10
The workspace tests were catching RuntimeError but the new error type
is UninitializedLedgerError (introduced in commit 263a93e). Update the
exception handling to match the current API.
faff-core has had Python (PyO3) and WASM (wasm-bindgen) bindings, but no
ergonomic path for other languages. This adds a third workspace member
that exposes the workspace through UniFFI, enabling uniffi-bindgen-go,
uniffi-bindgen-cs, uniffi-bindgen-swift, etc. to consume faff-core
in-process without shelling out to faff-cli.

The first foreign consumer is faff-tui (a Bubbletea TUI in Go), which
imports the generated bindings and calls FaffWorkspace methods directly.

## Surface

The UDL exposes:

- FaffWorkspace constructor that wraps Workspace::new() on a tokio runtime
  owned by the binding (one runtime per workspace, vs bindings-python's
  per-call Runtime::new())
- today() / timezone() — workspace metadata
- get_log(date) — read a daily log as a flat dictionary; time fields are
  RFC3339 strings to avoid teaching UniFFI about chrono::DateTime<Tz>
- start_session(...) / stop_current_session() — write paths
- get_roles/impacts/modes/subjects/trackers/session_hints(date) —
  vocabulary queries from PlanManager
- recent_prototypes(days) — a binding-side convenience that walks the
  last N days of logs and returns deduplicated session shapes ranked by
  usage count then recency. This replaces the old plan-level Intent
  concept (removed in test_plan_deserialization_ignores_intents) with
  a history-derived picker source. Lives here rather than in core
  because it's a UI-shaped concern.

## Pinned versions

- uniffi-rs 0.31.0 (workspace dep)
- targets uniffi-bindgen-go v0.7.0+v0.31.0 on the consumer side

## Build

  cargo build -p faff-core-uniffi          (debug, ~70MB cdylib)
  cargo build --release -p faff-core-uniffi (release, ~7.6MB cdylib)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two small immutable builders that LogManager will use to support
in-flight session edits in the next commit:

- Session::with_fields(title, role, impact, mode, subject, trackers, note)
  mirrors with_end and with_reflection — replaces the semantic field
  cluster wholesale, preserves start/end and any reflection.

- Log::update_active_session(new_session) returns a new Log with the
  active (last, end=None) session replaced. Errors if no session is
  currently active.

- LogError::NoActiveSession added for the new failure mode.

No behavior change to existing call paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the manager-level method that wires Log::update_active_session into
the workspace plumbing:

- Validates that any tracker passed is present in today's plan, mirroring
  start_session's existing tracker check.
- Loads today's log, finds the active session, applies with_fields, and
  persists via the standard write_log path.
- Errors cleanly if there's no active session (either empty timeline or
  the last session is closed).

Six new tests in mod tests:
- replaces_fields:               happy path, all fields swap
- errors_without_active:         empty log → error
- errors_when_last_session_closed: closed timeline → error
- rejects_unknown_trackers:      validation guard
- preserves_other_sessions:      neighbor session untouched
- preserves_reflection:          reflection_score/text survive an edit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the Stage 2 write path needed by faff-tui's edit form:

- New UDL method on FaffWorkspace, mirroring the Rust signature exactly:
  update_active_session(title?, role?, impact?, mode?, subject?, trackers, note?)
- New FaffError variant: NoActiveSession. Mapped from anyhow on the
  best-effort string-match path that the existing Other variant uses.
- The wrapper goes through the binding's owned tokio runtime — same
  pattern as start_session / stop_current_session.

The first foreign consumer (yncyrydybyl/faff-tui) calls UpdateActiveSession
from a 7-field edit form opened with `e` on the dashboard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extends the edit API beyond the currently-active session. Callers can now
edit the semantic fields of any session in any log by (date, index),
preserving start/end times and any reflection. Mirrors the existing
update_active_session pathway and shares its tracker validation.

Core
----
- Session::with_fields already preserved times, reused as-is.
- Log::update_session_at(index, new_session) — immutable index-based
  timeline replacement with bounds checking.
- LogError::IndexOutOfRange { index, len } — new variant for the above.
- LogManager::update_session_at(date, index, fields...) — resolves the
  plan for the target date (not today), validates trackers, writes the
  updated log.

Bindings (UniFFI)
-----------------
- FaffWorkspace::update_session_at exposed via faff_core.udl and
  bindings-uniffi/src/lib.rs. Takes date (YYYY-MM-DD) + u32 index.
- Errors map through anyhow to FaffError::Other, same as
  update_active_session.

Tests
-----
- test_update_session_at_replaces_fields — verifies field replacement
  preserves start, end, and leaves other sessions untouched.
- test_update_session_at_invalid_index — out-of-range index errors
  with "out of range" in the message.
- test_update_session_at_preserves_reflection — reflection survives
  semantic edits.
- All 169 existing tests still pass.

First consumer is faff-tui's edit form, which now targets any session
by cursor position rather than only the active one.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…_session_at

Two gaps identified in test-expert review:

1. Tracker validation rejection was untested — the check exists in
   update_session_at (mirrors update_active_session) but no test
   exercised the failure path. Added a test that passes "bogus:tracker"
   and asserts the error contains "not found".

2. Cross-date editing was untested — all previous tests used ws.today(),
   but the whole point of update_session_at vs update_active_session is
   editing past sessions. Added a test that writes a log for yesterday,
   edits a session in it, and asserts: the edit lands in yesterday's
   log (times preserved), and today's log stays empty.

5/5 update_session_at tests now pass (3 existing + 2 new).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

2 participants