const std = @import("std");const activity_status = @import("../output/activity_status.zig");const app_session_runtime = @import("app_session_runtime.zig");const debug_trace = @import("../shared/debug_trace.zig");const diff_mod = @import("../output/diff.zig");const file_mutation_contract = @import("../tooling/file_mutation_contract.zig");const command_output_content = @import("../tooling/command_output_content.zig");const io_mod = @import("../shared/io.zig");const permission_request = @import("../permissions/permission_request.zig");const task_helpers = @import("../tasks/task_helpers.zig");const text_utils = @import("../shared/text_utils.zig");const types = @import("../shared/types.zig");const worker_runtime = @import("../agent/worker_runtime.zig");const assistant_presentation = @import("../agent/assistant_presentation.zig");const assistant_pacer = @import("../../ui/assistant/pacer.zig");const ui_render = @import("../../ui/render.zig");const activity_runtime = @import("../output/activity_runtime.zig");const worker_status = @import("../output/worker_status.zig");const core_input_runtime = @import("../input/runtime.zig");const render_input = @import("../../ui/footer/render_input.zig");const interaction_state = @import("../../ui/footer/interaction_state.zig");const render_request = @import("../../ui/render_request.zig");const transcript_runtime = @import("../../ui/transcript/runtime.zig"); const QueuePreview = worker_runtime.QueuePreview;const WorkerEvent = worker_runtime.WorkerEvent;const InputRuntime = core_input_runtime.Runtime; const CancelledEventAdmission = enum { admit, drop, defer_until_closure,}; const reset_style = ui_render.reset_style; test { _ = activity_runtime;} fn allocDimmedTranscriptLine(alloc: std.mem.Allocator, text: []const u8) ![]u8 { if (std.mem.endsWith(u8, text, "\n")) { return std.fmt.allocPrint(alloc, "{s}{s}{s}\n", .{ ui_render.dim_style, text[0 .. text.len - 1], reset_style }); } return std.fmt.allocPrint(alloc, "{s}{s}{s}", .{ ui_render.dim_style, text, reset_style });} fn discardTable(_: *anyopaque, table: assistant_presentation.TablePayload) !void { var owned = table; owned.deinit(std.heap.c_allocator);} fn discardCodeBlock(_: *anyopaque, block: assistant_presentation.CodeBlockPayload) !void { var owned = block; owned.deinit(std.heap.c_allocator);} fn discardThematicRule(_: *anyopaque) !void {} pub const WorkerEventHandlers = struct { ctx: *anyopaque, tool_lifecycle: activity_runtime.LifecyclePresenter, write_user_prompt: *const fn (*anyopaque, types.UserTurn) anyerror!void, write_user_prompt_with_skill_bindings: ?*const fn (*anyopaque, types.UserTurn, []const worker_runtime.SkillBinding, []const worker_runtime.SkillDisplaySpan) anyerror!void = null, append_text: *const fn (*anyopaque, []const u8) anyerror!void, append_table: *const fn (*anyopaque, assistant_presentation.TablePayload) anyerror!void = discardTable, append_code_block: *const fn (*anyopaque, assistant_presentation.CodeBlockPayload) anyerror!void = discardCodeBlock, append_thematic_rule: *const fn (*anyopaque) anyerror!void = discardThematicRule, drain_assistant_text: *const fn (*anyopaque) anyerror!AssistantTextDrainResult, open_model_picker: *const fn (*anyopaque) anyerror!void, semantic_notice: *const fn (*anyopaque, types.SemanticNotice) anyerror!void, command_output: *const fn (*anyopaque, ?types.ToolLifecycleId, command_output_content.Stream, []const u8) anyerror!void, command_output_complete: *const fn (*anyopaque, ?types.ToolLifecycleId) anyerror!void, diff_block: *const fn (*anyopaque, diff_mod.DiffEntryPayload) anyerror!void, append_history_turn: *const fn (*anyopaque, types.FinishedPrompt) anyerror!void, session_grant: *const fn (*anyopaque, types.PermissionGrant) anyerror!void, error_text: *const fn (*anyopaque, types.SemanticNotice) anyerror!void,}; pub const AssistantTextDrainResult = enum { drained, blocked,}; const DetachedWorkerEventBatch = struct { alloc: std.mem.Allocator, events: std.ArrayList(WorkerEvent), next_unvisited: usize = 0, fn init( alloc: std.mem.Allocator, events: std.ArrayList(WorkerEvent), ) DetachedWorkerEventBatch { return .{ .alloc = alloc, .events = events, }; } fn claim(self: *DetachedWorkerEventBatch) ?WorkerEvent { if (self.next_unvisited >= self.events.items.len) return null; const event = self.events.items[self.next_unvisited]; self.next_unvisited += 1; return event; } fn claimedAndRemaining(self: *const DetachedWorkerEventBatch) []const WorkerEvent { std.debug.assert(self.next_unvisited > 0); return self.events.items[self.next_unvisited - 1 ..]; } fn markClaimedAndRemainingTransferred(self: *DetachedWorkerEventBatch) void { self.next_unvisited = self.events.items.len; } fn deinit(self: *DetachedWorkerEventBatch) void { while (self.next_unvisited < self.events.items.len) : (self.next_unvisited += 1) { worker_runtime.freeWorkerEvent(self.alloc, self.events.items[self.next_unvisited]); } self.events.deinit(self.alloc); self.* = undefined; }}; fn batchContainsInterruptedClosure( events: []const WorkerEvent, id: types.ToolLifecycleId,) bool { for (events) |event| { const lifecycle = switch (event) { .tool_lifecycle => |value| value, else => continue, }; switch (lifecycle) { .terminal => |terminal| { if (terminal.outcome.kind == .cancelled and terminal.id.turn_id == id.turn_id and std.mem.eql(u8, terminal.id.call_id, id.call_id)) return true; }, .turn_finished => |finished| { if (finished.outcome == .interrupted and finished.turn_id == id.turn_id) return true; }, .provisional, .authoritative_started, .progress => {}, } } return false;} fn batchSegmentEndsInterrupted(events: []const WorkerEvent) bool { for (events, 0..) |event, index| { if (index > 0) switch (event) { .begin_prompt, .begin_prompt_with_skill_bindings => return false, else => {}, }; const lifecycle = switch (event) { .tool_lifecycle => |value| value, else => continue, }; switch (lifecycle) { .turn_finished => |finished| if (finished.outcome == .interrupted) return true, .provisional, .authoritative_started, .progress, .terminal => {}, } } return false;} pub fn Runtime(comptime App: type) type { return struct { fn cancelledToolStartAdmission( presenter: activity_runtime.LifecyclePresenter, id: types.ToolLifecycleId, batch_events: []const WorkerEvent, ) CancelledEventAdmission { if (batchContainsInterruptedClosure(batch_events, id)) return .admit; if (id.turn_id <= presenter.snapshot().finalized_turn_watermark) return .drop; return .defer_until_closure; } fn cancelledEventAdmission( presenter: activity_runtime.LifecyclePresenter, event: WorkerEvent, batch_events: []const WorkerEvent, ) CancelledEventAdmission { return switch (event) { .assistant_presentation, .open_model_picker, .semantic_notice, .command_output, .turn_token_update, .tool_payload_started, .diff_block, => .drop, .route_recovery_status => |status| if (status.action == .paused) .admit else .drop, .command_output_complete => |lifecycle_id| if (cancelledCommandCompletionMatches(presenter, lifecycle_id)) .admit else .drop, .tool_lifecycle => |lifecycle| switch (lifecycle) { .provisional => |started| cancelledToolStartAdmission(presenter, started.id, batch_events), .authoritative_started => |started| cancelledToolStartAdmission(presenter, started.id, batch_events), .progress => .drop, .terminal => |terminal| if (presenter.record(terminal.id) == null) .drop else .admit, .turn_finished => .admit, }, .begin_prompt, .begin_prompt_with_skill_bindings, .append_user_feedback, .notification, .question_requested, .clear_route_recovery_status, .api_status_text, .finish_prompt, .session_grant, .error_text, => .admit, }; } fn cancelledCommandCompletionMatches( presenter: activity_runtime.LifecyclePresenter, lifecycle_id: ?types.ToolLifecycleId, ) bool { const id = lifecycle_id orelse return false; const record = presenter.record(id) orelse return false; return record.phase == .terminal and record.activity_kind == .command; } pub fn authorizeInteractiveAdmission(app: *App) !bool { if (comptime @hasDecl(@TypeOf(app.worker), "interactiveAdmissionSnapshot")) { switch (app.worker.interactiveAdmissionSnapshot()) { .open => return true, .stopped => return false, .finalization_failed => { if (comptime @hasField(App, "should_exit")) { app.should_exit = true; } return error.TurnFinalizationDeliveryFailed; }, } } return true; } pub fn queuePreview(app: *App) QueuePreview { return app.worker.queuePreview(); } pub fn syncQueuedPromptModel(app: *App, model: []const u8) !void { try app.worker.syncQueuedPromptModel(std.heap.c_allocator, model); } pub fn syncQueuedPromptPermissionSnapshot(app: *App, snapshot: worker_runtime.PermissionSnapshot) void { app.worker.syncQueuedPromptPermissionSnapshot(snapshot); } pub fn syncQueuedPromptPermissionState( app: *App, snapshot: worker_runtime.PermissionSnapshot, ) !void { try app.worker.syncQueuedPromptPermissionState( std.heap.c_allocator, app.permission_engine.grants.items, snapshot, ); } pub fn propagateHistoryTurn(app: *App, turn: types.HistoryTurn, max_history_turns: usize) !void { if (comptime @hasDecl(App, "propagateHistoryTurn")) { try app.propagateHistoryTurn(turn); return; } try app.worker.propagateHistoryTurn(std.heap.c_allocator, turn, max_history_turns); } pub fn propagateGrant(app: *App, tool_name: []const u8, target_path: []const u8) !void { if (comptime @hasDecl(App, "propagateGrant")) { try app.propagateGrant(tool_name, target_path); return; } try app.worker.propagateGrant(std.heap.c_allocator, tool_name, target_path); } pub fn pushEvent(app: *App, event: WorkerEvent) !void { if (comptime @hasDecl(App, "pushWorkerEvent")) { const owned = try worker_runtime.dupeWorkerEvent(std.heap.c_allocator, event); errdefer worker_runtime.freeWorkerEvent(std.heap.c_allocator, owned); try app.pushWorkerEvent(owned); return; } try app.worker.pushEvent(std.heap.c_allocator, event); } pub fn pushOwnedEvent(app: *App, event: WorkerEvent) !void { if (comptime @hasDecl(App, "pushWorkerEvent")) { try app.pushWorkerEvent(event); return; } try app.worker.pushOwnedEvent(std.heap.c_allocator, event); } pub fn pushText(app: *App, text: []const u8) !void { if (comptime @hasDecl(App, "pushText")) { try app.pushText(text); } else { try pushEvent(app, .{ .assistant_presentation = .{ .text = @constCast(text), } }); } debug_trace.eventf( "worker", "assistant_chunk_received", .{}, "chunk_bytes={d}", .{text.len}, ); } pub fn pushTable(app: *App, table: assistant_presentation.TablePayload) !void { try pushEvent(app, .{ .assistant_presentation = .{ .table = table } }); } pub fn pushCodeBlock(app: *App, block: assistant_presentation.CodeBlockPayload) !void { try pushEvent(app, .{ .assistant_presentation = .{ .code_block = block } }); } pub fn pushThematicRule(app: *App) !void { try pushEvent(app, .{ .assistant_presentation = .thematic_rule }); } pub fn pushCommandOutput( app: *App, lifecycle_id: ?types.ToolLifecycleId, stream: command_output_content.Stream, chunk: []const u8, ) !void { if (chunk.len == 0) return; if (comptime @hasDecl(App, "pushCommandOutputWithLifecycle")) { try app.pushCommandOutputWithLifecycle(lifecycle_id, stream, chunk); return; } if (comptime @hasDecl(App, "pushCommandOutput")) { try app.pushCommandOutput(stream, chunk); return; } try pushEvent(app, .{ .command_output = .{ .lifecycle_id = lifecycle_id, .stream = stream, .text = @constCast(chunk), } }); } pub fn pushCommandOutputComplete(app: *App, lifecycle_id: ?types.ToolLifecycleId) !void { try pushEvent(app, .{ .command_output_complete = lifecycle_id }); } pub fn pushToolLifecycle( app: *App, event: types.ToolLifecycleEvent, ) !void { try pushEvent(app, .{ .tool_lifecycle = event }); } pub fn pushWebSearchProgress(app: *App, call_id: []const u8, progress: types.WebSearchProgress) !void { const turn_id = app.worker.activeTurnId(); if (turn_id == 0 or call_id.len == 0) { debug_trace.logf( "ui_activity", "web search progress missing active lifecycle identity", .{}, ); return; } const label = try formatWebSearchProgress(std.heap.c_allocator, progress); defer std.heap.c_allocator.free(label); try pushToolLifecycle(app, .{ .progress = .{ .id = .{ .turn_id = turn_id, .call_id = call_id }, .text = label, } }); } pub fn pushWebFetchProgress(app: *App, call_id: []const u8, progress: types.WebFetchProgress) !void { const turn_id = app.worker.activeTurnId(); if (turn_id == 0 or call_id.len == 0) { debug_trace.logf( "ui_activity", "web fetch progress missing active lifecycle identity", .{}, ); return; } const label = try formatWebFetchProgress(std.heap.c_allocator, progress); defer std.heap.c_allocator.free(label); try pushToolLifecycle(app, .{ .progress = .{ .id = .{ .turn_id = turn_id, .call_id = call_id }, .text = label, } }); } pub fn pushSemanticNotice(app: *App, notice: types.SemanticNotice) !void { try pushEvent(app, .{ .semantic_notice = notice }); } pub fn pushDiffBlock(app: *App, payload: diff_mod.DiffEntryPayload) !void { try pushOwnedEvent(app, .{ .diff_block = payload }); } pub fn syncState( app: *App, presenter: activity_runtime.LifecyclePresenter, ) void { const snapshot = app.worker.snapshotState(app.alloc) catch return; defer snapshot.deinit(app.alloc); const was_approval_active = app.approval_prompt.isActive(); const worker_pending_request = if (snapshot.pending_permission_request) |*request| request.view() else null; const child_pending_request: ?permission_request.PermissionRequest = if (worker_pending_request == null) blk: { if (comptime @hasField(App, "subagents")) { if (comptime @hasDecl(@TypeOf(app.subagents), "mainApprovalRequest")) { break :blk app.subagents.mainApprovalRequest(); } } break :blk null; } else null; if (comptime @hasField(App, "subagents")) { if (comptime @hasDecl(@TypeOf(app.subagents), "markMainApprovalPresented")) { app.subagents.markMainApprovalPresented(child_pending_request != null); } } const pending_request = worker_pending_request orelse child_pending_request; const management_active = if (comptime @hasField( @TypeOf(app.approval_prompt), "rule_management", )) app.approval_prompt.rule_management != null else false; const approval_changed = if (management_active and pending_request == null) false else app.approval_prompt.syncRequest(app.alloc, pending_request) catch false; const review_changed = if (comptime @hasDecl(@TypeOf(app.approval_prompt), "syncReview")) blk: { const review = if (comptime @hasField(@TypeOf(snapshot), "pending_permission_review")) if (snapshot.pending_permission_review) |pending| if (pending_request) |request| if (pending.request_id == request.id) pending.review else null else null else null else null; break :blk app.approval_prompt.syncReview(review); } else false; if (comptime @hasField(App, "approval_screen")) { const file_review_changed = review_changed and (pending_request == null or pending_request.?.file != null); if (approval_changed or file_review_changed) { app.approval_screen.clear(); } } if (approval_changed or review_changed) { app.shell.render_requests.request(.modal); } if (!was_approval_active and app.approval_prompt.isActive()) { if (comptime @hasDecl(App, "dispatchAttentionRequired")) { app.dispatchAttentionRequired(snapshot.active_turn_id, .permission); } } if (app.question_prompt.isActive()) { if (app.worker.snapshotPendingQuestionBatch(app.alloc) catch return) |question_snapshot| { question_snapshot.deinit(app.alloc); } else { app.question_prompt.discard(app.alloc, "worker_cleared"); app.shell.render_requests.request(.modal); } } const modal_active = app.approval_prompt.isActive() or app.question_prompt.isActive(); const queue_review_active = if (comptime @hasField(@TypeOf(snapshot), "queue_review_reason")) snapshot.queue_review_reason != null else false; const worker_events_pending = if (comptime @hasField(@TypeOf(snapshot), "pending_event_count")) snapshot.pending_event_count > 0 else false; const visible_worker_active = app.stream.active and !snapshot.cancel_requested and (snapshot.processing or worker_events_pending orLines 1–500 of 4391. Load more
const std = @import("std"); const activity_status = @import("../output/activity_status.zig"); const app_session_runtime = @import("app_session_runtime.zig"); const debug_trace = @import("../shared/debug_trace.zig"); const diff_mod = @import("../output/diff.zig"); const file_mutation_contract = @import("../tooling/file_mutation_contract.zig"); const command_output_content = @import("../tooling/command_output_content.zig"); const io_mod = @import("../shared/io.zig"); const permission_request = @import("../permissions/permission_request.zig"); const task_helpers = @import("../tasks/task_helpers.zig"); const text_utils = @import("../shared/text_utils.zig"); const types = @import("../shared/types.zig"); const worker_runtime = @import("../agent/worker_runtime.zig"); const assistant_presentation = @import("../agent/assistant_presentation.zig"); const assistant_pacer = @import("../../ui/assistant/pacer.zig"); const ui_render = @import("../../ui/render.zig"); const activity_runtime = @import("../output/activity_runtime.zig"); const worker_status = @import("../output/worker_status.zig"); const core_input_runtime = @import("../input/runtime.zig"); const render_input = @import("../../ui/footer/render_input.zig"); const interaction_state = @import("../../ui/footer/interaction_state.zig"); const render_request = @import("../../ui/render_request.zig"); const transcript_runtime = @import("../../ui/transcript/runtime.zig"); const QueuePreview = worker_runtime.QueuePreview; const WorkerEvent = worker_runtime.WorkerEvent; const InputRuntime = core_input_runtime.Runtime; const CancelledEventAdmission = enum { admit, drop, defer_until_closure, }; const reset_style = ui_render.reset_style; test { _ = activity_runtime; } fn allocDimmedTranscriptLine(alloc: std.mem.Allocator, text: []const u8) ![]u8 { if (std.mem.endsWith(u8, text, "\n")) { return std.fmt.allocPrint(alloc, "{s}{s}{s}\n", .{ ui_render.dim_style, text[0 .. text.len - 1], reset_style }); } return std.fmt.allocPrint(alloc, "{s}{s}{s}", .{ ui_render.dim_style, text, reset_style }); } fn discardTable(_: *anyopaque, table: assistant_presentation.TablePayload) !void { var owned = table; owned.deinit(std.heap.c_allocator); } fn discardCodeBlock(_: *anyopaque, block: assistant_presentation.CodeBlockPayload) !void { var owned = block; owned.deinit(std.heap.c_allocator); } fn discardThematicRule(_: *anyopaque) !void {} pub const WorkerEventHandlers = struct { ctx: *anyopaque, tool_lifecycle: activity_runtime.LifecyclePresenter, write_user_prompt: *const fn (*anyopaque, types.UserTurn) anyerror!void, write_user_prompt_with_skill_bindings: ?*const fn (*anyopaque, types.UserTurn, []const worker_runtime.SkillBinding, []const worker_runtime.SkillDisplaySpan) anyerror!void = null, append_text: *const fn (*anyopaque, []const u8) anyerror!void, append_table: *const fn (*anyopaque, assistant_presentation.TablePayload) anyerror!void = discardTable, append_code_block: *const fn (*anyopaque, assistant_presentation.CodeBlockPayload) anyerror!void = discardCodeBlock, append_thematic_rule: *const fn (*anyopaque) anyerror!void = discardThematicRule, drain_assistant_text: *const fn (*anyopaque) anyerror!AssistantTextDrainResult, open_model_picker: *const fn (*anyopaque) anyerror!void, semantic_notice: *const fn (*anyopaque, types.SemanticNotice) anyerror!void, command_output: *const fn (*anyopaque, ?types.ToolLifecycleId, command_output_content.Stream, []const u8) anyerror!void, command_output_complete: *const fn (*anyopaque, ?types.ToolLifecycleId) anyerror!void, diff_block: *const fn (*anyopaque, diff_mod.DiffEntryPayload) anyerror!void, append_history_turn: *const fn (*anyopaque, types.FinishedPrompt) anyerror!void, session_grant: *const fn (*anyopaque, types.PermissionGrant) anyerror!void, error_text: *const fn (*anyopaque, types.SemanticNotice) anyerror!void, }; pub const AssistantTextDrainResult = enum { drained, blocked, }; const DetachedWorkerEventBatch = struct { alloc: std.mem.Allocator, events: std.ArrayList(WorkerEvent), next_unvisited: usize = 0, fn init( alloc: std.mem.Allocator, events: std.ArrayList(WorkerEvent), ) DetachedWorkerEventBatch { return .{ .alloc = alloc, .events = events, }; } fn claim(self: *DetachedWorkerEventBatch) ?WorkerEvent { if (self.next_unvisited >= self.events.items.len) return null; const event = self.events.items[self.next_unvisited]; self.next_unvisited += 1; return event; } fn claimedAndRemaining(self: *const DetachedWorkerEventBatch) []const WorkerEvent { std.debug.assert(self.next_unvisited > 0); return self.events.items[self.next_unvisited - 1 ..]; } fn markClaimedAndRemainingTransferred(self: *DetachedWorkerEventBatch) void { self.next_unvisited = self.events.items.len; } fn deinit(self: *DetachedWorkerEventBatch) void { while (self.next_unvisited < self.events.items.len) : (self.next_unvisited += 1) { worker_runtime.freeWorkerEvent(self.alloc, self.events.items[self.next_unvisited]); } self.events.deinit(self.alloc); self.* = undefined; } }; fn batchContainsInterruptedClosure( events: []const WorkerEvent, id: types.ToolLifecycleId, ) bool { for (events) |event| { const lifecycle = switch (event) { .tool_lifecycle => |value| value, else => continue, }; switch (lifecycle) { .terminal => |terminal| { if (terminal.outcome.kind == .cancelled and terminal.id.turn_id == id.turn_id and std.mem.eql(u8, terminal.id.call_id, id.call_id)) return true; }, .turn_finished => |finished| { if (finished.outcome == .interrupted and finished.turn_id == id.turn_id) return true; }, .provisional, .authoritative_started, .progress => {}, } } return false; } fn batchSegmentEndsInterrupted(events: []const WorkerEvent) bool { for (events, 0..) |event, index| { if (index > 0) switch (event) { .begin_prompt, .begin_prompt_with_skill_bindings => return false, else => {}, }; const lifecycle = switch (event) { .tool_lifecycle => |value| value, else => continue, }; switch (lifecycle) { .turn_finished => |finished| if (finished.outcome == .interrupted) return true, .provisional, .authoritative_started, .progress, .terminal => {}, } } return false; } pub fn Runtime(comptime App: type) type { return struct { fn cancelledToolStartAdmission( presenter: activity_runtime.LifecyclePresenter, id: types.ToolLifecycleId, batch_events: []const WorkerEvent, ) CancelledEventAdmission { if (batchContainsInterruptedClosure(batch_events, id)) return .admit; if (id.turn_id <= presenter.snapshot().finalized_turn_watermark) return .drop; return .defer_until_closure; } fn cancelledEventAdmission( presenter: activity_runtime.LifecyclePresenter, event: WorkerEvent, batch_events: []const WorkerEvent, ) CancelledEventAdmission { return switch (event) { .assistant_presentation, .open_model_picker, .semantic_notice, .command_output, .turn_token_update, .tool_payload_started, .diff_block, => .drop, .route_recovery_status => |status| if (status.action == .paused) .admit else .drop, .command_output_complete => |lifecycle_id| if (cancelledCommandCompletionMatches(presenter, lifecycle_id)) .admit else .drop, .tool_lifecycle => |lifecycle| switch (lifecycle) { .provisional => |started| cancelledToolStartAdmission(presenter, started.id, batch_events), .authoritative_started => |started| cancelledToolStartAdmission(presenter, started.id, batch_events), .progress => .drop, .terminal => |terminal| if (presenter.record(terminal.id) == null) .drop else .admit, .turn_finished => .admit, }, .begin_prompt, .begin_prompt_with_skill_bindings, .append_user_feedback, .notification, .question_requested, .clear_route_recovery_status, .api_status_text, .finish_prompt, .session_grant, .error_text, => .admit, }; } fn cancelledCommandCompletionMatches( presenter: activity_runtime.LifecyclePresenter, lifecycle_id: ?types.ToolLifecycleId, ) bool { const id = lifecycle_id orelse return false; const record = presenter.record(id) orelse return false; return record.phase == .terminal and record.activity_kind == .command; } pub fn authorizeInteractiveAdmission(app: *App) !bool { if (comptime @hasDecl(@TypeOf(app.worker), "interactiveAdmissionSnapshot")) { switch (app.worker.interactiveAdmissionSnapshot()) { .open => return true, .stopped => return false, .finalization_failed => { if (comptime @hasField(App, "should_exit")) { app.should_exit = true; } return error.TurnFinalizationDeliveryFailed; }, } } return true; } pub fn queuePreview(app: *App) QueuePreview { return app.worker.queuePreview(); } pub fn syncQueuedPromptModel(app: *App, model: []const u8) !void { try app.worker.syncQueuedPromptModel(std.heap.c_allocator, model); } pub fn syncQueuedPromptPermissionSnapshot(app: *App, snapshot: worker_runtime.PermissionSnapshot) void { app.worker.syncQueuedPromptPermissionSnapshot(snapshot); } pub fn syncQueuedPromptPermissionState( app: *App, snapshot: worker_runtime.PermissionSnapshot, ) !void { try app.worker.syncQueuedPromptPermissionState( std.heap.c_allocator, app.permission_engine.grants.items, snapshot, ); } pub fn propagateHistoryTurn(app: *App, turn: types.HistoryTurn, max_history_turns: usize) !void { if (comptime @hasDecl(App, "propagateHistoryTurn")) { try app.propagateHistoryTurn(turn); return; } try app.worker.propagateHistoryTurn(std.heap.c_allocator, turn, max_history_turns); } pub fn propagateGrant(app: *App, tool_name: []const u8, target_path: []const u8) !void { if (comptime @hasDecl(App, "propagateGrant")) { try app.propagateGrant(tool_name, target_path); return; } try app.worker.propagateGrant(std.heap.c_allocator, tool_name, target_path); } pub fn pushEvent(app: *App, event: WorkerEvent) !void { if (comptime @hasDecl(App, "pushWorkerEvent")) { const owned = try worker_runtime.dupeWorkerEvent(std.heap.c_allocator, event); errdefer worker_runtime.freeWorkerEvent(std.heap.c_allocator, owned); try app.pushWorkerEvent(owned); return; } try app.worker.pushEvent(std.heap.c_allocator, event); } pub fn pushOwnedEvent(app: *App, event: WorkerEvent) !void { if (comptime @hasDecl(App, "pushWorkerEvent")) { try app.pushWorkerEvent(event); return; } try app.worker.pushOwnedEvent(std.heap.c_allocator, event); } pub fn pushText(app: *App, text: []const u8) !void { if (comptime @hasDecl(App, "pushText")) { try app.pushText(text); } else { try pushEvent(app, .{ .assistant_presentation = .{ .text = @constCast(text), } }); } debug_trace.eventf( "worker", "assistant_chunk_received", .{}, "chunk_bytes={d}", .{text.len}, ); } pub fn pushTable(app: *App, table: assistant_presentation.TablePayload) !void { try pushEvent(app, .{ .assistant_presentation = .{ .table = table } }); } pub fn pushCodeBlock(app: *App, block: assistant_presentation.CodeBlockPayload) !void { try pushEvent(app, .{ .assistant_presentation = .{ .code_block = block } }); } pub fn pushThematicRule(app: *App) !void { try pushEvent(app, .{ .assistant_presentation = .thematic_rule }); } pub fn pushCommandOutput( app: *App, lifecycle_id: ?types.ToolLifecycleId, stream: command_output_content.Stream, chunk: []const u8, ) !void { if (chunk.len == 0) return; if (comptime @hasDecl(App, "pushCommandOutputWithLifecycle")) { try app.pushCommandOutputWithLifecycle(lifecycle_id, stream, chunk); return; } if (comptime @hasDecl(App, "pushCommandOutput")) { try app.pushCommandOutput(stream, chunk); return; } try pushEvent(app, .{ .command_output = .{ .lifecycle_id = lifecycle_id, .stream = stream, .text = @constCast(chunk), } }); } pub fn pushCommandOutputComplete(app: *App, lifecycle_id: ?types.ToolLifecycleId) !void { try pushEvent(app, .{ .command_output_complete = lifecycle_id }); } pub fn pushToolLifecycle( app: *App, event: types.ToolLifecycleEvent, ) !void { try pushEvent(app, .{ .tool_lifecycle = event }); } pub fn pushWebSearchProgress(app: *App, call_id: []const u8, progress: types.WebSearchProgress) !void { const turn_id = app.worker.activeTurnId(); if (turn_id == 0 or call_id.len == 0) { debug_trace.logf( "ui_activity", "web search progress missing active lifecycle identity", .{}, ); return; } const label = try formatWebSearchProgress(std.heap.c_allocator, progress); defer std.heap.c_allocator.free(label); try pushToolLifecycle(app, .{ .progress = .{ .id = .{ .turn_id = turn_id, .call_id = call_id }, .text = label, } }); } pub fn pushWebFetchProgress(app: *App, call_id: []const u8, progress: types.WebFetchProgress) !void { const turn_id = app.worker.activeTurnId(); if (turn_id == 0 or call_id.len == 0) { debug_trace.logf( "ui_activity", "web fetch progress missing active lifecycle identity", .{}, ); return; } const label = try formatWebFetchProgress(std.heap.c_allocator, progress); defer std.heap.c_allocator.free(label); try pushToolLifecycle(app, .{ .progress = .{ .id = .{ .turn_id = turn_id, .call_id = call_id }, .text = label, } }); } pub fn pushSemanticNotice(app: *App, notice: types.SemanticNotice) !void { try pushEvent(app, .{ .semantic_notice = notice }); } pub fn pushDiffBlock(app: *App, payload: diff_mod.DiffEntryPayload) !void { try pushOwnedEvent(app, .{ .diff_block = payload }); } pub fn syncState( app: *App, presenter: activity_runtime.LifecyclePresenter, ) void { const snapshot = app.worker.snapshotState(app.alloc) catch return; defer snapshot.deinit(app.alloc); const was_approval_active = app.approval_prompt.isActive(); const worker_pending_request = if (snapshot.pending_permission_request) |*request| request.view() else null; const child_pending_request: ?permission_request.PermissionRequest = if (worker_pending_request == null) blk: { if (comptime @hasField(App, "subagents")) { if (comptime @hasDecl(@TypeOf(app.subagents), "mainApprovalRequest")) { break :blk app.subagents.mainApprovalRequest(); } } break :blk null; } else null; if (comptime @hasField(App, "subagents")) { if (comptime @hasDecl(@TypeOf(app.subagents), "markMainApprovalPresented")) { app.subagents.markMainApprovalPresented(child_pending_request != null); } } const pending_request = worker_pending_request orelse child_pending_request; const management_active = if (comptime @hasField( @TypeOf(app.approval_prompt), "rule_management", )) app.approval_prompt.rule_management != null else false; const approval_changed = if (management_active and pending_request == null) false else app.approval_prompt.syncRequest(app.alloc, pending_request) catch false; const review_changed = if (comptime @hasDecl(@TypeOf(app.approval_prompt), "syncReview")) blk: { const review = if (comptime @hasField(@TypeOf(snapshot), "pending_permission_review")) if (snapshot.pending_permission_review) |pending| if (pending_request) |request| if (pending.request_id == request.id) pending.review else null else null else null else null; break :blk app.approval_prompt.syncReview(review); } else false; if (comptime @hasField(App, "approval_screen")) { const file_review_changed = review_changed and (pending_request == null or pending_request.?.file != null); if (approval_changed or file_review_changed) { app.approval_screen.clear(); } } if (approval_changed or review_changed) { app.shell.render_requests.request(.modal); } if (!was_approval_active and app.approval_prompt.isActive()) { if (comptime @hasDecl(App, "dispatchAttentionRequired")) { app.dispatchAttentionRequired(snapshot.active_turn_id, .permission); } } if (app.question_prompt.isActive()) { if (app.worker.snapshotPendingQuestionBatch(app.alloc) catch return) |question_snapshot| { question_snapshot.deinit(app.alloc); } else { app.question_prompt.discard(app.alloc, "worker_cleared"); app.shell.render_requests.request(.modal); } } const modal_active = app.approval_prompt.isActive() or app.question_prompt.isActive(); const queue_review_active = if (comptime @hasField(@TypeOf(snapshot), "queue_review_reason")) snapshot.queue_review_reason != null else false; const worker_events_pending = if (comptime @hasField(@TypeOf(snapshot), "pending_event_count")) snapshot.pending_event_count > 0 else false; const visible_worker_active = app.stream.active and !snapshot.cancel_requested and (snapshot.processing or worker_events_pending or