const std = @import("std");const question_prompt = @import("../../core/agent/question_prompt.zig");const approval_decision = @import("../../core/permissions/approval_decision.zig");const subagent_input = @import("../../core/subagent/input_action.zig");const paste_blocks = @import("../../core/input/pasted_blocks.zig");const core_input_runtime = @import("../../core/input/runtime.zig");const native_clear_probe_runtime = @import("native_clear_probe.zig");const image_attachments = @import("../../core/images/image_attachments.zig");const debug_trace = @import("../../core/shared/debug_trace.zig");const io_mod = @import("../../core/shared/io.zig");const types = @import("../../core/shared/types.zig");const cursor_probe = @import("../terminal/cursor_probe.zig");const theme_monitor = @import("../terminal/theme_monitor.zig");const gesture_state = @import("../../core/input/gesture_state.zig");const horizontal_navigation = @import("../../core/input/horizontal_navigation.zig");const picker_state = @import("../../core/input/picker_state.zig");const composer_history = @import("../../core/input/composer_history.zig");const composer_insertion = @import("../../core/input/composer_insertion.zig");const kill_ring = @import("../../core/input/kill_ring.zig");const entity_spans = @import("../../core/shared/entity_spans.zig");const input_action = @import("../../core/input/input_action.zig");const vertical_navigation = @import("../../core/input/vertical_navigation.zig");const visual_layout = @import("visual_layout.zig");const escape_parser = @import("escape_parser.zig");const shortcuts = @import("shortcuts.zig");const terminal_action_decoder = @import("terminal_action_decoder.zig"); const ImageBlocks = kill_ring.ImageBlocks;const InputRuntime = core_input_runtime.Runtime;const InputAppearance = core_input_runtime.InputAppearance; const Allocator = std.mem.Allocator;const InsertResult = composer_insertion.InsertResult; pub const DeferredTerminalInputSource = enum { theme_monitor, cursor_probe,}; pub const TerminalInputOwner = enum { theme_monitor, paste, takeover, fx_input,}; pub fn terminalInputOwner( monitor: *const theme_monitor.Monitor, paste_active: bool, takeover_active: bool,) TerminalInputOwner { if (monitor.ownsInput()) return .theme_monitor; if (paste_active) return .paste; if (takeover_active) return .takeover; if (monitor.enabled) return .theme_monitor; return .fx_input;} const InputEscapeAction = input_action.Action;const MouseInput = escape_parser.MouseInput; pub const shortcutFromControlByte = shortcuts.fromControlByte;pub const shortcutFromFocusedEditorControlByte = shortcuts.fromFocusedEditorControlByte;pub const shortcutFromEscapeAction = shortcuts.fromEscapeAction; pub fn approvalActionFromByte(byte: u8) ?approval_decision.Action { return switch (byte) { 3 => .deny, '\r', '\n' => .submit, '\t' => .tab, 0x7F, 0x08 => .backspace, '1'...'3' => .{ .number = byte - '1' }, else => if (byte >= 0x20 and byte <= 0x7E) .{ .insert_ascii = byte } else null, };} fn approvalDraftActionFromShortcut( action: input_action.ShortcutAction,) ?approval_decision.DraftAction { return switch (action) { .move => |intent| switch (intent.kind) { .character_left => .cursor_left, .character_right => .cursor_right, .line_start, .draft_start => .cursor_home, .line_end, .draft_end => .cursor_end, .word_left => .cursor_word_left, .word_right => .cursor_word_right, else => null, }, .delete_forward => .delete_next, .delete_word_left => .delete_word_left, .delete_whitespace_word_left => .delete_whitespace_word_left, .delete_word_right => .delete_word_right, .delete_to_line_start => .delete_to_line_start, .delete_to_line_end => .delete_to_line_end, else => null, };} fn approvalDraftActionFromControlByte(byte: u8) ?approval_decision.DraftAction { const action = shortcuts.fromFocusedEditorControlByte(byte) orelse return null; return approvalDraftActionFromShortcut(action);} fn approvalActionFromRawByte(byte: u8) ?approval_decision.Action { if (approvalDraftActionFromControlByte(byte)) |edit| { return .{ .edit_draft = edit }; } return approvalActionFromByte(byte);} fn withApprovalInput( ingress: input_action.TerminalInputIngress,) input_action.TerminalInputIngress { var typed = ingress; const event = typed.event orelse return typed; typed.event = switch (event) { .raw => |raw| input_action.TerminalInputEvent{ .raw = .{ .byte = raw.byte, .composer_shortcut = raw.composer_shortcut, .approval_action = approvalActionFromRawByte(raw.byte), } }, .action => |decoded| input_action.TerminalInputEvent{ .action = .{ .action = decoded.action, .composer_shortcut = decoded.composer_shortcut, .approval_focused_edit = if (decoded.composer_shortcut) |shortcut| approvalDraftActionFromShortcut(shortcut) else null, .cancel_pending = decoded.cancel_pending, } }, .paste_byte, .unrecognized_escape => event, }; return typed;} fn questionActionFromByte( byte: u8, freeform_selected: bool,) ?question_prompt.Action { return switch (byte) { 3 => .cancel, '\r', '\n' => .submit, '\t' => .next_entry, 0x7F, 0x08 => if (freeform_selected) .backspace else null, '1'...'9' => if (freeform_selected) .{ .insert_ascii = byte } else .{ .select_ordinal = byte - '1' }, else => if (freeform_selected and byte >= 0x20 and byte <= 0x7E) .{ .insert_ascii = byte } else null, };} fn questionFreeformActionFromShortcut( action: input_action.ShortcutAction,) ?question_prompt.FreeformAction { return switch (action) { .move => |intent| switch (intent.kind) { .character_left => .cursor_left, .character_right => .cursor_right, .line_start, .draft_start => .cursor_home, .line_end, .draft_end => .cursor_end, .word_left => .cursor_word_left, .word_right => .cursor_word_right, else => null, }, .delete_forward => .delete_next, .delete_word_left => .delete_word_left, .delete_whitespace_word_left => .delete_whitespace_word_left, .delete_word_right => .delete_word_right, .delete_to_line_start => .delete_to_line_start, .delete_to_line_end => .delete_to_line_end, else => null, };} fn questionActionFromShortcut( action: input_action.ShortcutAction, freeform_selected: bool,) ?question_prompt.Action { switch (action) { .move => |intent| { if (intent.extend_selection) { switch (intent.kind) { .visual_up => return .{ .move_choice = .previous }, .visual_down => return .{ .move_choice = .next }, else => {}, } } }, else => {}, } if (!freeform_selected) return null; const edit = questionFreeformActionFromShortcut(action) orelse return null; return .{ .edit_freeform = edit };} fn questionActionFromRawByte( byte: u8, freeform_selected: bool,) ?question_prompt.Action { if (freeform_selected) { if (shortcuts.fromFocusedEditorControlByte(byte)) |shortcut| { return questionActionFromShortcut(shortcut, true); } } return questionActionFromByte(byte, freeform_selected);} fn questionActionFromDecoded( action: input_action.Action, shortcut: ?input_action.ShortcutAction, freeform_selected: bool,) ?question_prompt.Action { switch (action) { .remapped_byte => |byte| return questionActionFromRawByte( byte, freeform_selected, ), else => {}, } return questionActionFromShortcut( shortcut orelse return null, freeform_selected, );} fn withQuestionInput( ingress: input_action.TerminalInputIngress, freeform_selected: bool,) input_action.TerminalInputIngress { var typed = ingress; const event = typed.event orelse return typed; typed.event = switch (event) { .raw => |raw| input_action.TerminalInputEvent{ .raw = .{ .byte = raw.byte, .composer_shortcut = raw.composer_shortcut, .approval_action = raw.approval_action, .question_action = questionActionFromRawByte( raw.byte, freeform_selected, ), } }, .action => |decoded| input_action.TerminalInputEvent{ .action = .{ .action = decoded.action, .composer_shortcut = decoded.composer_shortcut, .approval_focused_edit = decoded.approval_focused_edit, .question_action = questionActionFromDecoded( decoded.action, decoded.composer_shortcut, freeform_selected, ), .cancel_pending = decoded.cancel_pending, } }, .paste_byte, .unrecognized_escape => event, }; return typed;} fn subagentActionFromShortcut( action: input_action.ShortcutAction,) ?subagent_input.Action { return switch (action) { .move => |intent| if (intent.extend_selection) null else switch (intent.kind) { .character_left => .left, .character_right => .right, .word_left => .word_left, .word_right => .word_right, .line_start, .draft_start => .home, .line_end, .draft_end => .end, .visual_up => .up, .visual_down => .down, .page_up => .page_up, .page_down => .page_down, .paragraph_up, .paragraph_down => null, }, .delete_backward => .delete_backward, .delete_forward => .delete_next, .delete_word_left => .delete_word_left, .delete_word_right => .delete_word_right, .delete_to_line_start => .delete_to_line_start, .delete_to_line_end => .delete_to_line_end, .insert_newline => .insert_newline, .select_all, .copy_selection, .cut_selection, .undo, .redo, .history_previous, .history_next, .delete_whitespace_word_left, .yank, .redraw, => null, };} fn subagentActionFromRawByte(byte: u8) ?subagent_input.Action { return switch (byte) { 3 => .ctrl_c, 24 => .toggle, '\r' => .enter, '\t' => .focus_next, 1 => .home, 5 => .end, 0x7f, 8 => .delete_backward, 11 => .delete_to_line_end, 21 => .clear_line, 23 => .delete_word_left, else => null, };} fn subagentActionFromDecoded(action: input_action.Action) ?subagent_input.Action { return switch (action) { .escape => .escape, .history_up, .cursor_up => .up, .history_down, .cursor_down => .down, .cursor_left => .left, .cursor_right => .right, .home => .home, .end => .end, .word_left => .word_left, .word_right => .word_right, .delete_next => .delete_next, .delete_word_left => .delete_word_left, .delete_word_right => .delete_word_right, .delete_to_line_start => .delete_to_line_start, .delete_to_line_end => .delete_to_line_end, .clear_line => .clear_line, .insert_newline => .insert_newline, .page_up => .page_up, .page_down => .page_down, .composer_shortcut => |typed| subagentActionFromShortcut(typed), .remapped_byte => |byte| subagentActionFromRawByte(byte), .mouse_wheel, .mouse_pointer, .toggle_full_transcript, .toggle_permission_mode, .open_all_sessions, .paste_start, .paste_end, .ignore, => null, };} fn withSubagentInput( ingress: input_action.TerminalInputIngress,) input_action.TerminalInputIngress { var typed = ingress; const event = typed.event orelse return typed; typed.event = switch (event) { .raw => |raw| input_action.TerminalInputEvent{ .raw = .{ .byte = raw.byte, .composer_shortcut = raw.composer_shortcut, .approval_action = raw.approval_action, .question_action = raw.question_action, .subagent_action = subagentActionFromRawByte(raw.byte), } }, .action => |decoded| input_action.TerminalInputEvent{ .action = .{ .action = decoded.action, .composer_shortcut = decoded.composer_shortcut, .approval_focused_edit = decoded.approval_focused_edit, .question_action = decoded.question_action, .subagent_action = subagentActionFromDecoded(decoded.action), .cancel_pending = decoded.cancel_pending, } }, .paste_byte, .unrecognized_escape => event, }; return typed;} test "terminal input carries typed subagent controls and shortcuts" { var runtime = Runtime{}; defer runtime.deinit(std.testing.allocator); const context: input_action.TerminalDecodeContext = .{ .now_ms = 1, .paste_active = false, .cancel_pending = false, .child_route_active = true, }; const toggle = runtime.decodeTerminalByte(24, context); try std.testing.expectEqual( subagent_input.Action.toggle, toggle.event.?.raw.subagent_action.?, ); const line_start = runtime.decodeTerminalByte(1, context); try std.testing.expectEqual( subagent_input.Action.home, line_start.event.?.raw.subagent_action.?, ); const clear_line = runtime.decodeTerminalByte(21, context); try std.testing.expectEqual( subagent_input.Action.clear_line, clear_line.event.?.raw.subagent_action.?, ); const child_only_shortcut = runtime.decodeTerminalByte(2, context); try std.testing.expect(child_only_shortcut.event.?.raw.subagent_action == null); try std.testing.expectEqual( input_action.ShortcutAction{ .move = .{ .kind = .character_left } }, child_only_shortcut.event.?.raw.composer_shortcut.?, ); const text = runtime.decodeTerminalByte('j', context); try std.testing.expect(text.event.?.raw.subagent_action == null); var word_delete = input_action.TerminalInputIngress{}; for ("\x1bd") |byte| { word_delete = runtime.decodeTerminalByte(byte, context); } try std.testing.expectEqual( subagent_input.Action.delete_word_right, word_delete.event.?.action.subagent_action.?, ); var arrow = input_action.TerminalInputIngress{}; for ("\x1b[A") |byte| { arrow = runtime.decodeTerminalByte(byte, context); } try std.testing.expectEqual( subagent_input.Action.up, arrow.event.?.action.subagent_action.?, );} test "question bytes translate to typed prompt actions" { try std.testing.expectEqual( question_prompt.Action.cancel, questionActionFromByte(3, false).?, ); try std.testing.expectEqual( question_prompt.Action.submit, questionActionFromByte('\r', false).?, ); try std.testing.expectEqual( question_prompt.Action.next_entry, questionActionFromByte('\t', false).?, ); try std.testing.expectEqual( question_prompt.Action{ .select_ordinal = 2 }, questionActionFromByte('3', false).?, ); try std.testing.expectEqual( question_prompt.Action{ .insert_ascii = '3' }, questionActionFromByte('3', true).?, ); try std.testing.expectEqual( question_prompt.Action.backspace, questionActionFromByte(0x7F, true).?, ); try std.testing.expect(questionActionFromByte('x', false) == null); try std.testing.expect(questionActionFromByte(0x7F, false) == null); try std.testing.expect(questionActionFromByte(0x80, true) == null);} test "terminal input carries typed question decisions and focused edits" { var runtime = Runtime{}; defer runtime.deinit(std.testing.allocator); const choice_context: input_action.TerminalDecodeContext = .{ .now_ms = 1, .paste_active = false, .cancel_pending = false, .child_route_active = false, .question_freeform_selected = false, }; const choice = runtime.decodeTerminalByte('3', choice_context); try std.testing.expectEqual( question_prompt.Action{ .select_ordinal = 2 }, choice.event.?.raw.question_action.?, ); const freeform_context: input_action.TerminalDecodeContext = .{ .now_ms = 2, .paste_active = false, .cancel_pending = false, .child_route_active = false, .question_freeform_selected = true, }; const digit = runtime.decodeTerminalByte('3', freeform_context); try std.testing.expectEqual( question_prompt.Action{ .insert_ascii = '3' }, digit.event.?.raw.question_action.?, ); const focused_control = runtime.decodeTerminalByte(1, freeform_context); try std.testing.expectEqual( question_prompt.Action{ .edit_freeform = .cursor_home },Lines 1–500 of 4981. Load more
const std = @import("std"); const question_prompt = @import("../../core/agent/question_prompt.zig"); const approval_decision = @import("../../core/permissions/approval_decision.zig"); const subagent_input = @import("../../core/subagent/input_action.zig"); const paste_blocks = @import("../../core/input/pasted_blocks.zig"); const core_input_runtime = @import("../../core/input/runtime.zig"); const native_clear_probe_runtime = @import("native_clear_probe.zig"); const image_attachments = @import("../../core/images/image_attachments.zig"); const debug_trace = @import("../../core/shared/debug_trace.zig"); const io_mod = @import("../../core/shared/io.zig"); const types = @import("../../core/shared/types.zig"); const cursor_probe = @import("../terminal/cursor_probe.zig"); const theme_monitor = @import("../terminal/theme_monitor.zig"); const gesture_state = @import("../../core/input/gesture_state.zig"); const horizontal_navigation = @import("../../core/input/horizontal_navigation.zig"); const picker_state = @import("../../core/input/picker_state.zig"); const composer_history = @import("../../core/input/composer_history.zig"); const composer_insertion = @import("../../core/input/composer_insertion.zig"); const kill_ring = @import("../../core/input/kill_ring.zig"); const entity_spans = @import("../../core/shared/entity_spans.zig"); const input_action = @import("../../core/input/input_action.zig"); const vertical_navigation = @import("../../core/input/vertical_navigation.zig"); const visual_layout = @import("visual_layout.zig"); const escape_parser = @import("escape_parser.zig"); const shortcuts = @import("shortcuts.zig"); const terminal_action_decoder = @import("terminal_action_decoder.zig"); const ImageBlocks = kill_ring.ImageBlocks; const InputRuntime = core_input_runtime.Runtime; const InputAppearance = core_input_runtime.InputAppearance; const Allocator = std.mem.Allocator; const InsertResult = composer_insertion.InsertResult; pub const DeferredTerminalInputSource = enum { theme_monitor, cursor_probe, }; pub const TerminalInputOwner = enum { theme_monitor, paste, takeover, fx_input, }; pub fn terminalInputOwner( monitor: *const theme_monitor.Monitor, paste_active: bool, takeover_active: bool, ) TerminalInputOwner { if (monitor.ownsInput()) return .theme_monitor; if (paste_active) return .paste; if (takeover_active) return .takeover; if (monitor.enabled) return .theme_monitor; return .fx_input; } const InputEscapeAction = input_action.Action; const MouseInput = escape_parser.MouseInput; pub const shortcutFromControlByte = shortcuts.fromControlByte; pub const shortcutFromFocusedEditorControlByte = shortcuts.fromFocusedEditorControlByte; pub const shortcutFromEscapeAction = shortcuts.fromEscapeAction; pub fn approvalActionFromByte(byte: u8) ?approval_decision.Action { return switch (byte) { 3 => .deny, '\r', '\n' => .submit, '\t' => .tab, 0x7F, 0x08 => .backspace, '1'...'3' => .{ .number = byte - '1' }, else => if (byte >= 0x20 and byte <= 0x7E) .{ .insert_ascii = byte } else null, }; } fn approvalDraftActionFromShortcut( action: input_action.ShortcutAction, ) ?approval_decision.DraftAction { return switch (action) { .move => |intent| switch (intent.kind) { .character_left => .cursor_left, .character_right => .cursor_right, .line_start, .draft_start => .cursor_home, .line_end, .draft_end => .cursor_end, .word_left => .cursor_word_left, .word_right => .cursor_word_right, else => null, }, .delete_forward => .delete_next, .delete_word_left => .delete_word_left, .delete_whitespace_word_left => .delete_whitespace_word_left, .delete_word_right => .delete_word_right, .delete_to_line_start => .delete_to_line_start, .delete_to_line_end => .delete_to_line_end, else => null, }; } fn approvalDraftActionFromControlByte(byte: u8) ?approval_decision.DraftAction { const action = shortcuts.fromFocusedEditorControlByte(byte) orelse return null; return approvalDraftActionFromShortcut(action); } fn approvalActionFromRawByte(byte: u8) ?approval_decision.Action { if (approvalDraftActionFromControlByte(byte)) |edit| { return .{ .edit_draft = edit }; } return approvalActionFromByte(byte); } fn withApprovalInput( ingress: input_action.TerminalInputIngress, ) input_action.TerminalInputIngress { var typed = ingress; const event = typed.event orelse return typed; typed.event = switch (event) { .raw => |raw| input_action.TerminalInputEvent{ .raw = .{ .byte = raw.byte, .composer_shortcut = raw.composer_shortcut, .approval_action = approvalActionFromRawByte(raw.byte), } }, .action => |decoded| input_action.TerminalInputEvent{ .action = .{ .action = decoded.action, .composer_shortcut = decoded.composer_shortcut, .approval_focused_edit = if (decoded.composer_shortcut) |shortcut| approvalDraftActionFromShortcut(shortcut) else null, .cancel_pending = decoded.cancel_pending, } }, .paste_byte, .unrecognized_escape => event, }; return typed; } fn questionActionFromByte( byte: u8, freeform_selected: bool, ) ?question_prompt.Action { return switch (byte) { 3 => .cancel, '\r', '\n' => .submit, '\t' => .next_entry, 0x7F, 0x08 => if (freeform_selected) .backspace else null, '1'...'9' => if (freeform_selected) .{ .insert_ascii = byte } else .{ .select_ordinal = byte - '1' }, else => if (freeform_selected and byte >= 0x20 and byte <= 0x7E) .{ .insert_ascii = byte } else null, }; } fn questionFreeformActionFromShortcut( action: input_action.ShortcutAction, ) ?question_prompt.FreeformAction { return switch (action) { .move => |intent| switch (intent.kind) { .character_left => .cursor_left, .character_right => .cursor_right, .line_start, .draft_start => .cursor_home, .line_end, .draft_end => .cursor_end, .word_left => .cursor_word_left, .word_right => .cursor_word_right, else => null, }, .delete_forward => .delete_next, .delete_word_left => .delete_word_left, .delete_whitespace_word_left => .delete_whitespace_word_left, .delete_word_right => .delete_word_right, .delete_to_line_start => .delete_to_line_start, .delete_to_line_end => .delete_to_line_end, else => null, }; } fn questionActionFromShortcut( action: input_action.ShortcutAction, freeform_selected: bool, ) ?question_prompt.Action { switch (action) { .move => |intent| { if (intent.extend_selection) { switch (intent.kind) { .visual_up => return .{ .move_choice = .previous }, .visual_down => return .{ .move_choice = .next }, else => {}, } } }, else => {}, } if (!freeform_selected) return null; const edit = questionFreeformActionFromShortcut(action) orelse return null; return .{ .edit_freeform = edit }; } fn questionActionFromRawByte( byte: u8, freeform_selected: bool, ) ?question_prompt.Action { if (freeform_selected) { if (shortcuts.fromFocusedEditorControlByte(byte)) |shortcut| { return questionActionFromShortcut(shortcut, true); } } return questionActionFromByte(byte, freeform_selected); } fn questionActionFromDecoded( action: input_action.Action, shortcut: ?input_action.ShortcutAction, freeform_selected: bool, ) ?question_prompt.Action { switch (action) { .remapped_byte => |byte| return questionActionFromRawByte( byte, freeform_selected, ), else => {}, } return questionActionFromShortcut( shortcut orelse return null, freeform_selected, ); } fn withQuestionInput( ingress: input_action.TerminalInputIngress, freeform_selected: bool, ) input_action.TerminalInputIngress { var typed = ingress; const event = typed.event orelse return typed; typed.event = switch (event) { .raw => |raw| input_action.TerminalInputEvent{ .raw = .{ .byte = raw.byte, .composer_shortcut = raw.composer_shortcut, .approval_action = raw.approval_action, .question_action = questionActionFromRawByte( raw.byte, freeform_selected, ), } }, .action => |decoded| input_action.TerminalInputEvent{ .action = .{ .action = decoded.action, .composer_shortcut = decoded.composer_shortcut, .approval_focused_edit = decoded.approval_focused_edit, .question_action = questionActionFromDecoded( decoded.action, decoded.composer_shortcut, freeform_selected, ), .cancel_pending = decoded.cancel_pending, } }, .paste_byte, .unrecognized_escape => event, }; return typed; } fn subagentActionFromShortcut( action: input_action.ShortcutAction, ) ?subagent_input.Action { return switch (action) { .move => |intent| if (intent.extend_selection) null else switch (intent.kind) { .character_left => .left, .character_right => .right, .word_left => .word_left, .word_right => .word_right, .line_start, .draft_start => .home, .line_end, .draft_end => .end, .visual_up => .up, .visual_down => .down, .page_up => .page_up, .page_down => .page_down, .paragraph_up, .paragraph_down => null, }, .delete_backward => .delete_backward, .delete_forward => .delete_next, .delete_word_left => .delete_word_left, .delete_word_right => .delete_word_right, .delete_to_line_start => .delete_to_line_start, .delete_to_line_end => .delete_to_line_end, .insert_newline => .insert_newline, .select_all, .copy_selection, .cut_selection, .undo, .redo, .history_previous, .history_next, .delete_whitespace_word_left, .yank, .redraw, => null, }; } fn subagentActionFromRawByte(byte: u8) ?subagent_input.Action { return switch (byte) { 3 => .ctrl_c, 24 => .toggle, '\r' => .enter, '\t' => .focus_next, 1 => .home, 5 => .end, 0x7f, 8 => .delete_backward, 11 => .delete_to_line_end, 21 => .clear_line, 23 => .delete_word_left, else => null, }; } fn subagentActionFromDecoded(action: input_action.Action) ?subagent_input.Action { return switch (action) { .escape => .escape, .history_up, .cursor_up => .up, .history_down, .cursor_down => .down, .cursor_left => .left, .cursor_right => .right, .home => .home, .end => .end, .word_left => .word_left, .word_right => .word_right, .delete_next => .delete_next, .delete_word_left => .delete_word_left, .delete_word_right => .delete_word_right, .delete_to_line_start => .delete_to_line_start, .delete_to_line_end => .delete_to_line_end, .clear_line => .clear_line, .insert_newline => .insert_newline, .page_up => .page_up, .page_down => .page_down, .composer_shortcut => |typed| subagentActionFromShortcut(typed), .remapped_byte => |byte| subagentActionFromRawByte(byte), .mouse_wheel, .mouse_pointer, .toggle_full_transcript, .toggle_permission_mode, .open_all_sessions, .paste_start, .paste_end, .ignore, => null, }; } fn withSubagentInput( ingress: input_action.TerminalInputIngress, ) input_action.TerminalInputIngress { var typed = ingress; const event = typed.event orelse return typed; typed.event = switch (event) { .raw => |raw| input_action.TerminalInputEvent{ .raw = .{ .byte = raw.byte, .composer_shortcut = raw.composer_shortcut, .approval_action = raw.approval_action, .question_action = raw.question_action, .subagent_action = subagentActionFromRawByte(raw.byte), } }, .action => |decoded| input_action.TerminalInputEvent{ .action = .{ .action = decoded.action, .composer_shortcut = decoded.composer_shortcut, .approval_focused_edit = decoded.approval_focused_edit, .question_action = decoded.question_action, .subagent_action = subagentActionFromDecoded(decoded.action), .cancel_pending = decoded.cancel_pending, } }, .paste_byte, .unrecognized_escape => event, }; return typed; } test "terminal input carries typed subagent controls and shortcuts" { var runtime = Runtime{}; defer runtime.deinit(std.testing.allocator); const context: input_action.TerminalDecodeContext = .{ .now_ms = 1, .paste_active = false, .cancel_pending = false, .child_route_active = true, }; const toggle = runtime.decodeTerminalByte(24, context); try std.testing.expectEqual( subagent_input.Action.toggle, toggle.event.?.raw.subagent_action.?, ); const line_start = runtime.decodeTerminalByte(1, context); try std.testing.expectEqual( subagent_input.Action.home, line_start.event.?.raw.subagent_action.?, ); const clear_line = runtime.decodeTerminalByte(21, context); try std.testing.expectEqual( subagent_input.Action.clear_line, clear_line.event.?.raw.subagent_action.?, ); const child_only_shortcut = runtime.decodeTerminalByte(2, context); try std.testing.expect(child_only_shortcut.event.?.raw.subagent_action == null); try std.testing.expectEqual( input_action.ShortcutAction{ .move = .{ .kind = .character_left } }, child_only_shortcut.event.?.raw.composer_shortcut.?, ); const text = runtime.decodeTerminalByte('j', context); try std.testing.expect(text.event.?.raw.subagent_action == null); var word_delete = input_action.TerminalInputIngress{}; for ("\x1bd") |byte| { word_delete = runtime.decodeTerminalByte(byte, context); } try std.testing.expectEqual( subagent_input.Action.delete_word_right, word_delete.event.?.action.subagent_action.?, ); var arrow = input_action.TerminalInputIngress{}; for ("\x1b[A") |byte| { arrow = runtime.decodeTerminalByte(byte, context); } try std.testing.expectEqual( subagent_input.Action.up, arrow.event.?.action.subagent_action.?, ); } test "question bytes translate to typed prompt actions" { try std.testing.expectEqual( question_prompt.Action.cancel, questionActionFromByte(3, false).?, ); try std.testing.expectEqual( question_prompt.Action.submit, questionActionFromByte('\r', false).?, ); try std.testing.expectEqual( question_prompt.Action.next_entry, questionActionFromByte('\t', false).?, ); try std.testing.expectEqual( question_prompt.Action{ .select_ordinal = 2 }, questionActionFromByte('3', false).?, ); try std.testing.expectEqual( question_prompt.Action{ .insert_ascii = '3' }, questionActionFromByte('3', true).?, ); try std.testing.expectEqual( question_prompt.Action.backspace, questionActionFromByte(0x7F, true).?, ); try std.testing.expect(questionActionFromByte('x', false) == null); try std.testing.expect(questionActionFromByte(0x7F, false) == null); try std.testing.expect(questionActionFromByte(0x80, true) == null); } test "terminal input carries typed question decisions and focused edits" { var runtime = Runtime{}; defer runtime.deinit(std.testing.allocator); const choice_context: input_action.TerminalDecodeContext = .{ .now_ms = 1, .paste_active = false, .cancel_pending = false, .child_route_active = false, .question_freeform_selected = false, }; const choice = runtime.decodeTerminalByte('3', choice_context); try std.testing.expectEqual( question_prompt.Action{ .select_ordinal = 2 }, choice.event.?.raw.question_action.?, ); const freeform_context: input_action.TerminalDecodeContext = .{ .now_ms = 2, .paste_active = false, .cancel_pending = false, .child_route_active = false, .question_freeform_selected = true, }; const digit = runtime.decodeTerminalByte('3', freeform_context); try std.testing.expectEqual( question_prompt.Action{ .insert_ascii = '3' }, digit.event.?.raw.question_action.?, ); const focused_control = runtime.decodeTerminalByte(1, freeform_context); try std.testing.expectEqual( question_prompt.Action{ .edit_freeform = .cursor_home },