Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035const 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 file_completion_state = @import("../input/file_completion_state.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 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 provider_runtime = @import("provider_runtime.zig");const provider_picker_runtime = @import("provider_picker_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 surface_frame = @import("../../ui/footer/surface_frame.zig");const footer_paint_plan = @import("../../ui/footer/paint_plan.zig");const render_request = @import("../../ui/render_request.zig");
const ModelPickerStage = picker_state.ModelPickerStage;
pub const FilePickerReceipt = file_completion_state.Receipt;pub const file_picker_completion_cap: usize = file_completion_state.capacity;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 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.isInlinePickerSuppressed(.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; // 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. 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; // Escaping the picker abandons the columns it had opened, so the // next `/provider` starts from the provider column again. if (kind == .provider) provider_picker_runtime.Runtime(App).abandon(app); 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 (hasModelQuery(app)) return .model; if (hasFileQuery(app)) return .file; if (visibleInlineSlashCompletion(app) != null) return .slash; if (visibleSlashCompletionCount(app) > 0) return .slash; return null; } } if (hasModelQuery(app)) return .model; if (provider_picker_runtime.Runtime(App).hasQuery(app)) return .provider; if (hasFileQuery(app)) return .file; if (visibleInlineCompletion(app)) |completion| { return switch (completion) { .skill => .skill, .slash => .slash, }; } if (visibleSlashCompletionCount(app) > 0) 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 (try 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 (try routeNonSlashPickerMove(app, picker_delta)) { 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) { if (try restoreQueuedSteerToComposer(app)) return; 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); } }
/// Pulls the newest queued steering prompt that still waits for a /// boundary back into the empty composer for editing. Returns false when /// no steer is retractable, leaving history navigation to run unchanged. /// An active history episode keeps its stashed draft untouched. fn restoreQueuedSteerToComposer(app: *App) !bool { if (comptime !@hasField(App, "worker")) return false; if (comptime !@hasDecl(@TypeOf(app.worker), "popQueuedSteerForEdit")) return false; if (app.input_runtime.edit_state.input.items.len > 0) return false; if (app.input_runtime.composer_history.activeIndex() != null) return false; const text = (try app.worker.popQueuedSteerForEdit(std.heap.c_allocator)) orelse return false; defer std.heap.c_allocator.free(text); try app.input_runtime.edit_state.setText(app.alloc, text); app.input_runtime.vertical_navigation.reset(); app.input_runtime.historyBoundary(app.alloc); debug_trace.eventf( "input", "queued_steer_restored", .{}, "bytes={d}", .{text.len}, ); app.shell.render_requests.request(.footer); return true; }
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 (try routeNonSlashPickerMove(app, delta)) return true; return routeSlashCompletionMove(app, delta); }
fn routeNonSlashPickerMove(app: *App, delta: i32) !bool { if (try routeSettingsMenuMove(app, delta)) return true; if (try routeHelpMenuMove(app, delta)) return true; if (try routeModelMenuMove(app, delta)) return true; if (routeAuthPickerMove(app, delta)) return true; if (try routeSkillsMenuMove(app, delta)) return true; if (comptime runtime_profile.allows(App, .durable_sessions)) { if (try routeSessionPickerMove(app, delta)) return true; } if (hasFileQuery(app)) { navigateFilePicker(app, delta); return true; } if (hasModelQuery(app)) { navigateModelPicker(app, delta); return true; } if (provider_picker_runtime.Runtime(App).hasQuery(app)) { if (!app.stream.active) provider_picker_runtime.Runtime(App).navigate(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; const snapshot = app_commands.settingsCatalogSnapshot(app); var projection = render_input.settingsMenuProjection( menu, snapshot, app.input_runtime.edit_state.input.items, ); if (comptime @hasField(App, "model_cache")) { if (app.model_cache.menu.active) { projection.models = render_input.modelMenuProjection(&app.model_cache); _ = app.model_cache.menu.moveVisibleItems( delta, @max( settings_menu_presentation.visibleModelItemsForBudget( projection, app.shell.layout.cols, try inlineMenuRowBudget(app, settings_menu_presentation.max_inline_rows), ), 1, ), ); return true; } } const visible_items = settings_menu_presentation.visibleNavigationItemsForBudget( projection, app.shell.layout.cols, try inlineMenuRowBudget(app, settings_menu_presentation.max_inline_rows), ); 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 visible_items = help_menu_presentation.visibleNavigationItemsForBudget( projection, app.shell.layout.cols, try inlineMenuRowBudget(app, help_menu_presentation.max_inline_rows), ); 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, model_menu_presentation.visibleNavigationItemsForBudget( render_input.modelMenuProjection(&app.model_cache), try inlineMenuRowBudget(app, model_menu_presentation.max_inline_rows), ), ); return true; }
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, try 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, try skillsMenuVisibleRows(app)); }
fn skillsMenuVisibleRows(app: *App) !u16 { const projection = render_input.skillsMenuProjection(&app.skills); return skills_menu_presentation.inlineVisibleNavigationRowsForBudget( projection, try inlineMenuRowBudget(app, input_presentation.max_model_picker_rows + 2), ); }
fn inlineMenuRowBudget(app: *App, max_picker_rows: u16) !u16 { const budget = try footerRowBudget(app); return picker_presentation.inlinePickerRowBudgetCapped( app.shell.layout.rows, budget.input_extra, budget.banner_rows, max_picker_rows, ); }
const FooterRowBudget = struct { input_extra: u16, banner_rows: u16, };
fn footerRowBudget(app: *App) !FooterRowBudget { const scan = ui_input.scanInputCursorVertical( &app.input_runtime, .down, app.shell.layout.cols, app.pending_images.items, ); const capped = input_presentation.cappedInputRows( scan.total_rows, app.shell.layout.content_bottom, true, ); if (comptime !@hasDecl(@TypeOf(app.worker), "snapshotSteeringPresentation")) { return .{ .input_extra = capped.input_extra, .banner_rows = 0 }; } var steering = try app.worker.snapshotSteeringPresentation(app.alloc); defer steering.deinit(app.alloc); const requested_banner_rows = render_input.steeringBannerRowsForMessages( steering.messages, steering.waits_for_boundary, app.shell.layout.cols, ); return .{ .input_extra = capped.input_extra, .banner_rows = surface_frame.clampSteeringBannerRows( requested_banner_rows, app.shell.layout.rows, true, footer_paint_plan.composerTopChromeRows(), capped.input_extra, ), }; }
pub fn syncSessionPickerWindowStart(app: *App) !void { if (comptime !@hasField(App, "session_persistence")) return; const picker = &app.session_persistence.session_picker; const item_count = picker.navigationItemCount(); if (!picker.active or item_count == 0) { picker.window_start = 0; return; } picker.syncWindowStart(try sessionPickerVisibleItems(app)); }
fn sessionPickerVisibleItems(app: *App) !u16 { const picker = &app.session_persistence.session_picker; const projection: render_input.SessionMenuProjection = .{ .active = picker.active, .load_state = picker.load_state, .summaries = picker.summaries.items, .has_more = picker.has_more, .loading_more = picker.loading_more, .scope = picker.scope, .selected_index = picker.selected, .window_start = picker.window_start, .query = picker.query(), .selection_failure = picker.selection_failure, }; return resume_menu_presentation.visibleNavigationItemsForBudget( projection, try inlineMenuRowBudget(app, resume_menu_presentation.max_inline_rows), ); }
pub fn hasModelQuery(app: *App) bool { return app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) != null; }
pub fn hasFileQuery(app: *App) bool { if (comptime !runtime_profile.allows(App, .file_index)) return false; return app.input_runtime.picker.activeFilePickerQuery(&app.input_runtime.edit_state) != null; }
pub fn visibleInlineSkillCompletion(app: *App) ?InlineSkillCompletion { if (comptime !@hasField(App, "skills")) return null; if (!inlineCompletionSurfaceAvailable(app)) return null; if (app.input_runtime.picker.inlinePickerTriggerKind(&app.input_runtime.edit_state) != .skill) return null; const query = app.input_runtime.picker.activeInlineSkillQuery(&app.input_runtime.edit_state) orelse return null; const match = skill_runtime.firstSkillNameCompletion(app.skills.items, query.query) orelse return null; if (!inlineSuffixCanRender(app, match.suffix)) return null; return .{ .skill = match.skill, .suffix = match.suffix, .replace_start = query.dollar_offset, .replace_end = query.token_start + query.query.len, }; }
pub fn visibleInlineSlashCompletion(app: *App) ?InlineSlashCompletion { if (!inlineCompletionSurfaceAvailable(app)) return null; if (app.input_runtime.picker.inlinePickerTriggerKind(&app.input_runtime.edit_state) != .slash) return null; const query = app.input_runtime.picker.activeInlineSlashQuery(&app.input_runtime.edit_state) orelse return null; const command = command_specs.firstSlashCompletion(app.slashRegistry(), query.prefix) orelse return null; const suffix = command[query.prefix.len..]; if (!inlineSuffixCanRender(app, suffix)) return null; return .{ .suffix = suffix }; }
pub fn visibleInlineCompletion(app: *App) ?InlineCompletion { if (visibleInlineSkillCompletion(app)) |completion| return .{ .skill = completion }; if (visibleInlineSlashCompletion(app)) |completion| return .{ .slash = completion }; return null; }
pub fn autocompleteInlineCompletion(app: *App, max_input_len: usize) !edit_contract.InsertResult { const completion = visibleInlineCompletion(app) orelse return .inactive; return switch (completion) { .skill => |skill| bindInlineSkillCompletion(app, skill, max_input_len), .slash => |slash| app.input_runtime.insertionState().insertSliceBounded( app.alloc, slash.suffix, max_input_len, .preserve, ), }; }
pub fn autocompleteInlineSkillCompletion(app: *App, max_input_len: usize) !edit_contract.InsertResult { const completion = visibleInlineSkillCompletion(app) orelse return .inactive; return bindInlineSkillCompletion(app, completion, max_input_len); }
fn bindInlineSkillCompletion( app: *App, completion: InlineSkillCompletion, max_input_len: usize, ) !edit_contract.InsertResult { const inserted_len = app.input_runtime.entities.skillTokenInsertedLen( app.input_runtime.edit_state.input.items, completion.replace_end, completion.skill.name, ); if (!app.input_runtime.insertionState().canReplaceRange( completion.replace_start, completion.replace_end, inserted_len, max_input_len, )) { return .limit_exceeded; }
try app.input_runtime.skillBindingState().bindSkillToken( app.alloc, completion.replace_start, completion.replace_end, completion.skill.name, completion.skill.path, skill_runtime.skillDisplaySource(app.skills.items, completion.skill), ); app.input_runtime.picker.resetInlinePickerEpisode(); return .inserted; }
fn inlineSuffixCanRender(app: *App, suffix: []const u8) bool { if (comptime !@hasField(@TypeOf(app.shell), "layout")) return false; const terminal_cols = app.shell.layout.cols; if (terminal_cols == 0) return false;
const cursor = visual_layout.cursorPointAt(.{ .input = app.input_runtime.edit_state.input.items, .cursor = app.input_runtime.edit_state.cursor, .terminal_cols = terminal_cols, .images = if (comptime @hasField(App, "pending_images")) app.pending_images.items else &.{}, .pasted_blocks = app.input_runtime.entities.pasted_blocks.items, .image_tokens = app.input_runtime.entities.image_tokens.items, .skill_tokens = app.input_runtime.entities.skill_tokens.items, }, app.input_runtime.edit_state.cursor); const used_cells = visual_layout.inputPrefix(cursor.row_index).cell_width +| cursor.content_column; const available_cells = @as(usize, terminal_cols) -| used_cells; const first_unit = display_width.displayUnitAt(suffix, 0); return first_unit.byte_len > 0 and first_unit.cell_width > 0 and first_unit.cell_width <= available_cells; }
fn inlineCompletionSurfaceAvailable(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 (catalogMenuOwnsSurface(app)) return false; return true; }
fn catalogMenuOwnsSurface(app: *App) bool { if (app.input_runtime.help_menu.active or app.input_runtime.settings_menu.active) return true; if (comptime @hasField(App, "skills")) { if (comptime @hasField(@TypeOf(app.skills), "menu")) { if (app.skills.menuVisible()) return true; } } if (comptime @hasField(App, "model_cache")) { if (app.model_cache.menu.active) return true; } if (comptime @hasField(App, "session_persistence")) { if (app.session_persistence.session_picker.active) return true; } return false; }
fn fileScopeEpoch(app: *const App) u64 { if (comptime @hasDecl(App, "fileCompletionScopeEpoch")) return app.fileCompletionScopeEpoch(); return 0; }
fn filePickerEligible(app: *App) bool { if (comptime !runtime_profile.allows(App, .file_index)) return false; 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 (comptime @hasField(App, "auth")) { if (comptime @hasDecl(@TypeOf(app.auth), "pickerView")) if (app.auth.pickerView().active) return false; } if (comptime @hasField(App, "shell")) { if (comptime @hasDecl(@TypeOf(app.shell), "fullTranscriptActive")) if (app.shell.fullTranscriptActive()) return false; } return !catalogMenuOwnsSurface(app); }
pub fn reconcileFilePicker(app: *App) void { if (comptime !runtime_profile.allows(App, .file_index)) return; const picker = &app.input_runtime.picker; const query = picker.activeFilePickerQuery(&app.input_runtime.edit_state); var decoded_storage: [file_index.max_path_len]u8 = undefined; const indexed = if (query) |q| blk: { const decoded = q.decoded_query(&decoded_storage) catch break :blk true; break :blk fileCompletionsDependOnIndex(app, decoded); } else true; if (picker.file_completion.reconcile(query, fileScopeEpoch(app), indexed)) picker.resetFilePickerIndex(); const eligible = filePickerEligible(app); if (!eligible) picker.file_completion.distrust(); if (comptime @hasDecl(App, "reconcileDirectoryCompletion")) app.reconcileDirectoryCompletion(eligible); if (comptime @hasField(App, "shell")) { if (comptime @hasField(@TypeOf(app.shell), "render_requests")) { if (app.shell.render_requests.resizeLifecyclePending()) picker.file_completion.distrust(); } if (comptime @hasField(@TypeOf(app.shell), "terminal_reset_pending")) { if (app.shell.terminal_reset_pending or app.shell.terminal_dimensions_invalid) picker.file_completion.distrust(); } } }
pub fn collectFilePickerFacts(app: *App) void { if (comptime !runtime_profile.allows(App, .file_index)) return; reconcileFilePicker(app); app.harvestDirectoryCompletion(filePickerEligible(app)); }
/// The sole acquisition boundary. Called after input batching, never by /// painters, navigation or acceptance. Directory work is scheduled, not run. pub fn prepareFilePicker(app: *App) void { if (comptime !runtime_profile.allows(App, .file_index)) return; reconcileFilePicker(app); const picker = &app.input_runtime.picker; const state = &picker.file_completion; if (!state.active or !filePickerEligible(app)) return; if (state.refresh_requested) { state.refresh_requested = false; if (state.indexed) { var refresh = true; if (!picker.file_picker_episode_seen) { picker.file_picker_episode_seen = true; if (comptime @hasDecl(App, "isFileIndexLoading")) refresh = !app.isFileIndexLoading(); } if (refresh) { if (comptime @hasDecl(App, "refreshFileIndex")) app.refreshFileIndex(); } } } var source: file_index.ReadableRevision = .{ .scope_epoch = fileScopeEpoch(app), .state = .ready }; if (state.indexed) { if (comptime @hasDecl(App, "fileCompletionRevision")) { source = app.fileCompletionRevision(); } else { if (comptime @hasDecl(App, "isFileIndexLoading")) { if (app.isFileIndexLoading()) source.state = .loading; } if (comptime @hasDecl(App, "isFileIndexFailed")) { if (app.isFileIndexFailed()) source.state = .failed; } } } if (!state.needsLookup(source)) return; const query = picker.activeFilePickerQuery(&app.input_runtime.edit_state) orelse return; var decoded_storage: [file_index.max_path_len]u8 = undefined; const decoded = query.decoded_query(&decoded_storage) catch { state.stage(app.alloc, source, .unavailable, null); return; }; if (decoded.len > file_index.max_path_len or source.scope_epoch != fileScopeEpoch(app)) { state.stage(app.alloc, source, .unavailable, null); return; } if (!state.indexed) { app.prepareDirectoryCompletion(); return; } var results: [file_picker_completion_cap]file_index.SearchResult = undefined; var spans: [file_picker_match_span_cap]file_index.MatchSpan = undefined; var paths: [file_picker_path_storage_cap]u8 = undefined; const lookup = if (comptime @hasDecl(App, "fileCompletionsAtRevision")) app.fileCompletionsAtRevision(source, decoded, &results, &spans, &paths) else app.fileCompletions(decoded, &results, &spans, &paths); const count = lookup catch |err| { debug_trace.logf("input", "file picker preparation failed err={s}", .{@errorName(err)}); state.stage(app.alloc, source, .unavailable, null); return; }; const status: file_completion_state.Status = if (count > 0) .ready else switch (source.state) { .loading => .loading, .failed => .unavailable, .idle, .ready => .empty, }; const rows = file_completion_state.Rows.copy(app.alloc, results[0..count]) catch |err| { debug_trace.logf("input", "file picker result copy failed err={s}", .{@errorName(err)}); state.stage(app.alloc, source, .unavailable, null); return; }; state.stage(app.alloc, source, status, rows); debug_trace.logf("input", "file picker prepared revision={d} count={d} indexed={}", .{ state.next_revision -% 1, count, state.indexed }); app.shell.render_requests.request(.footer); }
pub fn filePickerView(app: *App) file_completion_state.View { if (comptime !runtime_profile.allows(App, .file_index)) return .{}; const picker = &app.input_runtime.picker; return picker.file_completion.view(picker.file_completion_index, picker.file_completion_window_start); }
pub fn acknowledgeFilePicker(app: *App, receipt: FilePickerReceipt) void { if (comptime !runtime_profile.allows(App, .file_index)) return; if (!filePickerEligible(app)) return; const picker = &app.input_runtime.picker; picker.file_completion.acknowledge(app.alloc, receipt, &picker.file_completion_index, &picker.file_completion_window_start); }
pub fn distrustFilePicker(app: *App) void { if (comptime runtime_profile.allows(App, .file_index)) app.input_runtime.picker.file_completion.distrust(); }
fn navigateFilePicker(app: *App, delta: i32) void { reconcileFilePicker(app); if (!filePickerEligible(app)) return; const picker = &app.input_runtime.picker; picker.file_completion.navigate(&picker.file_completion_index, &picker.file_completion_window_start, delta); }
pub fn autocompleteFilePickerSelection(app: *App, max_input_len: usize) !edit_contract.InsertResult { if (comptime !runtime_profile.allows(App, .file_index)) return .inactive; reconcileFilePicker(app); if (!filePickerEligible(app)) return .inactive; const selection = resolveFilePickerSelection(app) orelse { const picker = &app.input_runtime.picker; const status = filePickerView(app).status; if (status != .loading and (status == .unavailable or status == .stale or picker.file_completion.selection_missing)) picker.file_completion.retry(); app.shell.render_requests.request(.footer); return .inactive; }; return applyFilePickerSelection(app, selection, max_input_len); }
fn resolveFilePickerSelection(app: *App) ?FilePickerSelection { const picker = &app.input_runtime.picker; const query = picker.activeFilePickerQuery(&app.input_runtime.edit_state) orelse return null; const choice = picker.file_completion.selected(picker.file_completion_index) orelse return null; return .{ .query = query, .choice = choice }; }
fn applyFilePickerSelection( app: *App, selection: FilePickerSelection, max_input_len: usize, ) !edit_contract.InsertResult { if (!file_picker_path.isRepresentable(selection.choice.path)) { debug_trace.logf("input", "file picker rejected unrepresentable selection bytes={d}", .{selection.choice.path.len}); app.input_runtime.picker.resetFilePickerIndex(); return .inactive; } var query_storage: [file_index.max_path_len]u8 = undefined; const decoded_query = try selection.query.decoded_query(&query_storage); const indexed = fileCompletionsDependOnIndex(app, decoded_query); if (!isCurrentFileCompletion(app, decoded_query, selection.choice.path, selection.choice.kind)) { debug_trace.logf("input", "file picker rejected stale selection bytes={d} kind={s}", .{ selection.choice.path.len, @tagName(selection.choice.kind) }); app.input_runtime.picker.file_completion.rejectSelection(); return .inactive; }
const items = app.input_runtime.edit_state.input.items; const replace_start = selection.query.at_offset; const replace_end = selection.query.replace_end; const tail = items[replace_end..]; const reuses_terminator = tail.len > 0 and picker_state.isFilePickerTerminator(tail[0]); const is_directory = selection.choice.kind == .directory; const adds_terminator = !is_directory and !reuses_terminator; const encoded = try file_picker_path.encode(app.alloc, selection.choice.path, .{ .quoted = selection.query.quoted, .directory = is_directory, .workspace_relative = indexed, }); var replacement = std.ArrayList(u8).fromOwnedSlice(encoded); defer replacement.deinit(app.alloc); const separator_offset = replace_start + replacement.items.len; if (adds_terminator) try replacement.append(app.alloc, ' '); const cursor_after_replacement = replace_start + replacement.items.len + @intFromBool(!is_directory and reuses_terminator);
const result = try app.input_runtime.replacementState(&app.pending_images).replaceRangeBounded( app.alloc, .{ .start = replace_start, .end = replace_end }, replacement.items, cursor_after_replacement, max_input_len, ); if (result == .inserted) { if (adds_terminator) { app.input_runtime.entities.markFileCompletionSeparator( app.input_runtime.edit_state.input.items, app.input_runtime.edit_state.cursor, separator_offset, ); } if (is_directory) app.input_runtime.picker.resetFilePickerIndex(); } return result; }
pub fn submitFilePickerOnEnter(app: *App, max_input_len: usize) !?edit_contract.InsertResult { if (comptime !runtime_profile.allows(App, .file_index)) return null; reconcileFilePicker(app); if (!filePickerOwnsSurface(app)) return null; const selection = resolveFilePickerSelection(app) orelse { if (app.input_runtime.picker.file_completion.acceptedEmpty()) return null; return .inactive; }; const result = try applyFilePickerSelection(app, selection, max_input_len); debug_trace.logf( "input", "file picker Enter consumed selected={s} limit_exceeded={s}", .{ if (result == .inserted) "true" else "false", if (result == .limit_exceeded) "true" else "false", }, ); return result; }
pub fn filePickerOwnsSurface(app: *App) bool { return filePickerEligible(app) and hasFileQuery(app); }
fn isCurrentFileCompletion(app: *App, query: []const u8, path: []const u8, kind: file_index.CandidateKind) bool { if (comptime @hasDecl(App, "isCurrentFileCompletion")) { return app.isCurrentFileCompletion(query, path, kind); } return true; }
fn fileCompletionsDependOnIndex(app: *App, query: []const u8) bool { if (comptime @hasDecl(App, "fileCompletionsDependOnIndex")) { return app.fileCompletionsDependOnIndex(query); } return true; }
pub fn navigateModelPicker(app: *App, delta: i32) void { const query = app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) orelse return; switch (query.stage) { .model => navigateModelCompletion(app, query.query, delta), .effort => { var values: [types.ReasoningEffort.max_options + 1]types.ReasoningEffort = undefined; navigatePickerOptions(&app.input_runtime.picker.model_picker_effort_index, &app.input_runtime.picker.model_picker_effort_window_start, effortCompletionValues(app, query.query, &values), delta); }, .fast => { var labels: [2][]const u8 = undefined; navigatePickerOptions(&app.input_runtime.picker.model_picker_fast_index, &app.input_runtime.picker.model_picker_fast_window_start, fastCompletionLabels(query.query, &labels), delta); }, } }
fn navigateModelCompletion(app: *App, query: []const u8, delta: i32) void { var buf: [32][]const u8 = undefined; const count = modelPickerCompletions(app, query, &buf); if (count == 0) return; app.input_runtime.picker.model_completion_index = modelPickerIndex(app, buf[0..count]); app.input_runtime.picker.model_completion_window_start = modelPickerWindowStart(app, count, app.input_runtime.picker.model_completion_index); app.input_runtime.picker.model_completion_anchor_current = false; navigatePickerOptions(&app.input_runtime.picker.model_completion_index, &app.input_runtime.picker.model_completion_window_start, count, delta); }
pub fn modelPickerCompletions(app: *App, query: []const u8, out: *[32][]const u8) usize { const count = app.modelCompletions(query, out); if (comptime !provider_runtime.supported(App)) return count; if (!app.input_runtime.picker.model_completion_anchor_current or query.len != 0) return count;
const selected_model = provider_runtime.model(app); if (modelCompletionIndex(out[0..count], selected_model) == null) { if (catalogModelCompletion(app, selected_model)) |current| { if (count < out.len) { out[count] = current; return count + 1; } if (count > 0) out[count - 1] = current; } } return count; }
fn catalogModelCompletion(app: *App, model: []const u8) ?[]const u8 { if (comptime @hasDecl(App, "catalogModelCompletion")) return app.catalogModelCompletion(model);
var buf: [32][]const u8 = undefined; const count = app.modelCompletions(model, &buf); const index = modelCompletionIndex(buf[0..count], model) orelse return null; return buf[index]; }
pub fn modelPickerIndex(app: *App, completions: []const []const u8) usize { if (completions.len == 0) return 0; if (comptime !provider_runtime.supported(App)) return app.input_runtime.picker.model_completion_index % completions.len; if (app.input_runtime.picker.model_completion_anchor_current) { if (modelCompletionIndex(completions, provider_runtime.model(app))) |index| return index; } return app.input_runtime.picker.model_completion_index % completions.len; }
pub fn modelPickerWindowStart(app: *App, count: usize, index: usize) usize { if (app.input_runtime.picker.model_completion_anchor_current) { return list_window.updateEdgeStart(0, count, index, list_window.default_max_picker_rows); } return app.input_runtime.picker.model_completion_window_start; }
fn modelCompletionIndex(completions: []const []const u8, model: []const u8) ?usize { for (completions, 0..) |candidate, index| { if (std.mem.eql(u8, candidate, model)) return index; } return null; }
const navigatePickerOptions = list_window.advanceSelection;
fn setModelComposerText(app: *App, comptime fmt: []const u8, args: anytype) !void { const text = try std.fmt.allocPrint(app.alloc, fmt, args); defer app.alloc.free(text); try app.input_runtime.textReplacementState().replace(app.alloc, text); }
pub fn autocompleteModelPickerSelection(app: *App) !void { const query = app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) orelse return; switch (query.stage) { .model => { const model = selectedModelCompletion(app) orelse return; try setModelComposerText(app, "/model {s}", .{model}); }, .effort => { const model = try app.alloc.dupe(u8, app.input_runtime.picker.model_picker_pending_model.items); defer app.alloc.free(model); const effort = selectedEffortCompletion(app) orelse return; const capabilities = model_capabilities.resolveForApp(App, app, model); try setModelComposerText(app, "/model {s} {s}", .{ model, effort.label() }); try app.input_runtime.picker.beginModelPickerFlow( app.alloc, model, model_capabilities.reasoningEffortIndex(capabilities, effort), capabilities.supports_fast_mode, .effort, ); }, .fast => { const model = try app.alloc.dupe(u8, app.input_runtime.picker.model_picker_pending_model.items); defer app.alloc.free(model); const capabilities = model_capabilities.resolveForApp(App, app, model); const effort = model_capabilities.reasoningEffortAtIndex(capabilities, app.input_runtime.picker.model_picker_effort_index); const effort_index = model_capabilities.reasoningEffortIndex(capabilities, effort); const fast_mode = selectedFastCompletion(app) orelse return; const fast_label = if (fast_mode) picker_state.model_picker_fast_options[1] else picker_state.model_picker_fast_options[0]; try setModelComposerText(app, "/model {s} {s} {s}", .{ model, effort.label(), fast_label }); try app.input_runtime.picker.beginModelPickerFlow(app.alloc, model, effort_index, fast_mode, .fast); }, } app.shell.render_requests.request(.footer); }
pub fn advanceModelPickerOnSpace(app: *App) !bool { const query = app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) orelse return false; if (app.input_runtime.edit_state.cursor != app.input_runtime.edit_state.input.items.len) return false;
switch (query.stage) { .model => { const model = exactModelCompletion(app, query.query) orelse return false; try beginModelPickerOptions(app, model); return true; }, .effort => { if (!app.input_runtime.picker.hasPendingModelPickerSelection()) return false; const model = try app.alloc.dupe(u8, app.input_runtime.picker.model_picker_pending_model.items); defer app.alloc.free(model); const effort = exactEffortCompletion(app, query.query) orelse return false; const capabilities = model_capabilities.resolveForApp(App, app, model); if (!capabilities.supports_fast_mode) return false;
try setModelComposerText(app, "/model {s} {s} ", .{ model, effort.label() }); try app.input_runtime.picker.beginModelPickerFlow(app.alloc, model, model_capabilities.reasoningEffortIndex(capabilities, effort), true, .fast); return true; }, .fast => return false, } }
fn exactModelCompletion(app: *App, raw_query: []const u8) ?[]const u8 { const query = std.mem.trim(u8, raw_query, " \t"); if (query.len == 0) return null;
var buf: [32][]const u8 = undefined; const count = modelPickerCompletions(app, query, &buf); for (buf[0..count]) |candidate| { if (std.ascii.eqlIgnoreCase(candidate, query)) return candidate; } return null; }
pub fn submitModelPicker(app: *App) !bool { switch (app.input_runtime.picker.model_picker_stage) { .model => { if (app.isModelCacheLoading()) { const query = app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) orelse return false; if (std.mem.trim(u8, query.query, " \t").len > 0) return false; app.shell.render_requests.request(.footer); return true; } const model = selectedModelCompletion(app) orelse return false; try beginModelPickerOptions(app, model); return true; }, .effort => { if (!app.input_runtime.picker.hasPendingModelPickerSelection()) return false; const model = try app.alloc.dupe(u8, app.input_runtime.picker.model_picker_pending_model.items); defer app.alloc.free(model); const effort = selectedEffortCompletion(app) orelse { app.shell.render_requests.request(.footer); return true; }; const capabilities = model_capabilities.resolveForApp(App, app, model); if (capabilities.supports_fast_mode) { try setModelComposerText(app, "/model {s} {s} ", .{ model, effort.label() }); try app.input_runtime.picker.beginModelPickerFlow(app.alloc, model, model_capabilities.reasoningEffortIndex(capabilities, effort), true, .fast); } else { try session_commands.Commands(App).selectModelFromPicker(app, model, effort, app.fast_mode); app.input_runtime.inputResetState().clearCurrent(app.alloc); } app.shell.render_requests.request(.footer); return true; }, .fast => { if (!app.input_runtime.picker.hasPendingModelPickerSelection()) return false; const model = try app.alloc.dupe(u8, app.input_runtime.picker.model_picker_pending_model.items); defer app.alloc.free(model); const fast_mode = selectedFastCompletion(app) orelse { app.shell.render_requests.request(.footer); return true; }; const effort = model_capabilities.reasoningEffortAtIndex(model_capabilities.resolveForApp(App, app, model), app.input_runtime.picker.model_picker_effort_index); try session_commands.Commands(App).selectModelFromPicker(app, model, effort, fast_mode); app.input_runtime.inputResetState().clearCurrent(app.alloc); app.shell.render_requests.request(.footer); return true; }, } }
fn selectedModelCompletion(app: *App) ?[]const u8 { const query = app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) orelse return null; if (query.stage != .model) return null; if (exactModelCompletion(app, query.query)) |model| return model; var buf: [32][]const u8 = undefined; const count = modelPickerCompletions(app, query.query, &buf); if (count == 0) return null; const idx = modelPickerIndex(app, buf[0..count]); return buf[idx]; }
pub fn openCurrentModelPicker(app: *App) !void { try app.input_runtime.textReplacementState().replace(app.alloc, "/model "); app.input_runtime.picker.model_completion_anchor_current = true; app.shell.render_requests.request(.footer); }
fn beginModelPickerOptions(app: *App, model: []const u8) !void { const capabilities = model_capabilities.resolveForApp(App, app, model); const supports_effort = capabilities.reasoning_efforts.len > 0; const supports_fast = capabilities.supports_fast_mode;
if (!supports_effort and !supports_fast) { try session_commands.Commands(App).selectModelFromPicker(app, model, app.effort, app.fast_mode); app.input_runtime.inputResetState().clearCurrent(app.alloc); app.shell.render_requests.request(.footer); return; }
const stage: ModelPickerStage = if (supports_effort) .effort else .fast; if (supports_effort) { try setModelComposerText(app, "/model {s} ", .{model}); } else { try setModelComposerText(app, "/model {s} auto ", .{model}); } // Preselect the product effort default and enable Fast mode when supported. try app.input_runtime.picker.beginModelPickerFlow( app.alloc, model, model_capabilities.reasoningEffortIndex(capabilities, .auto), supports_fast, stage, ); app.shell.render_requests.request(.footer); }
pub fn beginExactModelSelection(app: *App, model: []const u8) !void { try beginModelPickerOptions(app, model); }
pub fn stepBackModelPicker(app: *App) !bool { const query = app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) orelse return false; if (query.stage == .model) return false; if (!app.input_runtime.picker.hasPendingModelPickerSelection()) return false;
const model = try app.alloc.dupe(u8, app.input_runtime.picker.model_picker_pending_model.items); defer app.alloc.free(model); switch (query.stage) { .model => return false, .effort => { try restoreModelPickerModelStage(app, model); return true; }, .fast => { const capabilities = model_capabilities.resolveForApp(App, app, model); if (capabilities.reasoning_efforts.len > 0) { const effort_index = model_capabilities.reasoningEffortIndex(capabilities, model_capabilities.reasoningEffortAtIndex(capabilities, app.input_runtime.picker.model_picker_effort_index)); try setModelComposerText(app, "/model {s} ", .{model}); try app.input_runtime.picker.beginModelPickerFlow(app.alloc, model, effort_index, app.fast_mode, .effort); } else { try restoreModelPickerModelStage(app, model); } return true; }, } }
fn restoreModelPickerModelStage(app: *App, model: []const u8) !void { try setModelComposerText(app, "/model {s}", .{model}); syncModelCompletionSelection(app, model, model); }
fn syncModelCompletionSelection(app: *App, query: []const u8, model: []const u8) void { var buf: [32][]const u8 = undefined; const count = app.modelCompletions(query, &buf); var index: usize = 0; for (buf[0..count], 0..) |candidate, i| { if (std.mem.eql(u8, candidate, model)) { index = i; break; } } app.input_runtime.picker.model_completion_index = index; app.input_runtime.picker.model_completion_window_start = list_window.updateEdgeStart( 0, count, index, list_window.default_max_picker_rows, ); }
pub fn abandonModelPickerOptions(app: *App) void { if (app.input_runtime.picker.model_picker_stage == .model) return; app.input_runtime.picker.clearModelPickerFlow(); }
pub fn shouldPreserveModelPickerInsert(app: *App) bool { const query = app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) orelse return false; return query.stage != .model and app.input_runtime.edit_state.cursor == app.input_runtime.edit_state.input.items.len; }
pub fn shouldPreserveModelPickerBackspace(app: *App) bool { const query = app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) orelse return false; return query.stage != .model and query.query.len > 0 and app.input_runtime.edit_state.cursor == app.input_runtime.edit_state.input.items.len; }
fn effortCompletionValues( app: *App, query: []const u8, out: *[types.ReasoningEffort.max_options + 1]types.ReasoningEffort, ) usize { if (!app.input_runtime.picker.hasPendingModelPickerSelection()) return 0; const capabilities = model_capabilities.resolveForApp(App, app, app.input_runtime.picker.model_picker_pending_model.items); const count = model_capabilities.reasoningEffortOptionCount(capabilities); if (count == 0) return 0;
const normalized_query = std.mem.trim(u8, query, " \t"); var result_count: usize = 0; for (0..count) |i| { const effort = model_capabilities.reasoningEffortAtIndex(capabilities, i); if (normalized_query.len > 0 and !text_utils.containsIgnoreCase(effort.displayLabel(), normalized_query)) continue; out[result_count] = effort; result_count += 1; } return result_count; }
fn exactEffortCompletion(app: *App, raw_query: []const u8) ?types.ReasoningEffort { if (!app.input_runtime.picker.hasPendingModelPickerSelection()) return null; const query = std.mem.trim(u8, raw_query, " \t"); const effort = types.ReasoningEffort.parseDisplayLabel(query) orelse return null; if (!model_capabilities.reasoningEffortSupported(model_capabilities.resolveForApp(App, app, app.input_runtime.picker.model_picker_pending_model.items), effort)) return null; return effort; }
fn selectedEffortCompletion(app: *App) ?types.ReasoningEffort { const query = app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) orelse return null; if (query.stage != .effort) return null;
const normalized_query = std.mem.trim(u8, query.query, " \t"); if (exactEffortCompletion(app, normalized_query)) |effort| return effort;
var values: [types.ReasoningEffort.max_options + 1]types.ReasoningEffort = undefined; const count = effortCompletionValues(app, normalized_query, &values); if (count == 0) return null; return values[app.input_runtime.picker.model_picker_effort_index % count]; }
fn fastCompletionLabels(query: []const u8, out: *[2][]const u8) usize { return picker_state.filterCompletionLabels(query, picker_state.model_picker_fast_options[0..], out[0..]); }
fn exactFastCompletion(raw_query: []const u8) ?bool { const query = std.mem.trim(u8, raw_query, " \t"); if (std.ascii.eqlIgnoreCase(query, picker_state.model_picker_fast_options[0])) return false; if (std.ascii.eqlIgnoreCase(query, picker_state.model_picker_fast_options[1])) return true; return null; }
fn selectedFastCompletion(app: *App) ?bool { const query = app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) orelse return null; if (query.stage != .fast) return null;
const normalized_query = std.mem.trim(u8, query.query, " \t"); if (exactFastCompletion(normalized_query)) |fast_mode| return fast_mode;
var labels: [2][]const u8 = undefined; const count = fastCompletionLabels(normalized_query, &labels); if (count == 0) return null; const label = labels[app.input_runtime.picker.model_picker_fast_index % count]; return std.mem.eql(u8, label, picker_state.model_picker_fast_options[1]); } };}
const FilePickerTestApp = struct { alloc: std.mem.Allocator, input_runtime: core_input_runtime.Runtime = .{}, pending_images: std.ArrayList(types.ImageAttachment) = .empty, stream: types.StreamState = .{}, shell: struct { render_requests: render_request.RenderRequestState = .{} } = .{}, file_completion_values: []const file_index.Candidate = &.{}, file_completion_calls: usize = 0, file_index_refreshes: usize = 0,
pub fn prepareDirectoryCompletion(self: *FilePickerTestApp) void { var results: [file_picker_completion_cap]file_index.SearchResult = undefined; for (self.file_completion_values, 0..) |value, i| results[i] = .{ .path = value.path, .kind = value.kind, .matched_spans = &.{} }; const state = &self.input_runtime.picker.file_completion; const rows = file_completion_state.Rows.copy(self.alloc, results[0..self.file_completion_values.len]) catch { state.stage(self.alloc, .{ .state = .ready }, .unavailable, null); return; }; self.file_completion_calls += 1; state.stage(self.alloc, .{ .state = .ready }, if (rows.results.len == 0) .empty else .ready, rows); }
pub fn refreshFileIndex(self: *FilePickerTestApp) void { self.file_index_refreshes += 1; }
pub fn fileCompletionsDependOnIndex(_: *const FilePickerTestApp, query: []const u8) bool { return @import("../workspace/path_completion.zig").queryMode(query) == .workspace_index; }
pub fn slashRegistry(_: *const FilePickerTestApp) command_specs.SlashRegistry { return .{}; }
fn deinit(self: *FilePickerTestApp) void { self.input_runtime.deinit(self.alloc); self.pending_images.deinit(self.alloc); }
pub fn fileCompletions( self: *FilePickerTestApp, _: []const u8, out: *[file_picker_completion_cap]file_index.SearchResult, _: *[file_picker_match_span_cap]file_index.MatchSpan, _: *[file_picker_path_storage_cap]u8, ) file_index.SearchError!usize { self.file_completion_calls += 1; const count = @min(self.file_completion_values.len, out.len); for (self.file_completion_values[0..count], 0..) |value, index| { out[index] = .{ .path = value.path, .kind = value.kind, .matched_spans = &.{}, }; } return count; }
pub fn isCurrentFileCompletion(_: *const FilePickerTestApp, _: []const u8, _: []const u8, _: file_index.CandidateKind) bool { return true; }};
const FilesystemFilePickerTestApp = struct { alloc: std.mem.Allocator, shell: struct { render_requests: render_request.RenderRequestState = .{} } = .{}, workspace_root: []const u8, workspace: app_workspace_runtime.State = .{}, file_index: file_index.FileIndex = .{}, input_runtime: core_input_runtime.Runtime = .{}, pending_images: std.ArrayList(types.ImageAttachment) = .empty,
pub fn reconcileDirectoryCompletion(self: *FilesystemFilePickerTestApp, eligible: bool) void { app_workspace_runtime.Runtime(FilesystemFilePickerTestApp).reconcileDirectoryCompletion(self, eligible); }
pub fn prepareDirectoryCompletion(self: *FilesystemFilePickerTestApp) void { app_workspace_runtime.Runtime(FilesystemFilePickerTestApp).prepareDirectoryCompletion(self); }
pub fn harvestDirectoryCompletion(self: *FilesystemFilePickerTestApp, eligible: bool) void { app_workspace_runtime.Runtime(FilesystemFilePickerTestApp).harvestDirectoryCompletion(self, eligible); }
fn deinit(self: *FilesystemFilePickerTestApp) void { app_workspace_runtime.Runtime(FilesystemFilePickerTestApp).deinit(self); self.file_index.deinit(self.alloc); self.input_runtime.deinit(self.alloc); self.pending_images.deinit(self.alloc); }
pub fn fileCompletions( self: *FilesystemFilePickerTestApp, query: []const u8, out: []file_index.SearchResult, match_spans: []file_index.MatchSpan, path_storage: []u8, ) app_workspace_runtime.FileCompletionError!usize { return app_workspace_runtime.Runtime(FilesystemFilePickerTestApp).fileCompletions( self, query, out, match_spans, path_storage, ); }
pub fn fileCompletionsDependOnIndex(self: *const FilesystemFilePickerTestApp, query: []const u8) bool { return app_workspace_runtime.Runtime(FilesystemFilePickerTestApp).fileCompletionsDependOnIndex(self, query); }
pub fn isCurrentFileCompletion( self: *const FilesystemFilePickerTestApp, query: []const u8, path: []const u8, kind: file_index.CandidateKind, ) bool { return app_workspace_runtime.Runtime(FilesystemFilePickerTestApp).isCurrentFileCompletion( self, query, path, kind, ); }};
const inline_completion_test_slash_specs = [_]command_specs.SlashSpec{ .{ .kind = .help, .command = "/help", .help_entry = "/help", }, .{ .kind = .resume_session, .command = "/resume", .help_entry = "/resume", },};const inline_completion_test_slash_registry = command_specs.SlashRegistry{ .commands = inline_completion_test_slash_specs[0..],};
const InlineCompletionTestApp = struct { alloc: std.mem.Allocator, input_runtime: core_input_runtime.Runtime = .{}, pending_images: std.ArrayList(types.ImageAttachment) = .empty, skills: skill_runtime.Runtime = .{}, model_cache: struct { menu: struct { active: bool = false, } = .{}, } = .{}, session_persistence: struct { session_picker: struct { active: bool = false, } = .{}, } = .{}, shell: struct { layout: struct { cols: u16 = 80, } = .{}, render_requests: render_request.RenderRequestState = .{}, } = .{},
pub fn slashRegistry(_: *const InlineCompletionTestApp) command_specs.SlashRegistry { return inline_completion_test_slash_registry; }
fn deinit(self: *InlineCompletionTestApp) void { self.input_runtime.deinit(self.alloc); self.pending_images.deinit(self.alloc); }};
const ModelPickerCompletionTestApp = struct { alloc: std.mem.Allocator, input_runtime: core_input_runtime.Runtime = .{},
fn deinit(self: *ModelPickerCompletionTestApp) void { self.input_runtime.deinit(self.alloc); }
pub fn resolvedModelCapabilities(_: *const ModelPickerCompletionTestApp, _: []const u8) model_capabilities.Capabilities { return .{ .reasoning_efforts = .fromSlice(&.{ types.ReasoningEffort.literal("future-tier"), types.ReasoningEffort.literal("high"), }), }; }};
noinline fn clobberCompletionStack() void { var bytes: [8192]u8 = undefined; @memset(&bytes, 0xa5); std.mem.doNotOptimizeAway(&bytes);}
test "model picker effort completion labels survive capability resolution" { const alloc = std.testing.allocator; const rt = CompletionRuntime(ModelPickerCompletionTestApp); var app = ModelPickerCompletionTestApp{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.picker.beginModelPickerFlow(alloc, "test/model", 0, false, .effort);
var values: [types.ReasoningEffort.max_options + 1]types.ReasoningEffort = undefined; const count = rt.effortCompletionValues(&app, "", &values); clobberCompletionStack();
try std.testing.expectEqual(@as(usize, 3), count); try std.testing.expectEqualStrings("default", values[0].displayLabel()); try std.testing.expectEqualStrings("future-tier", values[1].displayLabel()); try std.testing.expectEqualStrings("high", values[2].displayLabel());}
test "inline slash completion uses the shared visible suffix and acceptance path" { const alloc = std.testing.allocator; const rt = CompletionRuntime(InlineCompletionTestApp); var app = InlineCompletionTestApp{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "explain /he");
const completion = rt.visibleInlineCompletion(&app).?; try std.testing.expectEqualStrings("lp", completion.suffix()); try std.testing.expectEqualStrings( "lp", rt.visibleInlineSlashCompletion(&app).?.suffix, ); try std.testing.expectEqual( edit_contract.InsertResult.limit_exceeded, try rt.autocompleteInlineCompletion(&app, app.input_runtime.edit_state.input.items.len), ); try std.testing.expectEqualStrings("explain /he", app.input_runtime.edit_state.input.items); try std.testing.expectEqual( edit_contract.InsertResult.inserted, try rt.autocompleteInlineCompletion(&app, 4096), ); try std.testing.expectEqualStrings("explain /help", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(app.input_runtime.edit_state.input.items.len, app.input_runtime.edit_state.cursor);}
test "inline slash completion stays inactive when its suffix cannot render" { const alloc = std.testing.allocator; const rt = CompletionRuntime(InlineCompletionTestApp); var app = InlineCompletionTestApp{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "x /he");
app.shell.layout.cols = 7; try std.testing.expectEqual( @as(?InlineSlashCompletion, null), rt.visibleInlineSlashCompletion(&app), ); try std.testing.expectEqual( edit_contract.InsertResult.inactive, try rt.autocompleteInlineCompletion(&app, 4096), ); try std.testing.expectEqualStrings("x /he", app.input_runtime.edit_state.input.items);
app.shell.layout.cols = 8; try std.testing.expectEqualStrings( "lp", rt.visibleInlineSlashCompletion(&app).?.suffix, );}
test "root slash query is dismissible only while candidates are visible" { const alloc = std.testing.allocator; const rt = CompletionRuntime(InlineCompletionTestApp); var app = InlineCompletionTestApp{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "/hezzzzz");
try std.testing.expectEqual(@as(usize, 0), rt.visibleSlashCompletionCount(&app)); try std.testing.expect(!rt.dismissVisibleInlinePicker(&app)); try std.testing.expect(!app.input_runtime.picker.isInlinePickerDismissed(.slash));
try app.input_runtime.textReplacementState().replace(alloc, "/he"); try std.testing.expect(rt.visibleSlashCompletionCount(&app) > 0); try std.testing.expect(rt.dismissVisibleInlinePicker(&app)); try std.testing.expect(app.input_runtime.picker.isInlinePickerDismissed(.slash));}
test "root slash completion follows multiline and command argument ownership" { const alloc = std.testing.allocator; const rt = CompletionRuntime(InlineCompletionTestApp); const skills = [_]skill_runtime.Skill{.{ .name = "resume-helper", .description = "resume a workflow", .path = "/tmp/resume-helper/SKILL.md", .source = .global_fx, }}; var app = InlineCompletionTestApp{ .alloc = alloc, .skills = .{ .items = @constCast(&skills) }, }; defer app.deinit();
try app.input_runtime.textReplacementState().replace(alloc, "\n /"); try std.testing.expect(rt.visibleSlashCompletionCount(&app) > 0);
try app.input_runtime.textReplacementState().replace(alloc, "/resume "); try std.testing.expectEqual(@as(usize, 0), rt.visibleSlashCompletionCount(&app)); try std.testing.expect(!rt.dismissVisibleInlinePicker(&app));}
test "inline skill completion stays inactive when its suffix cannot render" { const alloc = std.testing.allocator; const rt = CompletionRuntime(InlineCompletionTestApp); const skills = [_]skill_runtime.Skill{.{ .name = "managed-menu", .description = "", .path = "/tmp/managed-menu/SKILL.md", .source = .global_fx, }}; var app = InlineCompletionTestApp{ .alloc = alloc, .skills = .{ .items = @constCast(&skills) }, }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "x $man");
app.shell.layout.cols = 8; try std.testing.expectEqual( @as(?InlineSkillCompletion, null), rt.visibleInlineSkillCompletion(&app), ); try std.testing.expectEqual( edit_contract.InsertResult.inactive, try rt.autocompleteInlineSkillCompletion(&app, 4096), ); try std.testing.expectEqualStrings("x $man", app.input_runtime.edit_state.input.items);
app.shell.layout.cols = 9; try std.testing.expectEqualStrings( "aged-menu", rt.visibleInlineSkillCompletion(&app).?.suffix, );}
test "model picker ownership suppresses inline skill completion" { const alloc = std.testing.allocator; const rt = CompletionRuntime(InlineCompletionTestApp); const skills = [_]skill_runtime.Skill{.{ .name = "managed-menu", .description = "", .path = "/tmp/managed-menu/SKILL.md", .source = .global_fx, }}; var app = InlineCompletionTestApp{ .alloc = alloc, .skills = .{ .items = @constCast(&skills) }, }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "/model anything $man");
try std.testing.expect(app.input_runtime.picker.activeModelPickerQuery(&app.input_runtime.edit_state) != null); try std.testing.expectEqual( @as(?InlineSkillCompletion, null), rt.visibleInlineSkillCompletion(&app), ); try std.testing.expectEqual( edit_contract.InsertResult.inactive, try rt.autocompleteInlineSkillCompletion(&app, 4096), ); try std.testing.expectEqualStrings("/model anything $man", app.input_runtime.edit_state.input.items);}
test "dedicated catalog ownership suppresses inline skill completion" { const alloc = std.testing.allocator; const rt = CompletionRuntime(InlineCompletionTestApp); const skills = [_]skill_runtime.Skill{.{ .name = "managed-menu", .description = "", .path = "/tmp/managed-menu/SKILL.md", .source = .global_fx, }}; var app = InlineCompletionTestApp{ .alloc = alloc, .skills = .{ .items = @constCast(&skills) }, }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "x $man");
try std.testing.expect(rt.visibleInlineSkillCompletion(&app) != null);
app.input_runtime.help_menu.active = true; try expectInlineSkillCompletionInactive(&app); app.input_runtime.help_menu.active = false;
app.input_runtime.settings_menu.active = true; try expectInlineSkillCompletionInactive(&app); app.input_runtime.settings_menu.active = false;
app.model_cache.menu.active = true; try expectInlineSkillCompletionInactive(&app); app.model_cache.menu.active = false;
app.session_persistence.session_picker.active = true; try expectInlineSkillCompletionInactive(&app); app.session_persistence.session_picker.active = false;
app.skills.menu.active = true; try expectInlineSkillCompletionInactive(&app);
try std.testing.expect(app.skills.menu.active); try std.testing.expectEqualStrings("x $man", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(@as(usize, 0), app.input_runtime.entities.skill_tokens.items.len);}
fn expectInlineSkillCompletionInactive(app: *InlineCompletionTestApp) !void { const rt = CompletionRuntime(InlineCompletionTestApp); try std.testing.expectEqual( @as(?InlineSkillCompletion, null), rt.visibleInlineSkillCompletion(app), ); try std.testing.expectEqual( edit_contract.InsertResult.inactive, try rt.autocompleteInlineSkillCompletion(app, 4096), );}
test "file picker disabled profile never claims literal input or prepares rows" { const DisabledApp = struct { pub const host_profile = runtime_profile.wasm; input_runtime: core_input_runtime.Runtime = .{}, }; const rt = CompletionRuntime(DisabledApp); var app: DisabledApp = .{}; defer app.input_runtime.deinit(std.testing.allocator); try app.input_runtime.textReplacementState().replace(std.testing.allocator, "@./file"); rt.prepareFilePicker(&app); rt.reconcileFilePicker(&app); rt.collectFilePickerFacts(&app); const workspace_rt = app_workspace_runtime.Runtime(DisabledApp); workspace_rt.prepareDirectoryCompletion(&app); workspace_rt.reconcileDirectoryCompletion(&app, true); workspace_rt.harvestDirectoryCompletion(&app, true); workspace_rt.requestStop(&app); try std.testing.expect(!rt.hasFileQuery(&app)); try std.testing.expect(!rt.filePickerOwnsSurface(&app)); try std.testing.expectEqual(@as(?edit_contract.InsertResult, null), try rt.submitFilePickerOnEnter(&app, 4096)); try std.testing.expectEqual(edit_contract.InsertResult.inactive, try rt.autocompleteFilePickerSelection(&app, 4096)); try std.testing.expect(!app.input_runtime.picker.file_completion.active); try std.testing.expectEqualStrings("@./file", app.input_runtime.edit_state.input.items);}
test "file picker defers bare at refresh until the settled query mode" { const alloc = std.testing.allocator; const rt = CompletionRuntime(FilePickerTestApp); var app = FilePickerTestApp{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.edit_state.insertSlice(alloc, "@"); rt.reconcileFilePicker(&app); try std.testing.expectEqual(@as(usize, 0), app.file_index_refreshes); try app.input_runtime.edit_state.insertSlice(alloc, "~"); rt.reconcileFilePicker(&app); rt.prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 0), app.file_index_refreshes); try std.testing.expect(!app.input_runtime.picker.file_completion.indexed); // A mode change inside the same occurrence must still refresh the index. try app.input_runtime.edit_state.insertSlice(alloc, "notes"); rt.reconcileFilePicker(&app); rt.prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 1), app.file_index_refreshes); rt.prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 1), app.file_index_refreshes);}
test "file picker resize invalidation blocks acceptance without another lookup" { const alloc = std.testing.allocator; const rt = CompletionRuntime(FilePickerTestApp); var app = FilePickerTestApp{ .alloc = alloc, .file_completion_values = &.{.{ .path = "file.txt", .kind = .file }} }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "@file"); presentFilePickerForTest(&app); app.shell.render_requests.observeResizeSignal(0, 100); try std.testing.expectEqual(edit_contract.InsertResult.inactive, (try rt.submitFilePickerOnEnter(&app, 4096)).?); try std.testing.expectEqualStrings("@file", app.input_runtime.edit_state.input.items); rt.prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 1), app.file_completion_calls); app.shell.render_requests.completeResizeGeometry(false); rt.acknowledgeFilePicker(&app, rt.filePickerView(&app).receipt.?); try std.testing.expectEqual(edit_contract.InsertResult.inserted, (try rt.submitFilePickerOnEnter(&app, 4096)).?); try std.testing.expectEqual(@as(usize, 1), app.file_completion_calls);}
test "file picker preparation caches data but requires an explicit visible receipt" { const alloc = std.testing.allocator; const rt = CompletionRuntime(FilePickerTestApp); var app = FilePickerTestApp{ .alloc = alloc, .file_completion_values = &.{.{ .path = "file.txt", .kind = .file }} }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "@file"); rt.prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 1), app.file_completion_calls); try std.testing.expectEqual(edit_contract.InsertResult.inactive, (try rt.submitFilePickerOnEnter(&app, 4096)).?); try std.testing.expectEqualStrings("@file", app.input_runtime.edit_state.input.items); rt.prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 1), app.file_completion_calls); rt.acknowledgeFilePicker(&app, rt.filePickerView(&app).receipt.?); rt.navigateFilePicker(&app, 1); _ = rt.filePickerView(&app); rt.prepareFilePicker(&app); try std.testing.expectEqual(@as(usize, 1), app.file_completion_calls); try std.testing.expectEqual(edit_contract.InsertResult.inserted, (try rt.submitFilePickerOnEnter(&app, 4096)).?); try std.testing.expectEqual(@as(usize, 1), app.file_completion_calls); try std.testing.expectEqualStrings("@file.txt ", app.input_runtime.edit_state.input.items);}
fn presentFilePickerForTest(app: anytype) void { const rt = CompletionRuntime(@TypeOf(app.*)); rt.prepareFilePicker(app); if (comptime @TypeOf(app.*) == FilesystemFilePickerTestApp) { while (app.input_runtime.picker.file_completion.directory_request != null) { rt.collectFilePickerFacts(app); std.Io.sleep(std.testing.io, .fromMilliseconds(1), .awake) catch unreachable; } } if (rt.filePickerView(app).receipt) |receipt| rt.acknowledgeFilePicker(app, receipt);}
test "streaming file selection commits the selected path" { const alloc = std.testing.allocator; const rt = CompletionRuntime(FilePickerTestApp); var app = FilePickerTestApp{ .alloc = alloc, .file_completion_values = &.{.{ .path = "src/main.zig", .kind = .file }}, }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "review @src/mai"); app.stream.active = true; presentFilePickerForTest(&app);
try std.testing.expectEqual( edit_contract.InsertResult.inserted, (try rt.submitFilePickerOnEnter(&app, 4096)).?, ); try std.testing.expectEqualStrings("review @src/main.zig ", app.input_runtime.edit_state.input.items);}
test "file picker encodes literal quotes without reopening quote grammar" { const alloc = std.testing.allocator; const rt = CompletionRuntime(FilePickerTestApp); var app = FilePickerTestApp{ .alloc = alloc, .file_completion_values = &.{.{ .path = "\"weird", .kind = .file }}, }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "@we"); presentFilePickerForTest(&app);
try std.testing.expectEqual( edit_contract.InsertResult.inserted, try rt.autocompleteFilePickerSelection(&app, 4096), ); try std.testing.expectEqualStrings("@\"\\\"weird\" ", app.input_runtime.edit_state.input.items); try std.testing.expect(app.input_runtime.picker.activeFilePickerQuery(&app.input_runtime.edit_state) == null);}
test "file picker quoted middle replacement preserves only the outside tail" { const alloc = std.testing.allocator; const rt = CompletionRuntime(FilePickerTestApp); var app = FilePickerTestApp{ .alloc = alloc, .file_completion_values = &.{.{ .path = "a\\b.txt", .kind = .file }} }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "read @\"a\\\\old.txt\" suffix"); app.input_runtime.edit_state.cursor = "read @\"a\\\\".len; presentFilePickerForTest(&app); try std.testing.expectEqual(edit_contract.InsertResult.inserted, try rt.autocompleteFilePickerSelection(&app, 4096)); try std.testing.expectEqualStrings("read @\"a\\\\b.txt\" suffix", app.input_runtime.edit_state.input.items); try std.testing.expectEqual("read @\"a\\\\b.txt\" ".len, app.input_runtime.edit_state.cursor);}
test "filesystem file picker pipeline drills into a directory and selects a file" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(std.testing.io, "workspace/space dir"); try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "workspace/space dir/item.txt", .data = "inside", });
const io_mod = @import("../shared/io.zig"); const workspace_root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(workspace_root);
const rt = CompletionRuntime(FilesystemFilePickerTestApp); var app = FilesystemFilePickerTestApp{ .alloc = alloc, .workspace_root = workspace_root, }; defer app.deinit();
try app.input_runtime.textReplacementState().replace(alloc, "@./space"); presentFilePickerForTest(&app); try std.testing.expectEqual( edit_contract.InsertResult.inserted, try rt.autocompleteFilePickerSelection(&app, 4096), ); try std.testing.expectEqualStrings( "@\"./space dir/", app.input_runtime.edit_state.input.items, ); try std.testing.expectEqualStrings( "./space dir/", app.input_runtime.picker.activeFilePickerQuery(&app.input_runtime.edit_state).?.query, );
try app.input_runtime.edit_state.insertSlice(alloc, "item"); presentFilePickerForTest(&app); try std.testing.expectEqual( edit_contract.InsertResult.inserted, try rt.autocompleteFilePickerSelection(&app, 4096), ); try std.testing.expectEqualStrings( "@\"./space dir/item.txt\" ", app.input_runtime.edit_state.input.items, ); try std.testing.expect(app.input_runtime.picker.activeFilePickerQuery(&app.input_runtime.edit_state) == null);}
test "file picker loading Tab preserves pending request and Enter never queues acceptance" { const alloc = std.testing.allocator; const rt = CompletionRuntime(FilePickerTestApp); var app: FilePickerTestApp = .{ .alloc = alloc }; defer app.deinit(); try app.input_runtime.textReplacementState().replace(alloc, "@./pending/"); rt.reconcileFilePicker(&app); const state = &app.input_runtime.picker.file_completion; state.stage(alloc, .{ .state = .ready }, .loading, null); state.directory_request = 71; state.rejectSelection(); // A retry of stale rows must also stay loading. for (0..4) |_| { try std.testing.expectEqual(edit_contract.InsertResult.inactive, try rt.autocompleteFilePickerSelection(&app, 4096)); try std.testing.expectEqual(edit_contract.InsertResult.inactive, (try rt.submitFilePickerOnEnter(&app, 4096)).?); rt.prepareFilePicker(&app); try std.testing.expectEqual(@as(?u64, 71), state.directory_request); try std.testing.expect(!state.lookup_dirty); } try std.testing.expectEqual(@as(usize, 0), app.file_completion_calls); try std.testing.expectEqualStrings("@./pending/", app.input_runtime.edit_state.input.items); state.directory_request = null; state.stage(alloc, .{ .state = .ready }, .ready, try file_completion_state.Rows.copy(alloc, &.{.{ .path = "./pending/a", .kind = .file, .matched_spans = &.{} }})); try std.testing.expectEqual(edit_contract.InsertResult.inactive, (try rt.submitFilePickerOnEnter(&app, 4096)).?); try std.testing.expectEqualStrings("@./pending/", app.input_runtime.edit_state.input.items); rt.acknowledgeFilePicker(&app, rt.filePickerView(&app).receipt.?); try std.testing.expectEqualStrings("@./pending/", app.input_runtime.edit_state.input.items); try std.testing.expectEqual(edit_contract.InsertResult.inactive, (try rt.submitFilePickerOnEnter(&app, 4096)).?); rt.navigateFilePicker(&app, 1); try std.testing.expectEqual(edit_contract.InsertResult.inserted, (try rt.submitFilePickerOnEnter(&app, 4096)).?);}