feat(familiars): icon field on /api/v1/familiars (read + write)#151
Merged
Conversation
Adds an optional `icon` field to `~/.coven/familiars.toml` and serves
it both ways through the daemon HTTP API. Clients with a richer icon
system — CovenCave's glyph picker — can pick either an emoji literal
or a Phosphor icon name and persist it back to the user's TOML so the
pick survives across devices, reinstalls, and other Cave surfaces.
## Schema
[[familiar]]
id = "cody"
icon = "ph:lightning-fill" # Phosphor icon
[[familiar]]
id = "kitten"
icon = "🐈⬛" # emoji literal
The `ph:` prefix is the wire discriminator — clients with a Phosphor
renderer treat it as an icon name; anything else is rendered as a
literal glyph. Both shapes round-trip through one field, so the
daemon stays neutral about which kind a user wants.
## Read path
- `FamiliarEntry` (TOML reader) gets `icon: Option<String>`.
- `FamiliarDto` (API output) gets `pub icon: Option<String>` with
`skip_serializing_if = "Option::is_none"` so older clients keep
their existing payload.
- `read_familiars()` threads `icon` through and normalizes
whitespace-only values to `None` so a `" "` icon can't escape as
an empty glyph.
## Write path
- New `PUT /api/v1/familiars/{id}/icon` endpoint.
- Body shape: `{ "icon": "ph:cat-fill" }`, `{ "icon": "🐈" }`,
`{ "icon": null }`, or an empty body — null/empty/whitespace clear
the field, anything else updates or inserts it.
- Non-string, non-null `icon` values return 400 so a typo can't write
garbage into the file.
- Unknown familiar id returns 404 with `error.code = "familiar_not_found"`.
- New `cockpit_sources::write_familiar_icon()` does the edit via
`toml_edit` so existing field order, comments, and whitespace in the
user's TOML are preserved across writes.
- Writes are atomic: write to `.familiars.toml.tmp` in the same
directory, then `rename` over the target — POSIX-atomic on the
same filesystem and Windows-safe.
## Tests (12 new)
`cockpit_sources::tests`:
- `read_familiars_carries_icon_field_for_both_shapes` — TOML round-trip
for `ph:` names, emoji literals, whitespace, and absent.
- `familiar_dto_skips_serializing_absent_icon` — `serde_json` wire
shape check (absent icon is not in the JSON; present icon is).
- `write_familiar_icon_inserts_when_absent_and_preserves_other_fields`
— surrounding fields + comments survive the edit.
- `write_familiar_icon_replaces_existing_value`.
- `write_familiar_icon_clears_field_when_value_is_none`.
- `write_familiar_icon_treats_whitespace_as_clear`.
- `write_familiar_icon_returns_not_found_for_unknown_id`.
- `write_familiar_icon_leaves_no_tempfile_on_success` — atomic-rename
hygiene check.
`api::tests`:
- `put_familiar_icon_updates_existing_value`.
- `put_familiar_icon_inserts_when_absent` — also asserts the other
familiar's icon stays untouched.
- `put_familiar_icon_clears_when_null`.
- `put_familiar_icon_returns_404_for_unknown_id`.
- `put_familiar_icon_rejects_non_string_icon`.
- `put_familiar_icon_with_empty_body_clears`.
Pre-existing `read_familiars_parses_toml_entries` now also asserts
`icon` defaults to `None` on fixtures that don't set it.
## Dependency
Adds `toml_edit = "0.22"` so writes can preserve formatting and
comments. `toml = "1.1"` (read-only) stays.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: OpenCoven <noreply@opencoven.ai>
ffcc82a to
40490da
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The daemon now owns the full read+write story for a familiar's glyph.
Adds an optional
iconfield to~/.coven/familiars.toml, served atGET /api/v1/familiarsand writable via a newPUT /api/v1/familiars/{id}/icon. The Cave's glyph picker (OpenCoven/coven-cave#6, already merged) can stop writing only to Cave-local localStorage and start persisting picks back to the user's TOML — surviving across devices, reinstalls, and other Cave surfaces.Schema
The
ph:prefix is the wire discriminator. Both shapes round-trip through one field, so the daemon stays neutral about which kind a user wants.Endpoints
GET/api/v1/familiarsFamiliarDto[]now includesiconwhen present, omitted when absentPUT/api/v1/familiars/{id}/icon{ "icon": "ph:cat-fill" }PUT/api/v1/familiars/{id}/icon{ "icon": null }or{}or empty bodyPUT/api/v1/familiars/{id}/icon{ "icon": [1,2,3] }PUT/api/v1/familiars/{id}/iconerror.code = "familiar_not_found"Whitespace-only values normalize to
Noneso a" "icon can't escape as an empty glyph.Write safety
cockpit_sources::write_familiar_icon()does the edit viatoml_editso existing field order, comments, and surrounding whitespace are preserved..familiars.toml.tmpin the same directory, thenrenameover the target — POSIX-atomic on the same filesystem and Windows-safe. A crash mid-write can leave the temp file behind but never a half-writtenfamiliars.toml.Tests (12 new)
cockpit_sources::tests(6):read_familiars_carries_icon_field_for_both_shapes— TOML round-trip forph:names, emoji literals, whitespace, absentfamiliar_dto_skips_serializing_absent_icon— wire shape contractwrite_familiar_icon_inserts_when_absent_and_preserves_other_fields— surrounding fields + comments survivewrite_familiar_icon_replaces_existing_valuewrite_familiar_icon_clears_field_when_value_is_nonewrite_familiar_icon_treats_whitespace_as_clearwrite_familiar_icon_returns_not_found_for_unknown_idwrite_familiar_icon_leaves_no_tempfile_on_success— atomic-rename hygieneapi::tests(6):put_familiar_icon_updates_existing_valueput_familiar_icon_inserts_when_absent— also asserts the other familiar's icon stays untouchedput_familiar_icon_clears_when_nullput_familiar_icon_returns_404_for_unknown_idput_familiar_icon_rejects_non_string_iconput_familiar_icon_with_empty_body_clearsPre-existing
read_familiars_parses_toml_entriesextended to also asserticon = Noneon fixtures that don't set it.Dependency
Adds
toml_edit = "0.22"(withserdefeature) for write-side formatting preservation.toml = "1.1"(read-only) stays.Test plan
cargo fmt --check -p coven-cliclean (the first CI run failed on this — fixed)cargo test -p coven-cliall green, all 12 new tests passcargo check -p coven-clicleanicon = "ph:cat-fill"on a familiar in local~/.coven/familiars.toml, restart the daemon, hitGET /api/v1/familiars, confirm the field appearsPUT—curl -X PUT --unix-socket "$COVEN_SOCK" http://x/api/v1/familiars/cody/icon -d '{"icon":"ph:cat-fill"}'— confirm the TOML is updated and surrounding formatting is preservedFollow-up
Cave-side daemon-writer: extend
cave-glyph-overrides.tsto alsoPUTto this endpoint when the user picks a glyph (with localStorage as a fast-path mirror that stays valid if the daemon is offline). One small PR oncoven-cave.🤖 Generated with Claude Code