summaryrefslogtreecommitdiff
path: root/src/tui_event.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/tui_event.zig')
-rw-r--r--src/tui_event.zig50
1 files changed, 50 insertions, 0 deletions
diff --git a/src/tui_event.zig b/src/tui_event.zig
index 3f59ba2..ec860cb 100644
--- a/src/tui_event.zig
+++ b/src/tui_event.zig
@@ -238,6 +238,21 @@ pub const Event = struct {
}
};
+/// The writable payload field for an event type, or null when the event is
+/// observe-only. This is the single authority for the "override via mutable
+/// attribute" paradigm: a Lua handler assigning `ev.<field> = value` on one
+/// of these events records an override on the bus; the firer takes it after
+/// dispatch and applies it to the real underlying state (conversation
+/// block, turn text). Values are strings on the Lua side except
+/// `tool_call_complete.input`, which is assigned as a JSON-able table and
+/// serialized at write time.
+pub fn writableField(event_name: []const u8) ?[]const u8 {
+ if (std.mem.eql(u8, event_name, "tool_result")) return "output";
+ if (std.mem.eql(u8, event_name, "user_message")) return "text";
+ if (std.mem.eql(u8, event_name, "tool_call_complete")) return "input";
+ return null;
+}
+
// ===========================================================================
// EventBus
// ===========================================================================
@@ -259,6 +274,14 @@ pub const EventBus = struct {
handlers: std.StringHashMapUnmanaged(std.ArrayListUnmanaged(Handler)) = .empty,
/// Owned copies of the event-name keys (the map borrows these).
keys: std.ArrayListUnmanaged([]u8) = .empty,
+ /// Dispatch-scoped payload-field override written by a handler during
+ /// the current fire (the Lua bridge's writable event fields, e.g.
+ /// `ev.output = ...` on `tool_result`). One slot suffices: each
+ /// writable event type has exactly one writable field (see
+ /// `writableField`), `emit` clears the slot at entry for writable
+ /// events, and the firer takes it immediately after the fire returns.
+ /// Owned by `alloc`.
+ override_value: ?[]u8 = null,
pub fn init(alloc: std.mem.Allocator) EventBus {
return .{ .alloc = alloc };
@@ -270,6 +293,27 @@ pub const EventBus = struct {
self.handlers.deinit(self.alloc);
for (self.keys.items) |k| self.alloc.free(k);
self.keys.deinit(self.alloc);
+ if (self.override_value) |v| self.alloc.free(v);
+ }
+
+ /// Record a handler's override of the current event's writable field
+ /// (last writer wins — matches `setComponent` semantics). Readers of
+ /// that field during the same dispatch see this value, so handlers
+ /// chain in registration order.
+ pub fn setOverride(self: *EventBus, value: []const u8) !void {
+ const copy = try self.alloc.dupe(u8, value);
+ if (self.override_value) |old| self.alloc.free(old);
+ self.override_value = copy;
+ }
+
+ /// Take ownership of the pending override (null if no handler wrote
+ /// one). The caller frees the returned slice with `bus.alloc` and is
+ /// responsible for applying it to the real underlying state — an
+ /// untaken override is simply dropped at the next writable fire.
+ pub fn takeOverride(self: *EventBus) ?[]u8 {
+ const v = self.override_value;
+ self.override_value = null;
+ return v;
}
/// Register `handler` for `name`. Handlers fire in registration order on
@@ -303,6 +347,12 @@ pub const EventBus = struct {
/// concurrent `tool` boundaries each pass their own `event` (with their own
/// default) and get back their own chosen component.
pub fn emit(self: *EventBus, event: *Event) ?Component {
+ // Writable events start with a clean override slot, so a stale
+ // value from a fire nobody took (e.g. a replayed `user_message`)
+ // can never leak into this dispatch.
+ if (writableField(event.name) != null) {
+ if (self.takeOverride()) |stale| self.alloc.free(stale);
+ }
if (self.handlers.getPtr(event.name)) |list| {
for (list.items) |h| h.call(event);
}