diff options
Diffstat (limited to 'src/tui_event.zig')
| -rw-r--r-- | src/tui_event.zig | 50 |
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); } |
