const std = @import("std");const runtime_profile = @import("../hosts/runtime_profile.zig");const edit_contract = @import("../input/editor_state.zig");const file_picker_path = @import("../input/file_picker_path.zig");const horizontal_navigation = @import("../input/horizontal_navigation.zig");const picker_state = @import("../input/picker_state.zig");const core_input_runtime = @import("../input/runtime.zig");const debug_trace = @import("../shared/debug_trace.zig");const display_width = @import("../shared/display_width.zig");const list_window = @import("../shared/list_window.zig");const model_capabilities = @import("../config/model_capabilities.zig");const command_specs = @import("../slash_commands/command_specs.zig");const sandbox = @import("../permissions/sandbox.zig");const session_commands = @import("../session/session_commands.zig");const skill_runtime = @import("../skills/skill_runtime.zig");const text_utils = @import("../shared/text_utils.zig");const file_index = @import("../workspace/file_index.zig");const app_workspace_runtime = @import("app_workspace_runtime.zig");const app_session_runtime = @import("app_session_runtime.zig");const app_commands = @import("app_commands.zig");const input_queue_runtime = @import("input_queue_runtime.zig");const input_limit_feedback = @import("input_limit_feedback.zig");const types = @import("../shared/types.zig");const ui_input = @import("../../ui/input/runtime.zig");const visual_layout = @import("../../ui/input/visual_layout.zig");const input_presentation = @import("../../ui/footer/input_presentation.zig");const picker_presentation = @import("../../ui/footer/picker_presentation.zig");const render_input = @import("../../ui/footer/render_input.zig");const skills_menu_presentation = @import("../../ui/footer/skills_menu_presentation.zig");const model_menu_presentation = @import("../../ui/footer/model_menu_presentation.zig");const resume_menu_presentation = @import("../../ui/footer/resume_menu_presentation.zig");const help_menu_presentation = @import("../../ui/footer/help_menu_presentation.zig");const settings_menu_presentation = @import("../../ui/footer/settings_menu_presentation.zig");const catalog_screen_layout = @import("../../ui/catalog_screen_layout.zig"); const ModelPickerStage = picker_state.ModelPickerStage; pub const file_picker_completion_cap: usize = 32;const file_picker_match_span_cap = file_picker_completion_cap * file_index.max_path_len;pub const file_picker_path_storage_cap = file_picker_completion_cap * file_index.max_path_len; pub const InlineSkillCompletion = struct { skill: skill_runtime.Skill, suffix: []const u8, replace_start: usize, replace_end: usize,}; pub const InlineSlashCompletion = struct { suffix: []const u8,}; pub const InlineCompletion = union(enum) { skill: InlineSkillCompletion, slash: InlineSlashCompletion, pub fn suffix(self: InlineCompletion) []const u8 { return switch (self) { .skill => |completion| completion.suffix, .slash => |completion| completion.suffix, }; }}; pub fn CompletionRuntime(comptime App: type) type { return struct { const queue_rt = input_queue_runtime.Runtime(App); const FilePickerSelection = struct { query: picker_state.FilePickerQuery, choice: file_index.SearchResult, }; pub fn slashCompletionCandidateCount(app: *App) usize { const prefix = command_specs.slashCompletionPrefix( app.slashRegistry(), app.input_runtime.edit_state.input.items, ) orelse return 0; if (comptime @hasField(App, "skills")) { return picker_presentation.mixedSlashCompletionCount( app.slashRegistry(), prefix, app.skills.items, ); } return command_specs.slashCompletionCount(app.slashRegistry(), prefix); } fn slashCompletionQueryActive(app: *App) bool { if (app.input_runtime.picker.isInlinePickerDismissed(.slash)) return false; if (app.input_runtime.picker.inlinePickerTriggerKind(&app.input_runtime.edit_state) != .slash) return false; // Model query always owns this slot. Mid-turn bare `/model` does too // (list stays hidden); idle bare `/model` still surfaces slash rows. if (comptime @hasField(App, "stream")) { if (app.stream.active and picker_state.isBareModelCommandAtCursor(&app.input_runtime.edit_state)) return false; } return command_specs.slashCompletionPrefix( app.slashRegistry(), app.input_runtime.edit_state.input.items, ) != null; } pub fn slashCompletionCount(app: *App) usize { if (!slashCompletionQueryActive(app)) return 0; return slashCompletionCandidateCount(app); } fn visibleSlashCompletionQueryActive(app: *App) bool { if (comptime @hasField(App, "question_prompt")) { if (app.question_prompt.isActive()) return false; } if (comptime @hasField(App, "approval_prompt")) { if (app.approval_prompt.isActive()) return false; } if (hasFileQuery(app)) return false; return slashCompletionQueryActive(app); } /// Returns selectable completions projected by the footer. The active /// query can remain visible at zero so dismissal still owns Escape. pub fn visibleSlashCompletionCount(app: *App) usize { if (!visibleSlashCompletionQueryActive(app)) return 0; return slashCompletionCandidateCount(app); } pub fn dismissVisibleInlinePicker(app: *App) bool { const kind = visibleInlinePickerKind(app) orelse return false; app.input_runtime.picker.dismissInlinePicker(kind); return true; } fn visibleInlinePickerKind(app: *App) ?picker_state.InlinePickerKind { if (comptime @hasField(App, "question_prompt")) { if (app.question_prompt.isActive()) return null; } if (comptime @hasField(App, "approval_prompt")) { if (app.approval_prompt.isActive()) return null; } if (comptime @hasField(App, "stream")) { if (app.stream.active) { if (queueReviewOwnsComposer(app)) { return if (hasFileQuery(app)) .file else null; } if (visibleInlineSlashCompletion(app) != null) return .slash; if (visibleSlashCompletionQueryActive(app)) return .slash; return null; } } if (hasModelQuery(app)) return .model; if (hasFileQuery(app)) return .file; if (visibleInlineCompletion(app)) |completion| { return switch (completion) { .skill => .skill, .slash => .slash, }; } if (visibleSlashCompletionQueryActive(app)) return .slash; return null; } fn navigateSlashCompletion(app: *App, delta: i32, count: usize) void { navigatePickerOptions(&app.input_runtime.picker.slash_completion_index, &app.input_runtime.picker.slash_completion_window_start, count, delta); } pub fn routeSlashCompletionMove(app: *App, delta: i32) bool { const count = visibleSlashCompletionCount(app); if (count == 0) return false; navigateSlashCompletion(app, delta, count); return true; } pub fn routeModifiedHistory(app: *App, direction: visual_layout.Direction, delta: i32) !void { app.input_runtime.vertical_navigation.reset(); if (routeNonSlashPickerMove(app, delta)) { return; } else if (!routeSlashCompletionMove(app, delta)) { if (app.input_runtime.composer_history.activeIndex() != null) { try navigatePromptHistory(app, delta); } else if (direction == .up and app.input_runtime.edit_state.cursor > 0) { _ = horizontal_navigation.move( .draft_start, &app.input_runtime.edit_state, &app.input_runtime.entities, &app.input_runtime.vertical_navigation, ); } else if (direction == .up) { try navigatePromptHistory(app, -1); } else if (app.input_runtime.edit_state.cursor < app.input_runtime.edit_state.input.items.len) { _ = horizontal_navigation.move( .draft_end, &app.input_runtime.edit_state, &app.input_runtime.entities, &app.input_runtime.vertical_navigation, ); } else { try navigatePromptHistory(app, 1); } } } pub fn routePlainVertical(app: *App, direction: visual_layout.Direction, delta: i32) !void { try routeComposerVertical(app, direction, delta, false, false); } pub fn routeComposerVertical( app: *App, direction: visual_layout.Direction, delta: i32, extend_selection: bool, page: bool, ) !void { const row_count = if (page) input_presentation.inputRowLimit(app.shell.layout.content_bottom) else @as(usize, 1); const picker_delta = if (page) delta * @as(i32, @intCast(@min(row_count, std.math.maxInt(i32)))) else delta; if (routeNonSlashPickerMove(app, picker_delta)) { app.input_runtime.vertical_navigation.reset(); return; } if (comptime @hasField(App, "queued_prompt_review")) { if (try queue_rt.routeVertical(app, direction)) { app.input_runtime.vertical_navigation.reset(); return; } } const scan = if (page) ui_input.scanInputCursorRows( &app.input_runtime, direction, row_count, app.shell.layout.cols, app.pending_images.items, ) else ui_input.scanInputCursorVertical( &app.input_runtime, direction, app.shell.layout.cols, app.pending_images.items, ); const capped = input_presentation.cappedInputRows( scan.total_rows, app.shell.layout.content_bottom, true, ); const visible_slash_count = if (capped.input_extra == 0) visibleSlashCompletionCount(app) else 0; if (visible_slash_count > 0) { app.input_runtime.vertical_navigation.reset(); navigateSlashCompletion(app, picker_delta, visible_slash_count); return; } if (app.input_runtime.vertical_navigation.applyTargetWithSelection( &app.input_runtime.edit_state, &app.input_runtime.entities, if (scan.target) |target| target.raw_offset else null, scan.preferred_column, extend_selection, )) return; if (page or extend_selection) return; if (direction == .up and app.input_runtime.edit_state.cursor > 0) { _ = horizontal_navigation.move( .draft_start, &app.input_runtime.edit_state, &app.input_runtime.entities, &app.input_runtime.vertical_navigation, ); } else if (direction == .up) { try navigatePromptHistory(app, -1); } else if (app.input_runtime.edit_state.cursor < app.input_runtime.edit_state.input.items.len) { _ = horizontal_navigation.move( .draft_end, &app.input_runtime.edit_state, &app.input_runtime.entities, &app.input_runtime.vertical_navigation, ); } else { try navigatePromptHistory(app, 1); } } pub fn navigatePromptHistory(app: *App, delta: i32) !void { const has_image_id_owner = @hasDecl(App, "peekNextImageId") and @hasDecl(App, "nextImageId"); const first_image_id = if (comptime has_image_id_owner) app.peekNextImageId() else 1; const max_input_len = if (comptime @hasDecl(App, "input_limits")) App.input_limits.composer_bytes else if (comptime @hasDecl(App, "input_byte_limit")) App.input_byte_limit else std.math.maxInt(usize); const result = app.input_runtime.composer_history.navigate( app.alloc, delta, .{ .edit = &app.input_runtime.edit_state, .entities = &app.input_runtime.entities, .picker = &app.input_runtime.picker, .vertical_navigation = &app.input_runtime.vertical_navigation, .input_limit_rejection = &app.input_runtime.input_limit_rejection, .images = &app.pending_images, }, first_image_id, max_input_len, ) catch |err| { switch (err) { error.FileNotFound, error.AccessDenied, error.MissingImageSnapshot, error.InvalidImageSnapshotDigest, error.ImageSnapshotPathUnsafe, error.NotRegularFile, error.ImageTooLarge, error.ImageSnapshotCorrupt, error.UnsupportedImageType, error.ImageSnapshotMediaTypeMismatch, => { debug_trace.logf( "prompt_history", "event=recall_rejected err={s}", .{@errorName(err)}, ); return; }, else => return err, } }; switch (result) { .unchanged => {}, .limit_exceeded => |attempted_bytes| try input_limit_feedback.report( App, app, .composer, attempted_bytes, ), .moved => |image_count| { app.input_runtime.historyBoundary(app.alloc); if (comptime has_image_id_owner) { for (0..image_count) |offset| { const committed_id = app.nextImageId(); std.debug.assert(committed_id == first_image_id + offset); } } else { std.debug.assert(image_count == 0); } }, } } pub fn routeVisiblePickerMove(app: *App, delta: i32) bool { if (routeNonSlashPickerMove(app, delta)) return true; return routeSlashCompletionMove(app, delta); } fn routeNonSlashPickerMove(app: *App, delta: i32) bool { if (routeSettingsMenuMove(app, delta)) return true; if (routeHelpMenuMove(app, delta)) return true; if (routeModelMenuMove(app, delta)) return true; if (routeAuthPickerMove(app, delta)) return true; if (routeSkillsMenuMove(app, delta)) return true; if (comptime runtime_profile.allows(App, .durable_sessions)) { if (routeSessionPickerMove(app, delta)) return true; } const stream_suppresses_file_picker = app.stream.active and !queueReviewOwnsComposer(app); if (!stream_suppresses_file_picker and hasFileQuery(app)) { navigateFilePicker(app, delta); return true; } if (hasModelQuery(app)) { if (!app.stream.active) navigateModelPicker(app, delta); return true; } // Mid-turn bare `/model`: consume arrows without slash/skill navigation. if (app.stream.active and picker_state.isBareModelCommandAtCursor(&app.input_runtime.edit_state)) return true; return false; } fn routeSettingsMenuMove(app: *App, delta: i32) bool { const menu = &app.input_runtime.settings_menu; if (!menu.active) return false; if (comptime @hasField(App, "model_cache")) { if (app.model_cache.menu.active) { _ = app.model_cache.menu.moveVisibleItems(delta, 6); return true; } } const snapshot = app_commands.settingsCatalogSnapshot(app); const projection = render_input.settingsMenuProjection( menu, snapshot, app.input_runtime.edit_state.input.items, ); const scan = ui_input.scanInputCursorVertical( &app.input_runtime, .down, app.shell.layout.cols, app.pending_images.items, ); const layout = catalog_screen_layout.screenLayout( app.shell.layout.rows, scan.total_rows, scan.cursor_row, ); const visible_items = settings_menu_presentation.visibleNavigationItemsForBudget( projection, app.shell.layout.cols, layout.menu_row_budget, ); return menu.move(&snapshot, app.input_runtime.edit_state.input.items, delta, visible_items); } fn routeHelpMenuMove(app: *App, delta: i32) bool { if (!app.input_runtime.help_menu.active) return false; const projection = render_input.helpMenuProjection( &app.input_runtime.help_menu, app.slashRegistry(), app.input_runtime.edit_state.input.items, ); const scan = ui_input.scanInputCursorVertical( &app.input_runtime, .down, app.shell.layout.cols, app.pending_images.items, ); const layout = catalog_screen_layout.screenLayout( app.shell.layout.rows, scan.total_rows, scan.cursor_row, ); const visible_items = help_menu_presentation.visibleNavigationItemsForBudget( projection, app.shell.layout.cols, layout.menu_row_budget, ); return app.input_runtime.help_menu.move( app.slashRegistry(), app.input_runtime.edit_state.input.items, delta, visible_items, ); } fn routeModelMenuMove(app: *App, delta: i32) bool { if (comptime !@hasField(App, "model_cache")) return false; if (!app.model_cache.menu.active) return false; _ = app.model_cache.menu.moveVisibleItems(delta, modelMenuVisibleItems(app)); return true; } fn modelMenuVisibleItems(app: *App) u16 { const scan = ui_input.scanInputCursorVertical( &app.input_runtime, .down, app.shell.layout.cols, app.pending_images.items, ); const layout = catalog_screen_layout.screenLayout( app.shell.layout.rows, scan.total_rows, scan.cursor_row, ); return model_menu_presentation.visibleNavigationItemsForBudget( render_input.modelMenuProjection(&app.model_cache), layout.menu_row_budget, ); } fn routeSessionPickerMove(app: *App, delta: i32) bool { if (comptime !@hasField(App, "session_persistence")) return false; if (app.stream.active) return false; if (!app.session_persistence.session_picker.active) return false; _ = app_session_runtime.Runtime(App).moveSessionPicker(app, delta, sessionPickerVisibleItems(app)); return true; } fn routeAuthPickerMove(app: *App, delta: i32) bool { if (comptime !@hasField(App, "auth")) return false; if (app.stream.active) return false; return app.auth.movePicker(delta); } fn routeSkillsMenuMove(app: *App, delta: i32) bool { if (comptime !@hasField(App, "skills")) return false; return app.skills.moveMenuSelectionVisibleRows(delta, skillsMenuVisibleRows(app)); } fn skillsMenuVisibleRows(app: *App) u16 { const scan = ui_input.scanInputCursorVertical( &app.input_runtime, .down,Lines 1–500 of 1805. Load more
const std = @import("std"); const runtime_profile = @import("../hosts/runtime_profile.zig"); const edit_contract = @import("../input/editor_state.zig"); const file_picker_path = @import("../input/file_picker_path.zig"); const horizontal_navigation = @import("../input/horizontal_navigation.zig"); const picker_state = @import("../input/picker_state.zig"); const core_input_runtime = @import("../input/runtime.zig"); const debug_trace = @import("../shared/debug_trace.zig"); const display_width = @import("../shared/display_width.zig"); const list_window = @import("../shared/list_window.zig"); const model_capabilities = @import("../config/model_capabilities.zig"); const command_specs = @import("../slash_commands/command_specs.zig"); const sandbox = @import("../permissions/sandbox.zig"); const session_commands = @import("../session/session_commands.zig"); const skill_runtime = @import("../skills/skill_runtime.zig"); const text_utils = @import("../shared/text_utils.zig"); const file_index = @import("../workspace/file_index.zig"); const app_workspace_runtime = @import("app_workspace_runtime.zig"); const app_session_runtime = @import("app_session_runtime.zig"); const app_commands = @import("app_commands.zig"); const input_queue_runtime = @import("input_queue_runtime.zig"); const input_limit_feedback = @import("input_limit_feedback.zig"); const types = @import("../shared/types.zig"); const ui_input = @import("../../ui/input/runtime.zig"); const visual_layout = @import("../../ui/input/visual_layout.zig"); const input_presentation = @import("../../ui/footer/input_presentation.zig"); const picker_presentation = @import("../../ui/footer/picker_presentation.zig"); const render_input = @import("../../ui/footer/render_input.zig"); const skills_menu_presentation = @import("../../ui/footer/skills_menu_presentation.zig"); const model_menu_presentation = @import("../../ui/footer/model_menu_presentation.zig"); const resume_menu_presentation = @import("../../ui/footer/resume_menu_presentation.zig"); const help_menu_presentation = @import("../../ui/footer/help_menu_presentation.zig"); const settings_menu_presentation = @import("../../ui/footer/settings_menu_presentation.zig"); const catalog_screen_layout = @import("../../ui/catalog_screen_layout.zig"); const ModelPickerStage = picker_state.ModelPickerStage; pub const file_picker_completion_cap: usize = 32; const file_picker_match_span_cap = file_picker_completion_cap * file_index.max_path_len; pub const file_picker_path_storage_cap = file_picker_completion_cap * file_index.max_path_len; pub const InlineSkillCompletion = struct { skill: skill_runtime.Skill, suffix: []const u8, replace_start: usize, replace_end: usize, }; pub const InlineSlashCompletion = struct { suffix: []const u8, }; pub const InlineCompletion = union(enum) { skill: InlineSkillCompletion, slash: InlineSlashCompletion, pub fn suffix(self: InlineCompletion) []const u8 { return switch (self) { .skill => |completion| completion.suffix, .slash => |completion| completion.suffix, }; } }; pub fn CompletionRuntime(comptime App: type) type { return struct { const queue_rt = input_queue_runtime.Runtime(App); const FilePickerSelection = struct { query: picker_state.FilePickerQuery, choice: file_index.SearchResult, }; pub fn slashCompletionCandidateCount(app: *App) usize { const prefix = command_specs.slashCompletionPrefix( app.slashRegistry(), app.input_runtime.edit_state.input.items, ) orelse return 0; if (comptime @hasField(App, "skills")) { return picker_presentation.mixedSlashCompletionCount( app.slashRegistry(), prefix, app.skills.items, ); } return command_specs.slashCompletionCount(app.slashRegistry(), prefix); } fn slashCompletionQueryActive(app: *App) bool { if (app.input_runtime.picker.isInlinePickerDismissed(.slash)) return false; if (app.input_runtime.picker.inlinePickerTriggerKind(&app.input_runtime.edit_state) != .slash) return false; // Model query always owns this slot. Mid-turn bare `/model` does too // (list stays hidden); idle bare `/model` still surfaces slash rows. if (comptime @hasField(App, "stream")) { if (app.stream.active and picker_state.isBareModelCommandAtCursor(&app.input_runtime.edit_state)) return false; } return command_specs.slashCompletionPrefix( app.slashRegistry(), app.input_runtime.edit_state.input.items, ) != null; } pub fn slashCompletionCount(app: *App) usize { if (!slashCompletionQueryActive(app)) return 0; return slashCompletionCandidateCount(app); } fn visibleSlashCompletionQueryActive(app: *App) bool { if (comptime @hasField(App, "question_prompt")) { if (app.question_prompt.isActive()) return false; } if (comptime @hasField(App, "approval_prompt")) { if (app.approval_prompt.isActive()) return false; } if (hasFileQuery(app)) return false; return slashCompletionQueryActive(app); } /// Returns selectable completions projected by the footer. The active /// query can remain visible at zero so dismissal still owns Escape. pub fn visibleSlashCompletionCount(app: *App) usize { if (!visibleSlashCompletionQueryActive(app)) return 0; return slashCompletionCandidateCount(app); } pub fn dismissVisibleInlinePicker(app: *App) bool { const kind = visibleInlinePickerKind(app) orelse return false; app.input_runtime.picker.dismissInlinePicker(kind); return true; } fn visibleInlinePickerKind(app: *App) ?picker_state.InlinePickerKind { if (comptime @hasField(App, "question_prompt")) { if (app.question_prompt.isActive()) return null; } if (comptime @hasField(App, "approval_prompt")) { if (app.approval_prompt.isActive()) return null; } if (comptime @hasField(App, "stream")) { if (app.stream.active) { if (queueReviewOwnsComposer(app)) { return if (hasFileQuery(app)) .file else null; } if (visibleInlineSlashCompletion(app) != null) return .slash; if (visibleSlashCompletionQueryActive(app)) return .slash; return null; } } if (hasModelQuery(app)) return .model; if (hasFileQuery(app)) return .file; if (visibleInlineCompletion(app)) |completion| { return switch (completion) { .skill => .skill, .slash => .slash, }; } if (visibleSlashCompletionQueryActive(app)) return .slash; return null; } fn navigateSlashCompletion(app: *App, delta: i32, count: usize) void { navigatePickerOptions(&app.input_runtime.picker.slash_completion_index, &app.input_runtime.picker.slash_completion_window_start, count, delta); } pub fn routeSlashCompletionMove(app: *App, delta: i32) bool { const count = visibleSlashCompletionCount(app); if (count == 0) return false; navigateSlashCompletion(app, delta, count); return true; } pub fn routeModifiedHistory(app: *App, direction: visual_layout.Direction, delta: i32) !void { app.input_runtime.vertical_navigation.reset(); if (routeNonSlashPickerMove(app, delta)) { return; } else if (!routeSlashCompletionMove(app, delta)) { if (app.input_runtime.composer_history.activeIndex() != null) { try navigatePromptHistory(app, delta); } else if (direction == .up and app.input_runtime.edit_state.cursor > 0) { _ = horizontal_navigation.move( .draft_start, &app.input_runtime.edit_state, &app.input_runtime.entities, &app.input_runtime.vertical_navigation, ); } else if (direction == .up) { try navigatePromptHistory(app, -1); } else if (app.input_runtime.edit_state.cursor < app.input_runtime.edit_state.input.items.len) { _ = horizontal_navigation.move( .draft_end, &app.input_runtime.edit_state, &app.input_runtime.entities, &app.input_runtime.vertical_navigation, ); } else { try navigatePromptHistory(app, 1); } } } pub fn routePlainVertical(app: *App, direction: visual_layout.Direction, delta: i32) !void { try routeComposerVertical(app, direction, delta, false, false); } pub fn routeComposerVertical( app: *App, direction: visual_layout.Direction, delta: i32, extend_selection: bool, page: bool, ) !void { const row_count = if (page) input_presentation.inputRowLimit(app.shell.layout.content_bottom) else @as(usize, 1); const picker_delta = if (page) delta * @as(i32, @intCast(@min(row_count, std.math.maxInt(i32)))) else delta; if (routeNonSlashPickerMove(app, picker_delta)) { app.input_runtime.vertical_navigation.reset(); return; } if (comptime @hasField(App, "queued_prompt_review")) { if (try queue_rt.routeVertical(app, direction)) { app.input_runtime.vertical_navigation.reset(); return; } } const scan = if (page) ui_input.scanInputCursorRows( &app.input_runtime, direction, row_count, app.shell.layout.cols, app.pending_images.items, ) else ui_input.scanInputCursorVertical( &app.input_runtime, direction, app.shell.layout.cols, app.pending_images.items, ); const capped = input_presentation.cappedInputRows( scan.total_rows, app.shell.layout.content_bottom, true, ); const visible_slash_count = if (capped.input_extra == 0) visibleSlashCompletionCount(app) else 0; if (visible_slash_count > 0) { app.input_runtime.vertical_navigation.reset(); navigateSlashCompletion(app, picker_delta, visible_slash_count); return; } if (app.input_runtime.vertical_navigation.applyTargetWithSelection( &app.input_runtime.edit_state, &app.input_runtime.entities, if (scan.target) |target| target.raw_offset else null, scan.preferred_column, extend_selection, )) return; if (page or extend_selection) return; if (direction == .up and app.input_runtime.edit_state.cursor > 0) { _ = horizontal_navigation.move( .draft_start, &app.input_runtime.edit_state, &app.input_runtime.entities, &app.input_runtime.vertical_navigation, ); } else if (direction == .up) { try navigatePromptHistory(app, -1); } else if (app.input_runtime.edit_state.cursor < app.input_runtime.edit_state.input.items.len) { _ = horizontal_navigation.move( .draft_end, &app.input_runtime.edit_state, &app.input_runtime.entities, &app.input_runtime.vertical_navigation, ); } else { try navigatePromptHistory(app, 1); } } pub fn navigatePromptHistory(app: *App, delta: i32) !void { const has_image_id_owner = @hasDecl(App, "peekNextImageId") and @hasDecl(App, "nextImageId"); const first_image_id = if (comptime has_image_id_owner) app.peekNextImageId() else 1; const max_input_len = if (comptime @hasDecl(App, "input_limits")) App.input_limits.composer_bytes else if (comptime @hasDecl(App, "input_byte_limit")) App.input_byte_limit else std.math.maxInt(usize); const result = app.input_runtime.composer_history.navigate( app.alloc, delta, .{ .edit = &app.input_runtime.edit_state, .entities = &app.input_runtime.entities, .picker = &app.input_runtime.picker, .vertical_navigation = &app.input_runtime.vertical_navigation, .input_limit_rejection = &app.input_runtime.input_limit_rejection, .images = &app.pending_images, }, first_image_id, max_input_len, ) catch |err| { switch (err) { error.FileNotFound, error.AccessDenied, error.MissingImageSnapshot, error.InvalidImageSnapshotDigest, error.ImageSnapshotPathUnsafe, error.NotRegularFile, error.ImageTooLarge, error.ImageSnapshotCorrupt, error.UnsupportedImageType, error.ImageSnapshotMediaTypeMismatch, => { debug_trace.logf( "prompt_history", "event=recall_rejected err={s}", .{@errorName(err)}, ); return; }, else => return err, } }; switch (result) { .unchanged => {}, .limit_exceeded => |attempted_bytes| try input_limit_feedback.report( App, app, .composer, attempted_bytes, ), .moved => |image_count| { app.input_runtime.historyBoundary(app.alloc); if (comptime has_image_id_owner) { for (0..image_count) |offset| { const committed_id = app.nextImageId(); std.debug.assert(committed_id == first_image_id + offset); } } else { std.debug.assert(image_count == 0); } }, } } pub fn routeVisiblePickerMove(app: *App, delta: i32) bool { if (routeNonSlashPickerMove(app, delta)) return true; return routeSlashCompletionMove(app, delta); } fn routeNonSlashPickerMove(app: *App, delta: i32) bool { if (routeSettingsMenuMove(app, delta)) return true; if (routeHelpMenuMove(app, delta)) return true; if (routeModelMenuMove(app, delta)) return true; if (routeAuthPickerMove(app, delta)) return true; if (routeSkillsMenuMove(app, delta)) return true; if (comptime runtime_profile.allows(App, .durable_sessions)) { if (routeSessionPickerMove(app, delta)) return true; } const stream_suppresses_file_picker = app.stream.active and !queueReviewOwnsComposer(app); if (!stream_suppresses_file_picker and hasFileQuery(app)) { navigateFilePicker(app, delta); return true; } if (hasModelQuery(app)) { if (!app.stream.active) navigateModelPicker(app, delta); return true; } // Mid-turn bare `/model`: consume arrows without slash/skill navigation. if (app.stream.active and picker_state.isBareModelCommandAtCursor(&app.input_runtime.edit_state)) return true; return false; } fn routeSettingsMenuMove(app: *App, delta: i32) bool { const menu = &app.input_runtime.settings_menu; if (!menu.active) return false; if (comptime @hasField(App, "model_cache")) { if (app.model_cache.menu.active) { _ = app.model_cache.menu.moveVisibleItems(delta, 6); return true; } } const snapshot = app_commands.settingsCatalogSnapshot(app); const projection = render_input.settingsMenuProjection( menu, snapshot, app.input_runtime.edit_state.input.items, ); const scan = ui_input.scanInputCursorVertical( &app.input_runtime, .down, app.shell.layout.cols, app.pending_images.items, ); const layout = catalog_screen_layout.screenLayout( app.shell.layout.rows, scan.total_rows, scan.cursor_row, ); const visible_items = settings_menu_presentation.visibleNavigationItemsForBudget( projection, app.shell.layout.cols, layout.menu_row_budget, ); return menu.move(&snapshot, app.input_runtime.edit_state.input.items, delta, visible_items); } fn routeHelpMenuMove(app: *App, delta: i32) bool { if (!app.input_runtime.help_menu.active) return false; const projection = render_input.helpMenuProjection( &app.input_runtime.help_menu, app.slashRegistry(), app.input_runtime.edit_state.input.items, ); const scan = ui_input.scanInputCursorVertical( &app.input_runtime, .down, app.shell.layout.cols, app.pending_images.items, ); const layout = catalog_screen_layout.screenLayout( app.shell.layout.rows, scan.total_rows, scan.cursor_row, ); const visible_items = help_menu_presentation.visibleNavigationItemsForBudget( projection, app.shell.layout.cols, layout.menu_row_budget, ); return app.input_runtime.help_menu.move( app.slashRegistry(), app.input_runtime.edit_state.input.items, delta, visible_items, ); } fn routeModelMenuMove(app: *App, delta: i32) bool { if (comptime !@hasField(App, "model_cache")) return false; if (!app.model_cache.menu.active) return false; _ = app.model_cache.menu.moveVisibleItems(delta, modelMenuVisibleItems(app)); return true; } fn modelMenuVisibleItems(app: *App) u16 { const scan = ui_input.scanInputCursorVertical( &app.input_runtime, .down, app.shell.layout.cols, app.pending_images.items, ); const layout = catalog_screen_layout.screenLayout( app.shell.layout.rows, scan.total_rows, scan.cursor_row, ); return model_menu_presentation.visibleNavigationItemsForBudget( render_input.modelMenuProjection(&app.model_cache), layout.menu_row_budget, ); } fn routeSessionPickerMove(app: *App, delta: i32) bool { if (comptime !@hasField(App, "session_persistence")) return false; if (app.stream.active) return false; if (!app.session_persistence.session_picker.active) return false; _ = app_session_runtime.Runtime(App).moveSessionPicker(app, delta, sessionPickerVisibleItems(app)); return true; } fn routeAuthPickerMove(app: *App, delta: i32) bool { if (comptime !@hasField(App, "auth")) return false; if (app.stream.active) return false; return app.auth.movePicker(delta); } fn routeSkillsMenuMove(app: *App, delta: i32) bool { if (comptime !@hasField(App, "skills")) return false; return app.skills.moveMenuSelectionVisibleRows(delta, skillsMenuVisibleRows(app)); } fn skillsMenuVisibleRows(app: *App) u16 { const scan = ui_input.scanInputCursorVertical( &app.input_runtime, .down,