diff --git a/chat/app.py b/chat/app.py index 1d47509..0acbf69 100644 --- a/chat/app.py +++ b/chat/app.py @@ -12,6 +12,7 @@ from chat.services.background import BackgroundWorker # Trigger handler registration: import chat.state.entities # noqa: F401 import chat.state.edges # noqa: F401 +import chat.state.manual_edit # noqa: F401 import chat.state.memory # noqa: F401 import chat.state.world # noqa: F401 diff --git a/chat/state/manual_edit.py b/chat/state/manual_edit.py new file mode 100644 index 0000000..ff6b247 --- /dev/null +++ b/chat/state/manual_edit.py @@ -0,0 +1,73 @@ +"""Handler for ``manual_edit`` events (T25, §6.4 final paragraph). + +A ``manual_edit`` event captures a user override of a projected field — its +payload snapshots both the prior value and the new value so any edit can +be reversed by emitting an inverse ``manual_edit`` later. This module +applies the new value to the appropriate target table; the snapshot of +``prior_value`` is taken by the route handler before this fires. + +Phase 1 covers three target kinds: +- ``edge_affinity`` and ``edge_trust`` — slider edits on a specific edge, + clamped to 0..100. +- ``memory_significance`` — dropdown edit, clamped to 0..3. +- ``memory_pov_summary`` — textarea edit (string). + +Other §6.4 editable fields (activity verb / attention / posture, edge +summary, knowledge_facts list manipulation) are deferred to Phase 1.5. + +Pin toggles intentionally use the existing ``memory_pin_changed`` event +(registered in :mod:`chat.state.memory`) rather than ``manual_edit`` so +the projection writes both ``pinned`` and ``auto_pinned`` atomically. +""" + +from __future__ import annotations + +from sqlite3 import Connection + +from chat.eventlog.log import Event +from chat.eventlog.projector import on + + +def _clamp(value: int, lo: int, hi: int) -> int: + return max(lo, min(hi, value)) + + +@on("manual_edit") +def _apply_manual_edit(conn: Connection, e: Event) -> None: + p = e.payload + kind = p["target_kind"] + target_id = p["target_id"] + new_value = p["new_value"] + + if kind == "edge_affinity": + conn.execute( + "UPDATE edges SET affinity = ? " + "WHERE source_id = ? AND target_id = ?", + ( + _clamp(int(new_value), 0, 100), + target_id["source_id"], + target_id["target_id"], + ), + ) + elif kind == "edge_trust": + conn.execute( + "UPDATE edges SET trust = ? " + "WHERE source_id = ? AND target_id = ?", + ( + _clamp(int(new_value), 0, 100), + target_id["source_id"], + target_id["target_id"], + ), + ) + elif kind == "memory_significance": + conn.execute( + "UPDATE memories SET significance = ? WHERE id = ?", + (_clamp(int(new_value), 0, 3), int(target_id)), + ) + elif kind == "memory_pov_summary": + conn.execute( + "UPDATE memories SET pov_summary = ? WHERE id = ?", + (str(new_value), int(target_id)), + ) + # Unknown target_kind: silently no-op for v1. Future kinds (activity + # fields, edge summary, knowledge_facts) extend the dispatch above. diff --git a/chat/templates/_drawer.html b/chat/templates/_drawer.html index 637a8d3..3ed1940 100644 --- a/chat/templates/_drawer.html +++ b/chat/templates/_drawer.html @@ -40,6 +40,18 @@
{{ host_bot.name }} → you

Affinity: {{ edge_b2y.affinity }}/100 · Trust: {{ edge_b2y.trust }}/100

+
+ + +
{% if edge_b2y.summary %}

{{ edge_b2y.summary }}

{% endif %} {% if edge_b2y.knowledge %}
Knowledge ({{ edge_b2y.knowledge|length }}) @@ -68,6 +80,12 @@
  • {{ ['·','•','★','★★'][m.significance|default(0)] }} {{ m.pov_summary }} +
    + + +
  • {% endfor %} @@ -84,6 +102,24 @@
  • {{ ['·','•','★','★★'][m.significance|default(0)] }} {{ m.pov_summary[:200] }}{% if m.pov_summary|length > 200 %}…{% endif %} +
    + + +
    +
    + + +
  • {% endfor %} diff --git a/chat/web/drawer.py b/chat/web/drawer.py index 6c5a1e9..580c956 100644 --- a/chat/web/drawer.py +++ b/chat/web/drawer.py @@ -1,21 +1,37 @@ -"""Read-only chat drawer (T24). +"""Chat drawer — read view (T24) and inline edits (T25). -Returns an HTML partial rendered into the chat shell's `