1805 lines · 79.6 KB · Zig
RawDownload
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.