Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211const std = @import("std");const build_options = @import("build_options");const app_permission_runtime = @import("../app/app_permission_runtime.zig");const app_session_runtime = @import("../app/app_session_runtime.zig");const auth_runtime = @import("../auth/auth_runtime.zig");const collections = @import("../shared/collections.zig");const config_runtime = @import("../config/config_runtime.zig");const debug_trace = @import("../shared/debug_trace.zig");const host = @import("../hosts/host.zig");const io_mod = @import("../shared/io.zig");const model_capabilities = @import("../config/model_capabilities.zig");const model_provider = @import("../config/model_provider.zig");const output_contracts = @import("../output/output_contracts.zig");const permissions = @import("../permissions/permissions.zig");const worker_runtime = @import("../agent/worker_runtime.zig");const prompt_history_runtime = @import("../app/prompt_history_runtime.zig");const provider_runtime = @import("../app/provider_runtime.zig");const tool_dispatch = @import("../tooling/tool_dispatch.zig");const text_utils = @import("../shared/text_utils.zig");const types = @import("../shared/types.zig");const render_request = @import("../../ui/render_request.zig");
const freeStringList = collections.freeStringList;const containsIgnoreCase = text_utils.containsIgnoreCase;const permissions_usage = "usage: /permissions [ask|auto|full-access|reset]\n /permissions remember <allow|deny> <tool-name> <arguments-json>\n /permissions revoke <rule-id>";
pub fn reportUserSettingsCommit( app: anytype, label: []const u8, patch: config_runtime.UserSettingsPatch, outcome: config_runtime.CommitOutcome, session_error: ?anyerror, // False when the caller announces state; drops the redundant "saved" line. announce_commit: bool,) !?config_runtime.ConfigSources { var detailed = loadDetailedSettingsForNotice(app) catch |err| { debug_trace.logf( "config", "{s} post-commit source resolution failed err={s}", .{ label, @errorName(err) }, ); try writeUserSettingsCommitUnknownSource( app, label, outcome, session_error, err, ); return null; }; defer detailed.deinit(app.alloc); if (postCommitResolutionError(detailed)) |err| { debug_trace.logf( "config", "{s} post-commit source resolution diagnostic err={s}", .{ label, @errorName(err) }, ); try writeUserSettingsCommitUnknownSource( app, label, outcome, session_error, err, ); return null; }
var out: std.Io.Writer.Allocating = .init(app.alloc); defer out.deinit();
const benign_commit = "saved to user settings (scope=user)"; try out.writer.writeAll(benign_commit); try appendShadowedUserSources(&out.writer, patch, detailed.sources); try appendCommitCleanup(&out.writer, outcome); if (session_error) |err| { try out.writer.print("; current-session recovery degraded ({s})", .{@errorName(err)}); } const text = try out.toOwnedSlice(); defer app.alloc.free(text); // Keep the line when any warning suffix was appended. if (announce_commit or !std.mem.eql(u8, text, benign_commit)) { try app.writeDomainNotice(.{ .topic = label, .tone = .neutral, .body = text }, true); } return detailed.sources;}
fn writeUserSettingsCommitUnknownSource( app: anytype, label: []const u8, outcome: config_runtime.CommitOutcome, session_error: ?anyerror, resolution_error: anyerror,) !void { var out: std.Io.Writer.Allocating = .init(app.alloc); defer out.deinit();
try out.writer.print( "saved to user settings (scope=user); next-startup source unknown ({s})", .{@errorName(resolution_error)}, ); try appendCommitCleanup(&out.writer, outcome); if (session_error) |err| { try out.writer.print("; current-session recovery degraded ({s})", .{@errorName(err)}); } const text = try out.toOwnedSlice(); defer app.alloc.free(text); try app.writeDomainNotice(.{ .topic = label, .tone = .warning, .body = text }, true);}
pub fn reportUserSettingsFailure( app: anytype, label: []const u8, err: anyerror, cleanup: config_runtime.LegacyCleanup, runtime_changed: bool,) !void { var out: std.Io.Writer.Allocating = .init(app.alloc); defer out.deinit();
if (err == error.SettingsCommitIndeterminate) { try out.writer.print( "user settings persistence uncertain (scope=user, error={s})", .{@errorName(err)}, ); } else { try out.writer.print( "{s}not saved to user settings ({s})", .{ if (runtime_changed) "active for this process but " else "", @errorName(err) }, ); } try appendLegacyCleanup(&out.writer, cleanup); const text = try out.toOwnedSlice(); defer app.alloc.free(text); try app.writeDomainNotice(.{ .topic = label, .tone = .@"error", .body = text }, true);}
fn loadDetailedSettingsForNotice(app: anytype) !config_runtime.DetailedSettings { if (comptime @hasDecl(@TypeOf(app.*), "loadDetailedSettingsForNotice")) { return app.loadDetailedSettingsForNotice(); } return config_runtime.loadMergedSettingsDetailed(app.alloc, app.workspace_root);}
fn postCommitResolutionError( detailed: config_runtime.DetailedSettings,) ?anyerror { for (detailed.diagnostics) |diagnostic| { if (diagnostic.layer != .user) continue; switch (diagnostic.cause) { .malformed_settings => return error.InvalidSettingsFormat, .settings_too_large => return error.SettingsPrimaryTooLarge, .durable_path_unsafe => return error.DurablePathUnsafe, .private_state_permissions_unsupported => return error.PrivateStatePermissionsUnsupported, .invalid_model_id => return error.InvalidModelValue, .retired_skill_match_fuzzy => return error.InvalidSettingsFormat, .invalid_context_limits => return error.InvalidSettingsFormat, .invalid_additional_directories => return error.InvalidSettingsFormat, .ignored_project_user_only_setting, .legacy_workspace_preferences, .manual_backup_available, => {}, } } return null;}
fn appendShadowedUserSources( writer: *std.Io.Writer, patch: config_runtime.UserSettingsPatch, sources: config_runtime.ConfigSources,) !void { var wrote_header = false; const model_source = if (patch.model_preference) |preference| sources.models.get(model_provider.NameKey.fromProvider(preference.provider)) else .compiled_default; try appendShadowedUserSource(writer, "model", patch.model_preference != null, model_source, &wrote_header); try appendShadowedUserSource(writer, "permission_mode", patch.permission_mode != null, sources.permission_mode, &wrote_header); try appendShadowedUserSource(writer, "effort", patch.effort != null, sources.effort, &wrote_header); try appendShadowedUserSource(writer, "fast_mode", patch.fast_mode != null, sources.fast_mode, &wrote_header); try appendShadowedUserSource(writer, "startup_scrollback", patch.startup_scrollback != null, sources.startup_scrollback, &wrote_header); try appendShadowedUserSource(writer, "prompt_history", patch.prompt_history_enabled != null, sources.prompt_history_enabled, &wrote_header); if (patch.statusline_item) |item| { const source: ?config_runtime.ConfigSource = switch (item.item) { .context => sources.statusline_context, .session => sources.statusline_session, .workspace => null, }; if (source) |resolved| { const field = switch (item.item) { .context => "statusLine.context", .session => "statusLine.session", .workspace => unreachable, }; try appendShadowedUserSource(writer, field, true, resolved, &wrote_header); } } try appendShadowedUserSource( writer, "notifications.turn_end", patch.notification_turn_end != null, sources.notification_turn_end, &wrote_header, ); try appendShadowedUserSource( writer, "notifications.attention_required", patch.notification_attention_required != null, sources.notification_attention_required, &wrote_header, ); try appendShadowedUserSource( writer, "notifications.max", patch.notification_max != null, sources.notification_max, &wrote_header, );}
fn appendShadowedUserSource( writer: *std.Io.Writer, field: []const u8, present: bool, source: config_runtime.ConfigSource, wrote_header: *bool,) !void { if (!present or source == .user_global) return; if (!wrote_header.*) { try writer.writeAll("; fresh sessions here use higher-precedence "); wrote_header.* = true; } else { try writer.writeAll(", "); } try writer.print("{s}={s}", .{ field, @tagName(source) });}
fn appendCommitCleanup(writer: *std.Io.Writer, outcome: config_runtime.CommitOutcome) !void { switch (outcome) { .unchanged => {}, .committed => |committed| try appendLegacyCleanup(writer, committed.cleanup), }}
fn appendLegacyCleanup(writer: *std.Io.Writer, cleanup: config_runtime.LegacyCleanup) !void { if (cleanup.fields_removed > 0) { try writer.print( "; normalized {d} legacy value{s} across {d} workspace{s}", .{ cleanup.fields_removed, if (cleanup.fields_removed == 1) "" else "s", cleanup.workspaces_changed, if (cleanup.workspaces_changed == 1) "" else "s", }, ); } for (cleanup.recovery_paths) |path| { try writer.print("; recovery={s}", .{path}); }}
pub fn Commands(comptime App: type) type { return struct { fn update_channel_label(app: *App) []const u8 { if (comptime @hasField(App, "upgrader")) { const Upgrader = @TypeOf(app.upgrader); if (comptime @hasDecl(Upgrader, "channel")) { return app.upgrader.channel().label(); } } return "stable"; }
pub fn showStatus(app: *App) !void { const provider = provider_runtime.provider(app); var preferred: ?types.CredentialSource = null; if (provider == .gateway and app.auth.credentialSource() == null) { var settings = config_runtime.loadMergedSettings(app.alloc, app.workspace_root) catch |err| { try writeSettingsLoadError(app, err); return; }; defer settings.deinit(app.alloc); preferred = settings.credential_source; } const auth = app.auth.statusSnapshot(provider, preferred); const text = try (output_contracts.StatusSnapshot{ .model = provider_runtime.model(app), .provider = provider, .provider_endpoint = if (comptime @hasField(App, "provider_selection")) if (app.provider_selection.definitions.get(provider.label())) |definition| definition.base_url else null else null, .update_channel = update_channel_label(app), .build_channel = if (@hasDecl(App, "build_update_channel")) App.build_update_channel.label() else "stable", .build_revision = if (@hasDecl(App, "build_revision")) App.build_revision else "", .auth = auth, .auth_help = auth.missingHelp(.interactive), .permission_mode = app.permission_engine.mode, .workspace_root = app.workspace_root, .history_turns = app.session.historyLen(), .session_permission_grants = app.permission_engine.grants.items.len, .agent_step_limit = app.agent_step_limit, }).renderInteractiveBody(app.alloc); defer app.alloc.free(text); try app.writeDomainNotice(.{ .topic = "status", .tone = .neutral, .body = text }, true); }
pub fn handleSettings(app: *App, rest: []const u8) !void { const trimmed = std.mem.trim(u8, rest, " \t"); if (trimmed.len == 0) { try writeSettingsStatus(app); return; }
const first = splitFirstWord(trimmed) orelse { try writeSettingsUsage(app); return; }; if (!std.ascii.eqlIgnoreCase(first.word, "startup-scrollback")) { try writeSettingsUsage(app); return; }
const value_split = splitFirstWord(first.rest); if (value_split) |split| { if (split.rest.len != 0) { try writeSettingsUsage(app); return; } if (std.ascii.eqlIgnoreCase(split.word, "on")) { try saveStartupScrollbackSetting(app, true); return; } if (std.ascii.eqlIgnoreCase(split.word, "off")) { try saveStartupScrollbackSetting(app, false); return; } try writeSettingsUsage(app); return; }
var settings = config_runtime.loadMergedSettings(app.alloc, app.workspace_root) catch |err| { try writeSettingsLoadError(app, err); return; }; defer settings.deinit(app.alloc);
try saveStartupScrollbackSetting(app, !(settings.startup_scrollback orelse true)); }
pub fn handleModel(app: *App, query: []const u8) !void { if (query.len == 0) { try app.writeDomainNotice(.{ .topic = "model", .tone = .neutral, .body = provider_runtime.model(app) }, true); return; }
const resolved = try resolveModelQuery(app, query); defer app.alloc.free(resolved); try setResolvedModel(app, resolved, true); }
pub fn handlePermissions(app: *App, rest: []const u8) !void { if (rest.len == 0) { try writePermissionsStatus(app); return; }
if (config_runtime.parsePermissionMode(rest)) |mode| { try app_permission_runtime.Runtime(App).selectMode(app, mode); try app.writeDomainNotice(.{ .topic = "permissions", .tone = if (mode == .yolo) .warning else .neutral, .body = switch (mode) { .ask => "mode set to ask", .auto => "mode set to auto", .yolo => "mode set to full access", }, }, true); return; }
if (std.ascii.eqlIgnoreCase(rest, "reset")) { try app_permission_runtime.Runtime(App).reset(app); try app.writeDomainNotice(.{ .topic = "permissions", .tone = .neutral, .body = "permissions reset to ask, session grants cleared" }, true); return; }
try app.writeDomainNotice(.{ .topic = "", .tone = .@"error", .body = permissions_usage }, true); }
pub fn handleAllowlist(app: *App, rest: []const u8) !void { const trimmed = std.mem.trim(u8, rest, " \t"); if (trimmed.len == 0) { try writeAllowlistStatus(app, .effective); return; }
const first = splitFirstWord(trimmed).?; if (std.ascii.eqlIgnoreCase(first.word, "view")) { const view = parseAllowlistView(first.rest) orelse { try writeAllowlistUsage(app); return; }; try writeAllowlistStatus(app, view); return; }
var permission_scope: config_runtime.PermissionScope = .local; var action = first; if (std.ascii.eqlIgnoreCase(first.word, "user") or std.ascii.eqlIgnoreCase(first.word, "local")) { permission_scope = if (std.ascii.eqlIgnoreCase(first.word, "user")) .user else .local; action = splitFirstWord(first.rest) orelse { try writeAllowlistUsage(app); return; }; }
if (std.ascii.eqlIgnoreCase(action.word, "add")) { try handleAllowlistAdd(app, permission_scope, action.rest); return; }
if (std.ascii.eqlIgnoreCase(action.word, "remove")) { try handleAllowlistRemove(app, permission_scope, action.rest); return; }
if (std.ascii.eqlIgnoreCase(action.word, "reset")) { try handleAllowlistReset(app, permission_scope, action.rest); return; }
try writeAllowlistUsage(app); }
pub fn handleHistory(app: *App, rest: []const u8) !void { const trimmed = std.mem.trim(u8, rest, " \t"); if (std.ascii.eqlIgnoreCase(trimmed, "off")) { app.prompt_history.disable(); var attempt = config_runtime.attemptUserPreferences( app.alloc, .{ .prompt_history_enabled = false }, ); defer attempt.deinit(app.alloc); switch (attempt) { .failure => |failure| { try reportUserSettingsFailure( app, "history", failure.err, failure.cleanup, true, ); return; }, .outcome => |outcome| _ = try reportUserSettingsCommit( app, "history", .{ .prompt_history_enabled = false }, outcome, null, true, ), } return; }
if (std.ascii.eqlIgnoreCase(trimmed, "on")) { var attempt = config_runtime.attemptUserPreferences( app.alloc, .{ .prompt_history_enabled = true }, ); defer attempt.deinit(app.alloc); switch (attempt) { .failure => |failure| { app.prompt_history.disable(); try reportUserSettingsFailure( app, "history", failure.err, failure.cleanup, false, ); return; }, .outcome => |outcome| { app.prompt_history.enable(); _ = try reportUserSettingsCommit( app, "history", .{ .prompt_history_enabled = true }, outcome, null, true, ); }, } return; }
try app.writeDomainNotice(.{ .topic = "history", .tone = .@"error", .body = "prompt history options: on, off" }, true); }
fn writeAllowlistUsage(app: *App) !void { try app.writeDomainNotice(.{ .topic = "", .tone = .@"error", .body = "usage: /allowlist [view [effective|local|user]|[local|user] add|remove|reset ...]", }, true); }
fn handleAllowlistAdd( app: *App, permission_scope: config_runtime.PermissionScope, raw: []const u8, ) !void { const target = (try parseAllowlistTargetAlloc(app.alloc, app.toolRegistry(), raw)) orelse { try app.writeDomainNotice(.{ .topic = "", .tone = .@"error", .body = "usage: /allowlist add [command|tool|url|web-fetch-domain] <pattern>", }, true); return; }; defer target.deinit(app.alloc);
var outcome = config_runtime.addPermissionRule( app.alloc, permission_scope, permissionWorkspaceRoot(app, permission_scope), target.category, target.pattern, .allow, ) catch |err| { const notice_rule_write_fail = try std.fmt.allocPrint( app.alloc, "failed to add rule to settings (scope={s}, error={s})", .{ @tagName(permission_scope), @errorName(err) }, ); defer app.alloc.free(notice_rule_write_fail); try app.writeDomainNotice(.{ .topic = "allowlist", .tone = .@"error", .body = notice_rule_write_fail }, true); debug_trace.logf( "config", "allowlist add failed scope={s} err={s}", .{ @tagName(permission_scope), @errorName(err) }, ); return; }; defer outcome.deinit(app.alloc);
try finishAllowlistMutation(app, permission_scope, "added", target); }
fn handleAllowlistRemove( app: *App, permission_scope: config_runtime.PermissionScope, raw: []const u8, ) !void { const target = (try parseAllowlistTargetAlloc(app.alloc, app.toolRegistry(), raw)) orelse { try app.writeDomainNotice(.{ .topic = "", .tone = .@"error", .body = "usage: /allowlist remove [command|tool|url|web-fetch-domain] <pattern>", }, true); return; }; defer target.deinit(app.alloc);
var outcome = config_runtime.removePermissionRule( app.alloc, permission_scope, permissionWorkspaceRoot(app, permission_scope), target.category, target.pattern, ) catch |err| { const notice = try std.fmt.allocPrint( app.alloc, "failed to remove rule from settings (scope={s}, error={s})", .{ @tagName(permission_scope), @errorName(err) }, ); defer app.alloc.free(notice); try app.writeDomainNotice(.{ .topic = "allowlist", .tone = .@"error", .body = notice }, true); debug_trace.logf( "config", "allowlist remove failed scope={s} err={s}", .{ @tagName(permission_scope), @errorName(err) }, ); return; }; defer outcome.deinit(app.alloc); if (outcome == .unchanged) { const msg = try formatAllowlistChange(app.alloc, "no matching rule for", target, permission_scope, ""); defer app.alloc.free(msg); try app.writeDomainNotice(.{ .topic = "allowlist", .tone = .neutral, .body = msg }, true); return; }
try finishAllowlistMutation(app, permission_scope, "removed", target); }
fn handleAllowlistReset( app: *App, permission_scope: config_runtime.PermissionScope, raw: []const u8, ) !void { const reset_scope = parseAllowlistResetScope(raw) orelse { try app.writeDomainNotice(.{ .topic = "", .tone = .@"error", .body = "usage: /allowlist reset [commands|tools|urls|web-fetch-domains|all]", }, true); return; };
var outcome = config_runtime.removeAllowlistRules( app.alloc, permission_scope, permissionWorkspaceRoot(app, permission_scope), reset_scope, ) catch |err| { const notice_reset_fail = try std.fmt.allocPrint( app.alloc, "failed to reset rules in settings (scope={s}, error={s})", .{ @tagName(permission_scope), @errorName(err) }, ); defer app.alloc.free(notice_reset_fail); try app.writeDomainNotice(.{ .topic = "allowlist", .tone = .@"error", .body = notice_reset_fail }, true); debug_trace.logf( "config", "allowlist reset failed scope={s} err={s}", .{ @tagName(permission_scope), @errorName(err) }, ); return; }; defer outcome.deinit(app.alloc); const removed: usize = switch (outcome) { .unchanged => 0, .committed => |committed| committed.permission_rules_removed, };
var detailed = loadDetailedSettingsForNotice(app) catch |err| { const msg = try std.fmt.allocPrint( app.alloc, "reset {s}: removed {d} rule{s} (scope={s}); saved but effective source unknown and runtime reload failed ({s})", .{ allowlistResetScopeLabel(reset_scope), removed, if (removed == 1) "" else "s", @tagName(permission_scope), @errorName(err), }, ); defer app.alloc.free(msg); try app.writeDomainNotice(.{ .topic = "allowlist", .tone = .warning, .body = msg }, true); return; }; defer detailed.deinit(app.alloc); if (postCommitResolutionError(detailed)) |err| { const msg = try std.fmt.allocPrint( app.alloc, "reset {s}: removed {d} rule{s} (scope={s}); saved but effective source unknown and runtime reload failed ({s})", .{ allowlistResetScopeLabel(reset_scope), removed, if (removed == 1) "" else "s", @tagName(permission_scope), @errorName(err), }, ); defer app.alloc.free(msg); try app.writeDomainNotice(.{ .topic = "allowlist", .tone = .warning, .body = msg }, true); return; } replaceEffectivePermissionRules(app, &detailed);
const shadow = permissionShadowLabel(detailed.permission_sources, permission_scope); const msg = try std.fmt.allocPrint( app.alloc, "reset {s}: removed {d} rule{s} (scope={s}){s}{s}", .{ allowlistResetScopeLabel(reset_scope), removed, if (removed == 1) "" else "s", @tagName(permission_scope), if (shadow.len > 0) "; user rules shadowed by " else "", shadow, }, ); defer app.alloc.free(msg); try app.writeDomainNotice(.{ .topic = "allowlist", .tone = .neutral, .body = msg }, true); }
pub fn toggleFast(app: *App) !void { try toggleFastForModel(app, provider_runtime.model(app), true); }
fn toggleFastForModel(app: *App, model: []const u8, announce: bool) !void { if (app.fast_mode) { try applyFastMode(app, false, announce, true); return; } if (!model_capabilities.resolveForApp(App, app, model).supports_fast_mode) { if (announce) { try app.writeDomainNotice(.{ .topic = "fast", .tone = .neutral, .body = "This model does not come with a fast mode.", }, true); } app.shell.render_requests.request(.footer); return; }
try applyFastMode(app, !app.fast_mode, announce, true); }
fn applyFastMode(app: *App, enabled: bool, announce: bool, persist: bool) !void { const previous = app.fast_mode; app.fast_mode = enabled; app.worker.syncQueuedPromptFastMode(app.fast_mode); debug_trace.logf( "session", "fast mode changed old={s} new={s} model={s} supports_fast={s}", .{ if (previous) "true" else "false", if (app.fast_mode) "true" else "false", provider_runtime.model(app), if (model_capabilities.resolveForApp(App, app, provider_runtime.model(app)).supports_fast_mode) "true" else "false", }, );
if (persist) { try persistPreferenceTargets( app, .{ .provider = provider_runtime.provider(app), .model = provider_runtime.model(app), .fast_mode = app.fast_mode, }, "fast", !announce, ); }
if (announce) { const label = if (app.fast_mode) "on" else "off"; try app.writeDomainNotice(.{ .topic = "fast", .tone = .neutral, .body = label }, true); if (comptime @hasDecl(App, "playInteractionSound")) app.playInteractionSound(); } app.shell.render_requests.request(.footer); }
fn applyEffort(app: *App, effort: types.ReasoningEffort, announce: bool, persist: bool) !void { app.effort = effort; app.worker.syncQueuedPromptEffort(app.effort);
if (persist) { try persistPreferenceTargets( app, .{ .effort = app.effort }, "effort", !announce, ); }
if (announce) { try app.writeDomainNotice(.{ .topic = "effort", .tone = .neutral, .body = app.effort.label() }, true); } app.shell.render_requests.request(.footer); }
pub fn selectEffortFromSettings(app: *App, effort: types.ReasoningEffort) !void { try applyEffort(app, effort, false, true); }
pub fn selectModelFromPicker(app: *App, model: []const u8, effort: types.ReasoningEffort, fast_mode: bool) !void { try setResolvedModelRuntime(app, model, true); var patch = app_session_runtime.SessionPreferencePatch{ .provider = provider_runtime.provider(app), .model = model, }; const capabilities = model_capabilities.resolveForApp(App, app, model); if (capabilities.reasoning_efforts.len > 0) { try applyEffort(app, effort, false, false); patch.effort = effort; } const selected_fast_mode = capabilities.supports_fast_mode and fast_mode; if (selected_fast_mode != app.fast_mode) { try applyFastMode(app, selected_fast_mode, false, false); } patch.fast_mode = selected_fast_mode; try persistPreferenceTargets(app, patch, "model picker", false); }
fn permissionWorkspaceRoot( app: *App, permission_scope: config_runtime.PermissionScope, ) ?[]const u8 { return switch (permission_scope) { .user => null, .local => app.workspace_root, }; }
fn replaceEffectivePermissionRules( app: *App, detailed: *config_runtime.DetailedSettings, ) void { app.permission_state.authority_mutex.lockUncancelable(io_mod.getIo()); defer app.permission_state.authority_mutex.unlock(io_mod.getIo()); if (detailed.settings.has_permission_rules) { app.permission_engine.replaceRules(app.alloc, detailed.settings.permission_rules); detailed.settings.permission_rules = .{}; detailed.settings.has_permission_rules = false; } else { app.permission_engine.rules.deinit(app.alloc); app.permission_engine.rules = .{}; } }
fn finishAllowlistMutation( app: *App, permission_scope: config_runtime.PermissionScope, verb: []const u8, target: AllowlistTarget, ) !void { var detailed = loadDetailedSettingsForNotice(app) catch |err| { const detail = try std.fmt.allocPrint( app.alloc, "saved but effective source unknown and runtime reload failed ({s})", .{@errorName(err)}, ); defer app.alloc.free(detail); const msg = try formatAllowlistChange(app.alloc, verb, target, permission_scope, detail); defer app.alloc.free(msg); try app.writeDomainNotice(.{ .topic = "allowlist", .tone = .warning, .body = msg }, true); return; }; defer detailed.deinit(app.alloc); if (postCommitResolutionError(detailed)) |err| { const detail = try std.fmt.allocPrint( app.alloc, "saved but effective source unknown and runtime reload failed ({s})", .{@errorName(err)}, ); defer app.alloc.free(detail); const msg = try formatAllowlistChange(app.alloc, verb, target, permission_scope, detail); defer app.alloc.free(msg); try app.writeDomainNotice(.{ .topic = "allowlist", .tone = .warning, .body = msg }, true); return; } replaceEffectivePermissionRules(app, &detailed);
const shadow = permissionShadowLabel(detailed.permission_sources, permission_scope); const detail = if (shadow.len > 0) try std.fmt.allocPrint(app.alloc, "user rules shadowed by {s}", .{shadow}) else try app.alloc.dupe(u8, ""); defer app.alloc.free(detail); const msg = try formatAllowlistChange(app.alloc, verb, target, permission_scope, detail); defer app.alloc.free(msg); try app.writeDomainNotice(.{ .topic = "allowlist", .tone = .neutral, .body = msg }, true); }
fn writeAllowlistStatus(app: *App, view: AllowlistView) !void { var detailed = loadDetailedSettingsForNotice(app) catch |err| { try writeSettingsLoadError(app, err); return; }; defer detailed.deinit(app.alloc); for (detailed.diagnostics) |diagnostic| { switch (diagnostic.cause) { .durable_path_unsafe => { try writeSettingsLoadError(app, error.DurablePathUnsafe); return; }, .private_state_permissions_unsupported => { try writeSettingsLoadError(app, error.PrivateStatePermissionsUnsupported); return; }, else => {}, } } replaceEffectivePermissionRules(app, &detailed);
const rules = switch (view) { .effective => app.permission_engine.rules.rules, .local => detailed.permission_sources.local.rules, .user => detailed.permission_sources.user.rules, };
var out: std.Io.Writer.Allocating = .init(app.alloc); defer out.deinit();
if (hasAllowlistRules(rules)) { try out.writer.print("{s} persistent allow rules:\n", .{@tagName(view)}); try writeAllowlistRuleGroups(&out.writer, rules); } else { try out.writer.print("{s} persistent allow rules: (none)\n", .{@tagName(view)}); } const shadow = permissionShadowLabel(detailed.permission_sources, .user); if ((view == .effective or view == .user) and detailed.permission_sources.user.rules.len > 0 and shadow.len > 0) { try out.writer.print("user rules are shadowed by {s}\n", .{shadow}); } const web_fetch_warning_count = permissions.webFetchRuleWarningCount(rules); if (web_fetch_warning_count > 0) { try out.writer.print("ignored {d} malformed web_fetch rule{s}; expected domain:<canonical-hostname>\n", .{ web_fetch_warning_count, if (web_fetch_warning_count == 1) "" else "s", }); }
const text = try out.toOwnedSlice(); defer app.alloc.free(text); try app.writeDomainNotice(.{ .topic = "allowlist", .tone = if (web_fetch_warning_count == 0) .neutral else .warning, .body = std.mem.trimEnd(u8, text, "\n"), }, true); }
fn writePermissionsStatus(app: *App) !void { const status = try app.permission_engine.formatPermissionsNoticeBody(app.alloc, app.workspace_root); defer app.alloc.free(status); const notice = try std.fmt.allocPrint(app.alloc, "{s}\n{s}", .{ status, permissions_usage }); defer app.alloc.free(notice); try app.writeDomainNotice(.{ .topic = "permissions", .tone = .neutral, .body = notice }, true); }
fn writeSettingsStatus(app: *App) !void { var detailed = config_runtime.loadMergedSettingsDetailed(app.alloc, app.workspace_root) catch |err| { try writeSettingsLoadError(app, err); return; }; defer detailed.deinit(app.alloc); const settings = &detailed.settings;
const startup_scrollback_label = if (settings.startup_scrollback orelse true) "on" else "off"; const msg = try std.fmt.allocPrint(app.alloc, "model: {s}\nmodel_config_source: {s}\npermission_mode: {s}\nworkspace: {s}\nstep_limit: {d}\nstartup_scrollback: {s}", .{ provider_runtime.model(app), @tagName(detailed.sources.models.get(model_provider.NameKey.fromProvider(.gateway))), permissions.permissionModeDisplayLabel(app.permission_engine.mode), app.workspace_root, app.agent_step_limit, startup_scrollback_label, }); defer app.alloc.free(msg); try app.writeDomainNotice(.{ .topic = "settings", .tone = .neutral, .body = msg }, true); }
fn saveStartupScrollbackSetting(app: *App, enabled: bool) !void { var attempt = config_runtime.attemptUserPreferences( app.alloc, .{ .startup_scrollback = enabled }, ); defer attempt.deinit(app.alloc); switch (attempt) { .failure => |failure| { try reportUserSettingsFailure( app, "startup-scrollback", failure.err, failure.cleanup, false, ); return; }, .outcome => |outcome| { const sources = try reportUserSettingsCommit( app, "startup-scrollback", .{ .startup_scrollback = enabled }, outcome, null, false, ); const label = if (enabled) "on" else "off"; var out: std.Io.Writer.Allocating = .init(app.alloc); defer out.deinit(); try out.writer.print("startup_scrollback: {s} ", .{label}); if (sources) |resolved| { switch (resolved.startup_scrollback) { .user_global => try out.writer.writeAll( "(applies on next launch)", ), else => try out.writer.print( "(saved user default; current source={s})", .{@tagName(resolved.startup_scrollback)}, ), } } else { try out.writer.writeAll( "(saved user default; next-launch source unknown)", ); } const msg = try out.toOwnedSlice(); defer app.alloc.free(msg); try app.writeDomainNotice(.{ .topic = "settings", .tone = .neutral, .body = msg }, true); return; }, } }
fn writeSettingsUsage(app: *App) !void { try app.writeDomainNotice(.{ .topic = "", .tone = .@"error", .body = "usage: /settings [startup-scrollback [on|off]]", }, true); }
fn writeSettingsLoadError(app: *App, err: anyerror) !void { const msg = try std.fmt.allocPrint(app.alloc, "Failed to load settings: {s}", .{@errorName(err)}); defer app.alloc.free(msg); try app.writeDomainNotice(.{ .topic = "settings", .tone = .@"error", .body = msg }, true); }
fn fetchModelIds(app: *App) !std.ArrayList([]u8) { return app.fetchModelIds(); }
fn resolveModelQuery(app: *App, query: []const u8) ![]u8 { if (try app.snapshotCachedModelIds(app.alloc)) |snapshot| { var ids = snapshot; defer freeStringList(app.alloc, &ids); if (try resolveModelQueryFromIds(app.alloc, ids.items, query)) |resolved| { return resolved; } }
var ids = fetchModelIds(app) catch { return app.alloc.dupe(u8, query); }; defer freeStringList(app.alloc, &ids);
if (try resolveModelQueryFromIds(app.alloc, ids.items, query)) |resolved| { return resolved; }
return app.alloc.dupe(u8, query); }
fn setResolvedModel(app: *App, resolved: []const u8, announce: bool) !void { const model_changed = !std.mem.eql(u8, provider_runtime.model(app), resolved); try setResolvedModelRuntime(app, resolved, announce); if (model_changed and app.fast_mode) { try applyFastMode(app, false, false, false); } try persistPreferenceTargets( app, .{ .provider = provider_runtime.provider(app), .model = resolved, .fast_mode = app.fast_mode, }, "model", !announce, ); }
fn persistPreferenceTargets( app: *App, patch: app_session_runtime.SessionPreferencePatch, label: []const u8, // False when the caller announces state; failures still report. announce_commit: bool, ) !void { var result = if (comptime @hasDecl(App, "persistRuntimePreferences")) blk: { break :blk app.persistRuntimePreferences(patch); } else blk: { var committed = app_session_runtime.PreferenceCommitResult{}; const attempt = config_runtime.attemptUserPreferences( app.alloc, patch.userSettingsPatch(), ); switch (attempt) { .outcome => |outcome| committed.settings_outcome = outcome, .failure => |failure| { committed.settings_error = failure.err; committed.settings_failure_cleanup = failure.cleanup; }, } break :blk committed; }; defer result.deinit(app.alloc);
if (result.settings_error) |err| { debug_trace.logf( "session", "{s} settings persistence failed err={s}", .{ label, @errorName(err) }, ); try reportUserSettingsFailure( app, label, err, result.settings_failure_cleanup, true, ); } else if (result.settings_outcome) |outcome| { _ = try reportUserSettingsCommit( app, label, patch.userSettingsPatch(), outcome, result.session_error, announce_commit, ); } if (result.session_error) |err| { debug_trace.logf( "session", "{s} session persistence failed err={s}", .{ label, @errorName(err) }, ); if (result.settings_outcome == null) { try app.writeDomainNotice(.{ .topic = label, .tone = .@"error", .body = "failed to persist current session", }, true); } } }
fn setResolvedModelRuntime(app: *App, resolved: []const u8, announce: bool) !void { if (!std.mem.eql(u8, provider_runtime.model(app), resolved)) { try provider_runtime.replaceModel(app, resolved); } const selected = provider_runtime.model(app); try app.worker.syncQueuedPromptModel(std.heap.c_allocator, selected); if (comptime @hasDecl(App, "persistAcceptedModel")) try app.persistAcceptedModel(selected); app_session_runtime.Runtime(App).syncTerminalTitle(app);
if (announce) { const active_response = if (comptime @hasField(App, "stream")) app.stream.active else false; const prefix: []const u8 = if (active_response) "Next turn will use " else "Switched to "; const line = try std.fmt.allocPrint(app.alloc, "{s}{s}", .{ prefix, selected }); defer app.alloc.free(line); try app.writeDomainNotice(.{ .topic = "", .tone = .neutral, .body = line }, true); if (comptime @hasDecl(App, "playInteractionSound")) app.playInteractionSound(); } }
fn resolveModelQueryFromIds(alloc: std.mem.Allocator, ids: []const []u8, query: []const u8) !?[]u8 { for (ids) |id| { if (std.ascii.eqlIgnoreCase(id, query)) { return try alloc.dupe(u8, id); } }
var best_score: i32 = 0; var best_id: ?[]const u8 = null;
for (ids) |id| { const score = fuzzyModelScore(id, query); if (score > best_score) { best_score = score; best_id = id; } }
if (best_id) |id| { return try alloc.dupe(u8, id); }
return null; } };}
fn startsWithWord(text: []const u8, word: []const u8) bool { if (!std.ascii.startsWithIgnoreCase(text, word)) return false; if (text.len == word.len) return true; return text[word.len] == ' ' or text[word.len] == '\t';}
const AllowlistKind = enum { command, tool, url, web_fetch_domain,};
const AllowlistTarget = struct { kind: AllowlistKind, category: []const u8, pattern: []const u8, pattern_owned: bool = false,
fn deinit(self: AllowlistTarget, alloc: std.mem.Allocator) void { if (self.pattern_owned) alloc.free(self.pattern); }};
fn parseAllowlistTargetAlloc( alloc: std.mem.Allocator, tool_registry: tool_dispatch.Registry, raw: []const u8,) !?AllowlistTarget { const trimmed = std.mem.trim(u8, raw, " \t"); const kind_split = splitFirstWord(trimmed) orelse return null; const pattern = parseQuotedOrRest(kind_split.rest); if (pattern.len == 0) return null;
if (std.ascii.eqlIgnoreCase(kind_split.word, "web-fetch-domain")) { const canonical = permissions.canonicalWebFetchDomainPattern(alloc, pattern) catch |err| switch (err) { error.OutOfMemory => return err, else => return null, }; return .{ .kind = .web_fetch_domain, .category = permissions.web_fetch_permission, .pattern = canonical, .pattern_owned = true, }; }
return parseAllowlistTarget(tool_registry, raw);}
fn parseAllowlistTarget(tool_registry: tool_dispatch.Registry, raw: []const u8) ?AllowlistTarget { const trimmed = std.mem.trim(u8, raw, " \t"); const kind_split = splitFirstWord(trimmed) orelse return null; const pattern = parseQuotedOrRest(kind_split.rest); if (pattern.len == 0) return null;
if (std.ascii.eqlIgnoreCase(kind_split.word, "command")) { return .{ .kind = .command, .category = "bash", .pattern = pattern }; } if (std.ascii.eqlIgnoreCase(kind_split.word, "url")) { return .{ .kind = .url, .category = "url", .pattern = pattern }; } if (std.ascii.eqlIgnoreCase(kind_split.word, "tool")) { if (std.mem.eql(u8, pattern, permissions.web_fetch_permission)) return null; if (!isKnownAllowlistTool(tool_registry, pattern)) return null; return .{ .kind = .tool, .category = permissions.permissionNameForTool(pattern), .pattern = "*" }; }
return null;}
fn isKnownAllowlistTool(tool_registry: tool_dispatch.Registry, name: []const u8) bool { if (tool_registry.lookup(name) != null) return true;
const categories = [_][]const u8{ "edit", "read", "glob", "grep", "skill", permissions.web_search_permission, }; for (categories) |category| { if (std.mem.eql(u8, name, category)) return true; } return false;}
const WordSplit = struct { word: []const u8, rest: []const u8,};
fn splitFirstWord(text: []const u8) ?WordSplit { const trimmed = std.mem.trim(u8, text, " \t"); if (trimmed.len == 0) return null; for (trimmed, 0..) |c, idx| { if (c == ' ' or c == '\t') { return .{ .word = trimmed[0..idx], .rest = std.mem.trimStart(u8, trimmed[idx + 1 ..], " \t"), }; } } return .{ .word = trimmed, .rest = "" };}
fn parseQuotedOrRest(raw: []const u8) []const u8 { const trimmed = std.mem.trim(u8, raw, " \t"); if (trimmed.len > 0 and trimmed[0] == '"') { if (std.mem.findScalarPos(u8, trimmed, 1, '"')) |close| { return trimmed[1..close]; } return trimmed[1..]; } return trimmed;}
fn parseAllowlistResetScope(raw: []const u8) ?config_runtime.AllowlistResetScope { const trimmed = std.mem.trim(u8, raw, " \t"); if (std.ascii.eqlIgnoreCase(trimmed, "all")) return .all; if (std.ascii.eqlIgnoreCase(trimmed, "command") or std.ascii.eqlIgnoreCase(trimmed, "commands")) return .commands; if (std.ascii.eqlIgnoreCase(trimmed, "tool") or std.ascii.eqlIgnoreCase(trimmed, "tools")) return .tools; if (std.ascii.eqlIgnoreCase(trimmed, "url") or std.ascii.eqlIgnoreCase(trimmed, "urls")) return .urls; if (std.ascii.eqlIgnoreCase(trimmed, "web-fetch-domain") or std.ascii.eqlIgnoreCase(trimmed, "web-fetch-domains")) return .web_fetch_domains; return null;}
const AllowlistView = enum { effective, local, user,};
fn parseAllowlistView(raw: []const u8) ?AllowlistView { const trimmed = std.mem.trim(u8, raw, " \t"); if (trimmed.len == 0 or std.ascii.eqlIgnoreCase(trimmed, "effective")) return .effective; if (std.ascii.eqlIgnoreCase(trimmed, "local")) return .local; if (std.ascii.eqlIgnoreCase(trimmed, "user")) return .user; return null;}
fn permissionShadowLabel( views: config_runtime.PermissionSourceViews, permission_scope: config_runtime.PermissionScope,) []const u8 { if (permission_scope != .user or views.user.rules.len == 0) return ""; if (views.user_shadowed_by_local) return "local settings"; return "";}
fn allowlistResetScopeLabel(scope: config_runtime.AllowlistResetScope) []const u8 { return switch (scope) { .all => "all", .commands => "commands", .tools => "tools", .urls => "urls", .web_fetch_domains => "web-fetch-domains", };}
const AllowlistDisplayKind = enum { tool, command, url, web_fetch_domain,};
const AllowlistRuleGroup = struct { kind: AllowlistDisplayKind, name: []const u8,};
fn hasAllowlistRules(rules: []const types.PermissionRule) bool { for (rules) |rule| { if (allowlistRuleDisplayable(rule)) return true; } return false;}
fn writeAllowlistRuleGroups(writer: *std.Io.Writer, rules: []const types.PermissionRule) !void { try writeAllowlistSection(writer, rules, .tool); try writeAllowlistSection(writer, rules, .command); try writeAllowlistSection(writer, rules, .url); try writeAllowlistSection(writer, rules, .web_fetch_domain);}
fn writeAllowlistSection(writer: *std.Io.Writer, rules: []const types.PermissionRule, kind: AllowlistDisplayKind) !void { if (!hasAllowlistKind(rules, kind)) return;
switch (kind) { .tool => try writer.writeAll(" tools:\n"), .command => try writer.writeAll(" commands:\n"), .url => try writer.writeAll(" urls:\n"), .web_fetch_domain => try writer.writeAll(" web-fetch domains:\n"), }
for (rules, 0..) |rule, idx| { if (!allowlistRuleDisplayable(rule)) continue; const group = allowlistRuleGroup(rule); if (group.kind != kind) continue; if (allowlistGroupSeen(rules[0..idx], group)) continue; try writeAllowlistGroupLine(writer, rules, group); }}
fn hasAllowlistKind(rules: []const types.PermissionRule, kind: AllowlistDisplayKind) bool { for (rules) |rule| { if (!allowlistRuleDisplayable(rule)) continue; if (allowlistRuleGroup(rule).kind == kind) return true; } return false;}
fn writeAllowlistGroupLine(writer: *std.Io.Writer, rules: []const types.PermissionRule, group: AllowlistRuleGroup) !void { switch (group.kind) { .tool => try writer.print(" {s}: ", .{group.name}), .command, .url, .web_fetch_domain => try writer.writeAll(" "), }
var first = true; for (rules) |rule| { if (!allowlistRuleDisplayable(rule)) continue; if (!sameAllowlistGroup(allowlistRuleGroup(rule), group)) continue; if (!first) try writer.writeAll(", "); first = false; try writeAllowlistPattern(writer, group, rule.pattern); } try writer.writeByte('\n');}
fn writeAllowlistPattern(writer: *std.Io.Writer, group: AllowlistRuleGroup, pattern: []const u8) !void { if (group.kind == .tool and std.mem.eql(u8, pattern, "*") and isWorkspacePathToolPermission(group.name)) { try writer.writeAll("workspace"); return; } try writer.writeAll(pattern);}
fn isWorkspacePathToolPermission(permission: []const u8) bool { const path_permissions = [_][]const u8{ "edit", "read", "glob", "grep", }; for (path_permissions) |path_permission| { if (std.mem.eql(u8, permission, path_permission)) return true; } return false;}
fn allowlistGroupSeen(rules: []const types.PermissionRule, group: AllowlistRuleGroup) bool { for (rules) |rule| { if (!allowlistRuleDisplayable(rule)) continue; if (sameAllowlistGroup(allowlistRuleGroup(rule), group)) return true; } return false;}
fn allowlistRuleGroup(rule: types.PermissionRule) AllowlistRuleGroup { if (std.mem.eql(u8, rule.permission, "bash")) { return .{ .kind = .command, .name = "command" }; } if (std.mem.eql(u8, rule.permission, "url")) { return .{ .kind = .url, .name = "url" }; } if (std.mem.eql(u8, rule.permission, permissions.web_fetch_permission)) { return .{ .kind = .web_fetch_domain, .name = "web-fetch-domain" }; } return .{ .kind = .tool, .name = rule.permission };}
fn allowlistRuleDisplayable(rule: types.PermissionRule) bool { if (rule.action != .allow) return false; if (std.mem.eql(u8, rule.permission, permissions.web_fetch_permission)) { return permissions.isCanonicalWebFetchDomainPattern(rule.pattern); } return true;}
fn sameAllowlistGroup(a: AllowlistRuleGroup, b: AllowlistRuleGroup) bool { return a.kind == b.kind and std.mem.eql(u8, a.name, b.name);}
fn formatAllowlistChange( alloc: std.mem.Allocator, verb: []const u8, target: AllowlistTarget, permission_scope: config_runtime.PermissionScope, detail: []const u8,) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try out.writer.print("{s} ", .{verb}); switch (target.kind) { .command => try out.writer.print("command: \"{s}\"", .{target.pattern}), .url => try out.writer.print("url: \"{s}\"", .{target.pattern}), .tool => try out.writer.print("tool {s}: \"{s}\"", .{ target.category, target.pattern }), .web_fetch_domain => try out.writer.print("web-fetch-domain: \"{s}\"", .{target.pattern}), } try out.writer.print(" (scope={s})", .{@tagName(permission_scope)}); if (detail.len > 0) try out.writer.print("; {s}", .{detail}); return out.toOwnedSlice();}
/// Scores prefix and substring matches above token, subsequence, and partial matches./// Higher scores rank first; zero means no match.pub fn fuzzyModelScore(id: []const u8, query: []const u8) i32 { if (query.len == 0) return 0; if (id.len == 0) return 0;
if (containsIgnoreCase(id, query)) { const bonus: i32 = @intCast(@min(query.len, 50)); if (std.mem.startsWith(u8, id, query) or (id.len > query.len and id[id.len - query.len - 1] == '/' and std.ascii.eqlIgnoreCase(id[id.len - query.len ..], query))) { return 120 + bonus; } return 100 + bonus; }
var tokens_buf: [16][]const u8 = undefined; const token_count = splitQueryTokens(query, &tokens_buf); if (token_count > 1) { var all_present = true; for (tokens_buf[0..token_count]) |token| { if (!containsIgnoreCase(id, token)) { all_present = false; break; } } if (all_present) { return 80 + @as(i32, @intCast(token_count)) * 5; } }
var qi: usize = 0; var matched: usize = 0; for (id) |c| { if (qi < query.len and std.ascii.toLower(c) == std.ascii.toLower(query[qi])) { qi += 1; matched += 1; } } if (qi == query.len) { return 40 + @as(i32, @intCast(@min(matched, 20))); }
if (token_count >= 1) { var any_match = false; for (tokens_buf[0..token_count]) |token| { if (containsIgnoreCase(id, token)) { any_match = true; break; } } if (any_match) return 20; }
return 0;}
fn splitQueryTokens(query: []const u8, out: *[16][]const u8) usize { var count: usize = 0; var i: usize = 0; while (i < query.len and count < 16) { while (i < query.len and isSplitter(query[i])) : (i += 1) {} const start = i; while (i < query.len and !isSplitter(query[i])) : (i += 1) {} if (i > start) { out[count] = query[start..i]; count += 1; } } return count;}
fn isSplitter(c: u8) bool { return c == ' ' or c == '-' or c == '/' or c == '_';}
const FakeSession = struct { history_len: usize = 0,
fn historyLen(self: *FakeSession) usize { return self.history_len; }};
const FakeWorker = struct { synced_mode: ?types.PermissionMode = null, synced_state_mode: ?types.PermissionMode = null, synced_model: ?[]u8 = null, synced_model_alloc: ?std.mem.Allocator = null, synced_fast_mode: ?bool = null, fast_sync_count: usize = 0, synced_effort: ?types.ReasoningEffort = null, effort_sync_count: usize = 0,
fn deinit(self: *FakeWorker) void { if (self.synced_model) |model| self.synced_model_alloc.?.free(model); self.* = .{}; }
pub fn syncQueuedPromptPermissionSnapshot(self: *FakeWorker, snapshot: worker_runtime.PermissionSnapshot) void { self.synced_mode = snapshot.mode; }
pub fn syncQueuedPromptPermissionState(self: *FakeWorker, alloc: std.mem.Allocator, grants: []const types.PermissionGrant, snapshot: worker_runtime.PermissionSnapshot) !void { _ = alloc; _ = grants; self.synced_state_mode = snapshot.mode; }
fn syncQueuedPromptModel(self: *FakeWorker, alloc: std.mem.Allocator, model: []const u8) !void { if (self.synced_model) |old| { self.synced_model_alloc.?.free(old); self.synced_model = null; self.synced_model_alloc = null; } self.synced_model = try alloc.dupe(u8, model); self.synced_model_alloc = alloc; }
fn syncQueuedPromptFastMode(self: *FakeWorker, enabled: bool) void { self.synced_fast_mode = enabled; self.fast_sync_count += 1; }
fn syncQueuedPromptEffort(self: *FakeWorker, effort: types.ReasoningEffort) void { self.synced_effort = effort; self.effort_sync_count += 1; }};
const FakeHistoryCommandApp = struct { alloc: std.mem.Allocator, workspace_root: []u8, prompt_history: prompt_history_runtime.PromptHistoryRuntime = .{}, transcript: std.ArrayList(u8) = .empty, last_tone: ?types.NoticeTone = null,
fn init( alloc: std.mem.Allocator, home: []const u8, workspace_root: []const u8, ) !FakeHistoryCommandApp { var app = FakeHistoryCommandApp{ .alloc = alloc, .workspace_root = try alloc.dupe(u8, workspace_root), }; errdefer app.deinit(); _ = try app.prompt_history.initialize(alloc, home, true, true); return app; }
fn deinit(self: *FakeHistoryCommandApp) void { self.prompt_history.deinit(self.alloc); self.transcript.deinit(self.alloc); self.alloc.free(self.workspace_root); self.* = undefined; }
fn writeDomainNotice(self: *FakeHistoryCommandApp, notice: types.SemanticNotice, _: bool) !void { self.last_tone = notice.tone; try self.transcript.appendSlice(self.alloc, notice.body); try self.transcript.append(self.alloc, '\n'); }};
const FakeApp = struct { alloc: std.mem.Allocator, workspace_root: []u8, tool_registry: tool_dispatch.Registry = .{}, selected_model: std.ArrayList(u8) = .empty, selected_provider: model_provider.ProviderId = .gateway, auth: auth_runtime.Runtime = .{}, permission_engine: permissions.PermissionEngine = .{}, permission_state: app_permission_runtime.State = .{}, session: FakeSession = .{}, worker: FakeWorker = .{}, shell: struct { render_requests: render_request.RenderRequestState = .{}, } = .{}, transcript: std.ArrayList(u8) = .empty, agent_step_limit: usize = 24, fast_mode: bool = false, effort: types.ReasoningEffort = .auto, cached_ids: ?[]const []const u8 = null, gateway_metadata_model: ?[]const u8 = null, gateway_metadata: model_capabilities.GatewayMetadata = .{}, fetch_ids: []const []const u8 = &.{}, fail_fetch: bool = false, last_tone: ?types.NoticeTone = null, preference_commit_count: usize = 0, last_preference_model: std.ArrayList(u8) = .empty, last_preference_provider: ?model_provider.ProviderId = null, last_preference_effort: ?types.ReasoningEffort = null, last_preference_fast_mode: ?bool = null, preference_settings_error: ?anyerror = null, preference_session_error: ?anyerror = null, preference_failure_cleanup: config_runtime.LegacyCleanup = .{}, post_commit_resolution_error: ?anyerror = null, post_commit_resolution_diagnostic: ?config_runtime.ConfigDiagnosticCause = null, post_commit_resolution_count: usize = 0, permission_mode_preference_commit_count: usize = 0, last_preference_permission_mode: ?types.PermissionMode = null, semantic_write_count: usize = 0, terminal_title_label: [128]u8 = undefined, terminal_title_label_len: usize = 0,
fn init(alloc: std.mem.Allocator, workspace_root: []const u8, model: []const u8) !FakeApp { var app = FakeApp{ .alloc = alloc, .workspace_root = try alloc.dupe(u8, workspace_root), }; errdefer app.deinit(); try app.selected_model.appendSlice(alloc, model); return app; }
fn deinit(self: *FakeApp) void { self.selected_model.deinit(self.alloc); self.auth.deinit(self.alloc); self.permission_engine.deinit(self.alloc); self.worker.deinit(); self.transcript.deinit(self.alloc); self.last_preference_model.deinit(self.alloc); self.preference_failure_cleanup.deinit(self.alloc); self.alloc.free(self.workspace_root); self.* = undefined; }
fn writeDomainNotice(self: *FakeApp, notice: types.SemanticNotice, _: bool) !void { self.semantic_write_count += 1; self.last_tone = notice.tone; const rendered = if (notice.topic.len > 0) try std.fmt.allocPrint(self.alloc, "{s} {s}: {s}\n", .{ types.noticeGlyph(notice.tone), notice.topic, notice.body, }) else try std.fmt.allocPrint(self.alloc, "{s} {s}\n", .{ types.noticeGlyph(notice.tone), notice.body }); defer self.alloc.free(rendered); try self.transcript.appendSlice(self.alloc, rendered); }
fn text(self: *const FakeApp) []const u8 { return self.transcript.items; }
fn clearTranscript(self: *FakeApp) void { self.transcript.clearRetainingCapacity(); }
fn toolRegistry(self: *const FakeApp) tool_dispatch.Registry { return self.tool_registry; }
// Public so `@hasDecl` sees it from the session runtime, which is where // the terminal title is now resolved. pub fn terminalTitle(self: *FakeApp) host.TerminalTitle { return .{ .context = self, .set_fn = setTerminalTitleLabelForTest, .clear_fn = clearTerminalTitleForTest, }; }
fn setTerminalTitleLabelForTest(raw: ?*anyopaque, label: []const u8) void { const self: *FakeApp = @ptrCast(@alignCast(raw.?)); self.terminal_title_label_len = @min(label.len, self.terminal_title_label.len); @memcpy( self.terminal_title_label[0..self.terminal_title_label_len], label[0..self.terminal_title_label_len], ); }
fn clearTerminalTitleForTest(raw: ?*anyopaque) void { const self: *FakeApp = @ptrCast(@alignCast(raw.?)); self.terminal_title_label_len = 0; }
fn terminalTitleLabelText(self: *const FakeApp) []const u8 { return self.terminal_title_label[0..self.terminal_title_label_len]; }
fn snapshotCachedModelIds(self: *FakeApp, alloc: std.mem.Allocator) !?std.ArrayList([]u8) { if (self.cached_ids) |ids| return try makeStringList(alloc, ids); return null; }
pub fn persistPermissionModePreference(self: *FakeApp, mode: types.PermissionMode) !void { self.permission_mode_preference_commit_count += 1; self.last_preference_permission_mode = mode; }
pub fn resolvedModelCapabilities(self: *FakeApp, model: []const u8) model_capabilities.Capabilities { if (self.gateway_metadata_model) |metadata_model| { if (std.mem.eql(u8, metadata_model, model)) { return model_capabilities.resolveCapabilities(model, self.gateway_metadata); } } return model_capabilities.capabilitiesForModel(model); }
fn setGatewayControls( self: *FakeApp, model: []const u8, efforts: []const types.ReasoningEffort, supports_fast_mode: bool, ) void { self.gateway_metadata_model = model; self.gateway_metadata = .{ .reasoning_efforts = .fromSlice(efforts), .supports_fast_mode = supports_fast_mode, }; }
fn fetchModelIds(self: *FakeApp) !std.ArrayList([]u8) { if (self.fail_fetch) return error.FetchFailed; return makeStringList(self.alloc, self.fetch_ids); }
fn loadDetailedSettingsForNotice(self: *FakeApp) !config_runtime.DetailedSettings { self.post_commit_resolution_count += 1; if (self.post_commit_resolution_error) |err| return err; if (self.post_commit_resolution_diagnostic) |cause| { const diagnostics = try self.alloc.alloc(config_runtime.ConfigDiagnostic, 1); diagnostics[0] = .{ .layer = .user, .cause = cause }; return .{ .settings = .{}, .diagnostics = diagnostics, }; } return config_runtime.loadMergedSettingsDetailed(self.alloc, self.workspace_root); }
fn persistRuntimePreferences( self: *FakeApp, patch: app_session_runtime.SessionPreferencePatch, ) app_session_runtime.PreferenceCommitResult { self.preference_commit_count += 1; self.last_preference_provider = patch.provider; self.last_preference_model.clearRetainingCapacity(); if (patch.model) |model| { self.last_preference_model.appendSlice(self.alloc, model) catch return .{ .settings_error = error.OutOfMemory }; } self.last_preference_effort = patch.effort; self.last_preference_fast_mode = patch.fast_mode; if (self.preference_settings_error == null) { const attempt = config_runtime.attemptUserPreferences( self.alloc, patch.userSettingsPatch(), ); return switch (attempt) { .outcome => |outcome| .{ .settings_outcome = outcome, .session_error = self.preference_session_error, }, .failure => |failure| .{ .settings_error = failure.err, .settings_failure_cleanup = failure.cleanup, .session_error = self.preference_session_error, }, }; } const cleanup = self.preference_failure_cleanup; self.preference_failure_cleanup = .{}; return .{ .settings_error = self.preference_settings_error, .settings_failure_cleanup = cleanup, .session_error = self.preference_session_error, }; }};
fn makeStringList(alloc: std.mem.Allocator, ids: []const []const u8) !std.ArrayList([]u8) { var list: std.ArrayList([]u8) = .empty; errdefer freeStringList(alloc, &list); for (ids) |id| { try list.append(alloc, try alloc.dupe(u8, id)); } return list;}
fn expectTranscriptContains(app: *const FakeApp, needle: []const u8) !void { try std.testing.expect(std.mem.find(u8, app.text(), needle) != null);}
var session_command_stable_test_environ: ?*std.process.Environ.Map = null;
fn stableSessionCommandTestEnviron() !*const std.process.Environ.Map { if (session_command_stable_test_environ) |map| return map;
const alloc = std.heap.page_allocator; const map = try alloc.create(std.process.Environ.Map); map.* = std.process.Environ.Map.init(alloc); session_command_stable_test_environ = map; return map;}
const SessionCommandTestHome = struct { alloc: std.mem.Allocator, map: std.process.Environ.Map,
fn install(alloc: std.mem.Allocator, home: ?[]const u8) !*SessionCommandTestHome { _ = try stableSessionCommandTestEnviron();
const self = try alloc.create(SessionCommandTestHome); errdefer alloc.destroy(self);
self.* = .{ .alloc = alloc, .map = std.process.Environ.Map.init(alloc), }; errdefer self.map.deinit();
if (home) |value| { try self.map.put("HOME", value); } io_mod.setEnvironMap(&self.map); return self; }
fn deinit(self: *SessionCommandTestHome) void { if (session_command_stable_test_environ) |map| { io_mod.setEnvironMap(map); } self.map.deinit(); const alloc = self.alloc; alloc.destroy(self); }};
fn expectRule(rule: types.PermissionRule, permission: []const u8, pattern: []const u8, action: types.PermissionAction) !void { try std.testing.expectEqualStrings(permission, rule.permission); try std.testing.expectEqualStrings(pattern, rule.pattern); try std.testing.expectEqual(action, rule.action);}
fn writeFixtureFile(dir: std.Io.Dir, sub_path: []const u8, text: []const u8) !void { var file = try dir.createFile(io_mod.getIo(), sub_path, .{ .truncate = true }); defer file.close(io_mod.getIo()); try file.writeStreamingAll(io_mod.getIo(), text);}
test "session_commands showStatus writes session status snapshot" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const home_root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(home_root); const env = try SessionCommandTestHome.install(alloc, home_root); defer env.deinit(); var app = try FakeApp.init(alloc, "/tmp/workspace", "anthropic/test-model"); defer app.deinit(); app.permission_engine.mode = .auto; app.session.history_len = 7;
try Commands(FakeApp).showStatus(&app);
try expectTranscriptContains(&app, "* status: model=anthropic/test-model\n"); try expectTranscriptContains(&app, "auth=missing\n"); try expectTranscriptContains(&app, "auth_refreshable=false\n"); try expectTranscriptContains(&app, "permission_mode=auto\n"); try expectTranscriptContains(&app, "history_turns=7\n"); try expectTranscriptContains(&app, "agent_step_limit=24\n");
app.clearTranscript(); app.permission_engine.mode = .yolo; try Commands(FakeApp).showStatus(&app); try expectTranscriptContains(&app, "permission_mode=full access\n");}
test "session_commands history setting toggles durable input history" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "home"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "home"); defer alloc.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc( alloc, tmp.dir, "workspace", ); defer alloc.free(workspace_root);
const env = try SessionCommandTestHome.install(alloc, home_root); defer env.deinit(); var app = try FakeHistoryCommandApp.init( alloc, home_root, workspace_root, ); defer app.deinit(); try Commands(FakeHistoryCommandApp).handleHistory(&app, "off"); try std.testing.expect(!app.prompt_history.enabled); var disabled = try config_runtime.loadMergedSettings( alloc, workspace_root, ); defer disabled.deinit(alloc); try std.testing.expectEqual(false, disabled.prompt_history_enabled.?);
try Commands(FakeHistoryCommandApp).handleHistory(&app, "on"); try std.testing.expect(app.prompt_history.enabled); var enabled = try config_runtime.loadMergedSettings( alloc, workspace_root, ); defer enabled.deinit(alloc); try std.testing.expectEqual(true, enabled.prompt_history_enabled.?);}
test "session_commands handleSettings shows startup scrollback status" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try writeFixtureFile(tmp.dir, "home/.fx/settings.json", "{\"startup_scrollback\":false}"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit();
var app = try FakeApp.init(std.testing.allocator, workspace_root, "anthropic/test-model"); defer app.deinit(); app.permission_engine.mode = .auto; app.agent_step_limit = 12;
try Commands(FakeApp).handleSettings(&app, "");
try expectTranscriptContains(&app, "model: anthropic/test-model"); try expectTranscriptContains(&app, "permission_mode: auto"); try expectTranscriptContains(&app, "step_limit: 12"); try expectTranscriptContains(&app, "startup_scrollback: off");}
test "session_commands handleSettings toggles and persists startup scrollback" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit();
var app = try FakeApp.init(std.testing.allocator, workspace_root, "anthropic/test-model"); defer app.deinit();
try Commands(FakeApp).handleSettings(&app, "startup-scrollback"); try expectTranscriptContains(&app, "startup_scrollback: off (applies on next launch)");
var toggled = try config_runtime.loadMergedSettings(std.testing.allocator, workspace_root); defer toggled.deinit(std.testing.allocator); try std.testing.expectEqual(false, toggled.startup_scrollback.?);
app.clearTranscript(); try Commands(FakeApp).handleSettings(&app, "startup-scrollback on"); try expectTranscriptContains(&app, "startup_scrollback: on (applies on next launch)");
var enabled = try config_runtime.loadMergedSettings(std.testing.allocator, workspace_root); defer enabled.deinit(std.testing.allocator); try std.testing.expectEqual(true, enabled.startup_scrollback.?);
app.clearTranscript(); try Commands(FakeApp).handleSettings(&app, "startup-scrollback off"); try expectTranscriptContains(&app, "startup_scrollback: off (applies on next launch)");
var disabled = try config_runtime.loadMergedSettings(std.testing.allocator, workspace_root); defer disabled.deinit(std.testing.allocator); try std.testing.expectEqual(false, disabled.startup_scrollback.?);}
test "session_commands startup scrollback ignores project profile-only shadowing" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try writeFixtureFile( tmp.dir, "workspace/.fx.json", "{\"startup_scrollback\":true}\n", ); const home_root = try io_mod.dirRealpathAlloc( std.testing.allocator, tmp.dir, "home", ); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc( std.testing.allocator, tmp.dir, "workspace", ); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install( std.testing.allocator, home_root, ); defer home.deinit();
var app = try FakeApp.init( std.testing.allocator, workspace_root, "anthropic/test-model", ); defer app.deinit();
try Commands(FakeApp).handleSettings( &app, "startup-scrollback off", );
try expectTranscriptContains(&app, "startup_scrollback: off (applies on next launch)"); try std.testing.expect(std.mem.find(u8, app.text(), "higher-precedence startup_scrollback=project") == null);}
test "session_commands statusline shadow notice names the preference field" { var out: std.Io.Writer.Allocating = .init(std.testing.allocator); defer out.deinit();
try appendShadowedUserSources( &out.writer, .{ .statusline_item = .{ .item = .context, .enabled = false, }, }, .{ .statusline_context = .project }, );
try std.testing.expectEqualStrings( "; fresh sessions here use higher-precedence statusLine.context=project", out.written(), );}
test "session_commands notification shadow notice preserves valid workspace scope" { var out: std.Io.Writer.Allocating = .init(std.testing.allocator); defer out.deinit();
try appendShadowedUserSources( &out.writer, .{ .notification_turn_end = true, .notification_max = true, }, .{ .notification_turn_end = .user_workspace, .notification_max = .user_workspace, }, );
try std.testing.expectEqualStrings( "; fresh sessions here use higher-precedence notifications.turn_end=user_workspace, notifications.max=user_workspace", out.written(), );}
test "session_commands handleSettings reports usage and save failures" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const env = try SessionCommandTestHome.install(std.testing.allocator, null); defer env.deinit();
var app = try FakeApp.init(std.testing.allocator, workspace_root, "anthropic/test-model"); defer app.deinit();
try Commands(FakeApp).handleSettings(&app, "bogus"); try expectTranscriptContains(&app, "usage: /settings [startup-scrollback [on|off]]");
app.clearTranscript(); try Commands(FakeApp).handleSettings(&app, "startup-scrollback off extra"); try expectTranscriptContains(&app, "usage: /settings [startup-scrollback [on|off]]");
app.clearTranscript(); try Commands(FakeApp).handleSettings(&app, "startup-scrollback off"); try expectTranscriptContains(&app, "✗ startup-scrollback: not saved to user settings (HomeNotSet)"); try std.testing.expectEqual(types.NoticeTone.@"error", app.last_tone.?);}
test "session_commands handleModel reports current model for empty query" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "anthropic/claude-opus-4.6"); defer app.deinit();
try Commands(FakeApp).handleModel(&app, "");
try std.testing.expectEqualStrings("* model: anthropic/claude-opus-4.6\n", app.text()); try std.testing.expect(app.worker.synced_model == null); try std.testing.expectEqualStrings("", app.terminalTitleLabelText());}
test "session_commands handleModel resolves fuzzy cached model and syncs queued prompts" { const alloc = std.testing.allocator; const ids = [_][]const u8{ "openai/gpt-4o-mini", "anthropic/claude-sonnet-4-20250514", }; var app = try FakeApp.init(alloc, "/tmp/workspace", "openai/gpt-4o"); defer app.deinit(); app.cached_ids = &ids; app.selected_provider = .codex;
try Commands(FakeApp).handleModel(&app, "claude sonnet");
try std.testing.expectEqualStrings("anthropic/claude-sonnet-4-20250514", app.selected_model.items); try std.testing.expectEqualStrings("anthropic/claude-sonnet-4-20250514", app.worker.synced_model.?); try std.testing.expectEqual(model_provider.ProviderId.codex, app.last_preference_provider.?); try std.testing.expectEqualStrings( "fx v" ++ build_options.app_version ++ " | workspace", app.terminalTitleLabelText(), ); try expectTranscriptContains(&app, "* Switched to anthropic/claude-sonnet-4-20250514");}
test "session_commands handleModel falls back to raw query when model fetch fails" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "openai/gpt-4o"); defer app.deinit(); app.fail_fetch = true;
try Commands(FakeApp).handleModel(&app, "custom/provider-model");
try std.testing.expectEqualStrings("custom/provider-model", app.selected_model.items); try std.testing.expectEqualStrings("custom/provider-model", app.worker.synced_model.?); try std.testing.expectEqualStrings( "fx v" ++ build_options.app_version ++ " | workspace", app.terminalTitleLabelText(), );}
test "session_commands handlePermissions persists modes and reset clears session grants" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "anthropic/claude-opus-4.6"); defer app.deinit(); try app.permission_engine.allow(alloc, "bash", "git *");
try Commands(FakeApp).handlePermissions(&app, "auto"); try std.testing.expectEqual(types.PermissionMode.auto, app.permission_engine.mode); try std.testing.expectEqual(types.PermissionMode.auto, app.worker.synced_mode.?); try std.testing.expectEqual(@as(usize, 1), app.permission_mode_preference_commit_count); try std.testing.expectEqual(@as(?types.PermissionMode, .auto), app.last_preference_permission_mode); try expectTranscriptContains(&app, "mode set to auto");
app.clearTranscript(); try Commands(FakeApp).handlePermissions(&app, "yolo"); try std.testing.expectEqual(types.PermissionMode.yolo, app.permission_engine.mode); try std.testing.expectEqual(types.PermissionMode.yolo, app.worker.synced_mode.?); try std.testing.expectEqual(@as(usize, 2), app.permission_mode_preference_commit_count); try std.testing.expectEqual(@as(?types.PermissionMode, .yolo), app.last_preference_permission_mode); try expectTranscriptContains(&app, "mode set to full access");
app.clearTranscript(); try Commands(FakeApp).handlePermissions(&app, "ask"); try std.testing.expectEqual(types.PermissionMode.ask, app.permission_engine.mode); try std.testing.expectEqual(types.PermissionMode.ask, app.worker.synced_mode.?); try std.testing.expectEqual(@as(usize, 3), app.permission_mode_preference_commit_count); try std.testing.expectEqual(@as(?types.PermissionMode, .ask), app.last_preference_permission_mode); try expectTranscriptContains(&app, "mode set to ask");
app.clearTranscript(); try Commands(FakeApp).handlePermissions(&app, "reset"); try std.testing.expectEqual(types.PermissionMode.ask, app.permission_engine.mode); try std.testing.expectEqual(@as(usize, 0), app.permission_engine.grants.items.len); try std.testing.expectEqual(types.PermissionMode.ask, app.worker.synced_state_mode.?); try std.testing.expectEqual(@as(usize, 4), app.permission_mode_preference_commit_count); try std.testing.expectEqual(@as(?types.PermissionMode, .ask), app.last_preference_permission_mode); try expectTranscriptContains(&app, "permissions reset to ask, session grants cleared");}
test "session_commands argless permissions writes one read-only status notice with switching usage" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "anthropic/claude-opus-4.6"); defer app.deinit(); app.permission_engine.mode = .auto; try app.permission_engine.allow(alloc, "bash", "git *");
try Commands(FakeApp).handlePermissions(&app, "");
try std.testing.expectEqual(@as(usize, 1), app.semantic_write_count); try std.testing.expectEqual(types.NoticeTone.neutral, app.last_tone.?); const status_index = std.mem.find(u8, app.text(), "mode=auto") orelse return error.TestExpectedEqual; const usage = permissions_usage ++ "\n"; const usage_index = std.mem.find(u8, app.text(), usage) orelse return error.TestExpectedEqual; try std.testing.expect(status_index < usage_index); try std.testing.expect(std.mem.endsWith(u8, app.text(), usage)); try std.testing.expectEqual(types.PermissionMode.auto, app.permission_engine.mode); try std.testing.expectEqual(@as(usize, 1), app.permission_engine.grants.items.len); try std.testing.expectEqualStrings("bash", app.permission_engine.grants.items[0].tool_name); try std.testing.expectEqualStrings("git *", app.permission_engine.grants.items[0].target_path); try std.testing.expectEqual(@as(usize, 0), app.permission_mode_preference_commit_count); try std.testing.expectEqual(@as(?types.PermissionMode, null), app.worker.synced_mode); try std.testing.expectEqual(@as(?types.PermissionMode, null), app.worker.synced_state_mode);}
test "session_commands handlePermissions reports usage and invalid action before touching settings" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "anthropic/claude-opus-4.6"); defer app.deinit();
try Commands(FakeApp).handlePermissions(&app, "add"); try expectTranscriptContains(&app, "usage: /permissions [ask|auto|full-access|reset]");
app.clearTranscript(); try Commands(FakeApp).handlePermissions(&app, "remove"); try expectTranscriptContains(&app, "usage: /permissions [ask|auto|full-access|reset]"); try std.testing.expectEqual(@as(usize, 0), app.permission_mode_preference_commit_count);}
test "session_commands handleAllowlist adds lists and removes workspace rules" { const builtin_tools = @import("../../builtins/tools.zig"); var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit();
var app = try FakeApp.init(std.testing.allocator, workspace_root, "anthropic/claude-opus-4.6"); defer app.deinit(); app.tool_registry = .{ .tools = &.{builtin_tools.read_file} };
try Commands(FakeApp).handleAllowlist(&app, "add command \"git *\""); try expectTranscriptContains(&app, "* allowlist: added command: \"git *\"");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "add tool read_file"); try expectTranscriptContains(&app, "* allowlist: added tool read: \"*\"");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "add url \"https://example.com/*\""); try expectTranscriptContains(&app, "* allowlist: added url: \"https://example.com/*\"");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "add command echo"); try expectTranscriptContains(&app, "* allowlist: added command: \"echo\"");
_ = try config_runtime.addPermissionRule(std.testing.allocator, .local, workspace_root, "read", "docs/*", .allow);
var settings = try config_runtime.loadMergedSettings(std.testing.allocator, workspace_root); defer settings.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 5), settings.permission_rules.rules.len); try expectRule(settings.permission_rules.rules[0], "bash", "git *", .allow); try expectRule(settings.permission_rules.rules[1], "bash", "echo", .allow); try expectRule(settings.permission_rules.rules[2], "read", "*", .allow); try expectRule(settings.permission_rules.rules[3], "read", "docs/*", .allow); try expectRule(settings.permission_rules.rules[4], "url", "https://example.com/*", .allow);
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, ""); try expectTranscriptContains(&app, "* allowlist: effective persistent allow rules:"); try expectTranscriptContains(&app, " tools:\n read: workspace, docs/*"); try expectTranscriptContains(&app, " commands:\n git *, echo"); try expectTranscriptContains(&app, " urls:\n https://example.com/*");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "view"); try expectTranscriptContains(&app, "* allowlist: effective persistent allow rules:"); try expectTranscriptContains(&app, " commands:\n git *, echo");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "remove command \"git *\""); try expectTranscriptContains(&app, "* allowlist: removed command: \"git *\"");
var after_remove = try config_runtime.loadMergedSettings(std.testing.allocator, workspace_root); defer after_remove.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 4), after_remove.permission_rules.rules.len); try expectRule(after_remove.permission_rules.rules[0], "bash", "echo", .allow); try expectRule(after_remove.permission_rules.rules[1], "read", "*", .allow); try expectRule(after_remove.permission_rules.rules[2], "read", "docs/*", .allow); try expectRule(after_remove.permission_rules.rules[3], "url", "https://example.com/*", .allow);
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "reset tools"); try expectTranscriptContains(&app, "* allowlist: reset tools: removed 2 rules");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "reset all"); try expectTranscriptContains(&app, "* allowlist: reset all: removed 2 rules");
var after_reset = try config_runtime.loadMergedSettings(std.testing.allocator, workspace_root); defer after_reset.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), after_reset.permission_rules.rules.len);
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "view"); try expectTranscriptContains(&app, "* allowlist: effective persistent allow rules: (none)");}
test "session_commands allowlist scopes expose and mutate hidden user rules independently" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit(); var app = try FakeApp.init(std.testing.allocator, workspace_root, "anthropic/claude-opus-4.6"); defer app.deinit();
try Commands(FakeApp).handleAllowlist(&app, "user add command \"user *\""); try expectTranscriptContains(&app, "(scope=user)"); try std.testing.expect(std.mem.find(u8, app.text(), "shadowed") == null);
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "add command \"local *\""); try expectTranscriptContains(&app, "(scope=local)");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "view user"); try expectTranscriptContains(&app, "* allowlist: user persistent allow rules:"); try expectTranscriptContains(&app, "user *"); try expectTranscriptContains(&app, "user rules are shadowed by local settings");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "view local"); try expectTranscriptContains(&app, "* allowlist: local persistent allow rules:"); try expectTranscriptContains(&app, "local *"); try std.testing.expect(std.mem.find(u8, app.text(), "user *") == null);
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "view effective"); try expectTranscriptContains(&app, "* allowlist: effective persistent allow rules:"); try expectTranscriptContains(&app, "local *"); try std.testing.expect(std.mem.find(u8, app.text(), "user *") == null);
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "user remove command \"user *\""); try expectTranscriptContains(&app, "(scope=user)"); try std.testing.expect(std.mem.find(u8, app.text(), "shadowed") == null);
var after_remove = try config_runtime.loadMergedSettingsDetailed( std.testing.allocator, workspace_root, ); defer after_remove.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), after_remove.permission_sources.user.rules.len); try std.testing.expectEqual(@as(usize, 1), after_remove.permission_sources.local.rules.len);
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "user add command \"user-again *\""); app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "user reset all"); try expectTranscriptContains(&app, "reset all: removed 1 rule (scope=user)"); try std.testing.expect(std.mem.find(u8, app.text(), "shadowed") == null);
var after_reset = try config_runtime.loadMergedSettingsDetailed( std.testing.allocator, workspace_root, ); defer after_reset.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), after_reset.permission_sources.user.rules.len); try std.testing.expectEqual(@as(usize, 1), after_reset.permission_sources.local.rules.len);}
test "session_commands allowlist view reports unsafe settings without returning an error" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home"); try tmp.dir.createDirPath(io_mod.getIo(), "outside"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); tmp.dir.symLink(io_mod.getIo(), "../outside", "home/.fx", .{ .is_directory = true, }) catch |err| switch (err) { error.AccessDenied => return error.SkipZigTest, else => return err, };
const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit();
var app = try FakeApp.init(std.testing.allocator, workspace_root, "anthropic/claude-opus-4.6"); defer app.deinit();
try Commands(FakeApp).handleAllowlist(&app, "view"); try expectTranscriptContains(&app, "Failed to load settings: DurablePathUnsafe");}
test "web_fetch allowlist add remove view and reset persist exact canonical domains" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit();
var app = try FakeApp.init(std.testing.allocator, workspace_root, "anthropic/claude-opus-4.6"); defer app.deinit();
try Commands(FakeApp).handleAllowlist(&app, "add web-fetch-domain Example.COM."); try expectTranscriptContains(&app, "* allowlist: added web-fetch-domain: \"domain:example.com\"");
var settings = try config_runtime.loadMergedSettings(std.testing.allocator, workspace_root); defer settings.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), settings.permission_rules.rules.len); try expectRule(settings.permission_rules.rules[0], "web_fetch", "domain:example.com", .allow);
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "view"); try expectTranscriptContains(&app, " web-fetch domains:\n domain:example.com");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "remove web-fetch-domain EXAMPLE.com"); try expectTranscriptContains(&app, "* allowlist: removed web-fetch-domain: \"domain:example.com\"");
_ = try config_runtime.addPermissionRule(std.testing.allocator, .local, workspace_root, "web_fetch", "domain:example.com", .allow); _ = try config_runtime.addPermissionRule(std.testing.allocator, .local, workspace_root, "web_fetch", "domain:example.org", .allow); app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "reset web-fetch-domains"); try expectTranscriptContains(&app, "* allowlist: reset web-fetch-domains: removed 2 rules");}
test "web_fetch allowlist rejects wildcard url shaped and tool wide authorization" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "anthropic/claude-opus-4.6"); defer app.deinit();
try Commands(FakeApp).handleAllowlist(&app, "add web-fetch-domain *"); try expectTranscriptContains(&app, "usage: /allowlist add [command|tool|url|web-fetch-domain] <pattern>");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "add web-fetch-domain https://example.com"); try expectTranscriptContains(&app, "usage: /allowlist add [command|tool|url|web-fetch-domain] <pattern>");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "add tool web_fetch"); try expectTranscriptContains(&app, "usage: /allowlist add [command|tool|url|web-fetch-domain] <pattern>");}
test "web_fetch malformed hand edited rules render bounded allowlist warnings" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit();
var app = try FakeApp.init(std.testing.allocator, workspace_root, "anthropic/claude-opus-4.6"); defer app.deinit();
_ = try config_runtime.addPermissionRule(std.testing.allocator, .local, workspace_root, "web_fetch", "*", .allow); _ = try config_runtime.addPermissionRule(std.testing.allocator, .local, workspace_root, "read", "*", .allow);
try Commands(FakeApp).handleAllowlist(&app, "view"); try expectTranscriptContains(&app, "ignored 1 malformed web_fetch rule"); try expectTranscriptContains(&app, " tools:\n read: workspace");}
test "session_commands handleAllowlist reports usage for invalid input" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "anthropic/claude-opus-4.6"); defer app.deinit();
try Commands(FakeApp).handleAllowlist(&app, "add"); try expectTranscriptContains(&app, "usage: /allowlist add [command|tool|url|web-fetch-domain] <pattern>");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "remove nope value"); try expectTranscriptContains(&app, "usage: /allowlist remove [command|tool|url|web-fetch-domain] <pattern>");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "add tool not_a_tool"); try expectTranscriptContains(&app, "usage: /allowlist add [command|tool|url|web-fetch-domain] <pattern>");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "remove tool not_a_tool"); try expectTranscriptContains(&app, "usage: /allowlist remove [command|tool|url|web-fetch-domain] <pattern>");
app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "reset"); try expectTranscriptContains(&app, "usage: /allowlist reset [commands|tools|urls|web-fetch-domains|all]");}
test "session_commands allowlist recognizes whole-tool web_search grant" { const builtin_tools = @import("../../builtins/tools.zig"); const tool_registry = tool_dispatch.Registry{ .tools = &.{builtin_tools.web_search} }; const target = parseAllowlistTarget(tool_registry, "tool web_search") orelse return error.TestExpectedEqual;
try std.testing.expectEqual(AllowlistKind.tool, target.kind); try std.testing.expectEqualStrings("web_search", target.category); try std.testing.expectEqualStrings("*", target.pattern); try std.testing.expect(parseAllowlistTarget(tool_registry, "tool web_search current news") == null);}
test "session_commands handleAllowlist recognizes tools from the active registry" { const builtin_tools = @import("../../builtins/tools.zig"); var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit();
const provider_tool = blk: { var tool = builtin_tools.read_file; tool.name = "provider_custom"; break :blk tool; }; var app = try FakeApp.init(std.testing.allocator, workspace_root, "test-model"); defer app.deinit(); app.tool_registry = .{ .tools = &.{provider_tool} };
try Commands(FakeApp).handleAllowlist(&app, "add tool provider_custom"); try expectTranscriptContains(&app, "* allowlist: added tool provider_custom: \"*\"");
var settings = try config_runtime.loadMergedSettings(std.testing.allocator, workspace_root); defer settings.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), settings.permission_rules.rules.len); try expectRule(settings.permission_rules.rules[0], "provider_custom", "*", .allow);
app.tool_registry = .{}; app.clearTranscript(); try Commands(FakeApp).handleAllowlist(&app, "remove tool provider_custom"); try expectTranscriptContains(&app, "usage: /allowlist remove [command|tool|url|web-fetch-domain] <pattern>"); try std.testing.expect(parseAllowlistTarget(.{}, "tool read") != null); try std.testing.expect(parseAllowlistTarget(.{}, "tool memory") == null);}
test "session_commands toggleFast reports unsupported model and redraws footer" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "openai/gpt-4o"); defer app.deinit();
try Commands(FakeApp).toggleFast(&app);
try std.testing.expect(!app.fast_mode); try std.testing.expect(app.shell.render_requests.hasReason(.footer)); try std.testing.expect(app.worker.synced_fast_mode == null); try std.testing.expectEqual(@as(usize, 0), app.worker.fast_sync_count); try expectTranscriptContains(&app, "* fast: This model does not come with a fast mode.");}
test "session_commands toggleFast disables stale fast mode for unsupported model" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit(); var fast_outcome = try config_runtime.setUserPreferences( std.testing.allocator, .{ .fast_mode = true }, ); defer fast_outcome.deinit(std.testing.allocator);
var app = try FakeApp.init(std.testing.allocator, workspace_root, "openai/gpt-4o"); defer app.deinit(); app.fast_mode = true; app.worker.synced_fast_mode = true;
try Commands(FakeApp).toggleFast(&app);
try std.testing.expect(!app.fast_mode); try std.testing.expectEqual(@as(?bool, false), app.worker.synced_fast_mode); try std.testing.expectEqual(@as(usize, 1), app.worker.fast_sync_count);
var settings = try config_runtime.loadMergedSettings(std.testing.allocator, workspace_root); defer settings.deinit(std.testing.allocator); try std.testing.expectEqual(@as(?bool, false), settings.fast_mode);}
test "session_commands toggleFast syncs queued fast mode for supported models" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit();
var app = try FakeApp.init(std.testing.allocator, workspace_root, "anthropic/claude-opus-4.6"); defer app.deinit(); app.setGatewayControls("anthropic/claude-opus-4.6", &.{}, true);
try Commands(FakeApp).toggleFast(&app);
try std.testing.expect(app.fast_mode); try std.testing.expectEqual(@as(?bool, true), app.worker.synced_fast_mode); try std.testing.expectEqual(@as(usize, 1), app.worker.fast_sync_count); try expectTranscriptContains(&app, "* fast: on");}
test "session_commands selectModelFromPicker skips effort changes for models without reasoning support" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "anthropic/claude-opus-4.6"); defer app.deinit(); app.effort = types.ReasoningEffort.literal("high");
try Commands(FakeApp).selectModelFromPicker(&app, "openai/gpt-4o", types.ReasoningEffort.literal("low"), true);
try std.testing.expectEqualStrings("openai/gpt-4o", app.selected_model.items); try std.testing.expectEqualStrings("openai/gpt-4o", app.worker.synced_model.?); try std.testing.expectEqual(types.ReasoningEffort.literal("high"), app.effort); try std.testing.expect(!app.fast_mode);}
test "session_commands model picker accepts the current selected model slice" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "anthropic/claude-opus-4.6"); defer app.deinit(); const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("high")}; app.setGatewayControls("anthropic/claude-opus-4.6", &efforts, true);
try Commands(FakeApp).selectModelFromPicker( &app, app.selected_model.items, types.ReasoningEffort.literal("high"), false, );
try std.testing.expectEqualStrings("anthropic/claude-opus-4.6", app.selected_model.items); try std.testing.expectEqualStrings("anthropic/claude-opus-4.6", app.worker.synced_model.?); try std.testing.expectEqualStrings("anthropic/claude-opus-4.6", app.last_preference_model.items); try std.testing.expectEqualStrings( "fx v" ++ build_options.app_version ++ " | workspace", app.terminalTitleLabelText(), ); try std.testing.expectEqual(types.ReasoningEffort.literal("high"), app.effort);}
test "session_commands selectModelFromPicker persists portable Gateway reasoning effort" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "anthropic/claude-opus-4.6"); defer app.deinit(); app.effort = types.ReasoningEffort.literal("high"); const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("low")}; app.setGatewayControls("provider/new-reasoning-model", &efforts, false);
try Commands(FakeApp).selectModelFromPicker(&app, "provider/new-reasoning-model", types.ReasoningEffort.literal("low"), true);
try std.testing.expectEqualStrings("provider/new-reasoning-model", app.selected_model.items); try std.testing.expectEqualStrings("provider/new-reasoning-model", app.worker.synced_model.?); try std.testing.expectEqual(types.ReasoningEffort.literal("low"), app.effort); try std.testing.expect(!app.fast_mode); try std.testing.expectEqual(@as(?types.ReasoningEffort, types.ReasoningEffort.literal("low")), app.worker.synced_effort); try std.testing.expectEqual(@as(usize, 1), app.worker.effort_sync_count); try std.testing.expectEqual(types.ReasoningEffort.literal("low"), app.last_preference_effort.?); try std.testing.expectEqual(false, app.last_preference_fast_mode.?);}
test "session_commands model selection clears fast mode when the selected model has no fast control" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "anthropic/claude-opus-4.6"); defer app.deinit(); app.fast_mode = true; app.worker.synced_fast_mode = true; const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("max")}; app.setGatewayControls("zai/glm-5.3", &efforts, false);
try Commands(FakeApp).selectModelFromPicker( &app, "zai/glm-5.3", types.ReasoningEffort.literal("max"), true, );
try std.testing.expectEqualStrings("zai/glm-5.3", app.selected_model.items); try std.testing.expect(!app.fast_mode); try std.testing.expectEqual(@as(?bool, false), app.worker.synced_fast_mode); try std.testing.expectEqual(false, app.last_preference_fast_mode.?);}
test "session_commands selectModelFromPicker syncs queued fast mode and effort for supported models" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit();
var app = try FakeApp.init(std.testing.allocator, workspace_root, "anthropic/claude-opus-4.6"); defer app.deinit(); const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("high")}; app.setGatewayControls("anthropic/claude-opus-4.6", &efforts, true);
try Commands(FakeApp).selectModelFromPicker(&app, "anthropic/claude-opus-4.6", types.ReasoningEffort.literal("high"), true);
try std.testing.expect(app.fast_mode); try std.testing.expectEqual(types.ReasoningEffort.literal("high"), app.effort); try std.testing.expectEqual(@as(?bool, true), app.worker.synced_fast_mode); try std.testing.expectEqual(@as(usize, 1), app.worker.fast_sync_count); try std.testing.expectEqual(@as(?types.ReasoningEffort, types.ReasoningEffort.literal("high")), app.worker.synced_effort); try std.testing.expectEqual(@as(usize, 1), app.worker.effort_sync_count);}
test "session_commands model picker follows mock catalog controls independent of provider" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "provider/model"); defer app.deinit(); const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("future-tier")}; app.setGatewayControls("provider/model", &efforts, true);
try Commands(FakeApp).selectModelFromPicker(&app, "provider/model", types.ReasoningEffort.literal("future-tier"), true);
try std.testing.expect(app.fast_mode); try std.testing.expectEqualStrings("future-tier", app.effort.label()); try std.testing.expectEqual(@as(?bool, true), app.worker.synced_fast_mode); try std.testing.expectEqualStrings("future-tier", app.worker.synced_effort.?.label());}
test "session_commands model picker emits one combined preference transaction" { const alloc = std.testing.allocator; var app = try FakeApp.init( alloc, "/tmp/workspace", "anthropic/claude-opus-4.7", ); defer app.deinit(); app.selected_provider = .codex; const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("high")}; app.setGatewayControls("anthropic/claude-opus-4.7", &efforts, true);
try Commands(FakeApp).selectModelFromPicker( &app, "anthropic/claude-opus-4.7", types.ReasoningEffort.literal("high"), true, );
try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try std.testing.expectEqual(model_provider.ProviderId.codex, app.last_preference_provider.?); try std.testing.expectEqualStrings( "anthropic/claude-opus-4.7", app.last_preference_model.items, ); try std.testing.expectEqual( types.ReasoningEffort.literal("high"), app.last_preference_effort.?, ); try std.testing.expectEqual(true, app.last_preference_fast_mode.?);}
test "session_commands user save notice uses one post-commit load after legacy cleanup" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root); const fixture = try std.fmt.allocPrint( std.testing.allocator, "{{\"model\":\"user/old\",\"workspaces\":{{\"{s}\":{{\"model\":\"legacy/local\"}}}}}}\n", .{workspace_root}, ); defer std.testing.allocator.free(fixture); try writeFixtureFile(tmp.dir, "home/.fx/settings.json", fixture); try writeFixtureFile(tmp.dir, "workspace/.fx.json", "{\"model\":\"project/model\"}\n");
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit(); var app = try FakeApp.init(std.testing.allocator, workspace_root, "old/runtime"); defer app.deinit(); app.fail_fetch = true;
try Commands(FakeApp).handleModel(&app, "user/new");
try std.testing.expectEqual(@as(usize, 1), app.post_commit_resolution_count); try expectTranscriptContains(&app, "* model: saved to user settings (scope=user)"); try std.testing.expect(std.mem.find(u8, app.text(), "model=project") == null); try expectTranscriptContains(&app, "normalized 1 legacy value across 1 workspace"); try expectTranscriptContains(&app, "settings.json.preference-migration.model.");}
test "session_commands durable user save survives post-commit resolver failure" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root); const fixture = try std.fmt.allocPrint( std.testing.allocator, "{{\"workspaces\":{{\"{s}\":{{\"model\":\"legacy/local\"}}}}}}\n", .{workspace_root}, ); defer std.testing.allocator.free(fixture); try writeFixtureFile(tmp.dir, "home/.fx/settings.json", fixture);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit(); var app = try FakeApp.init(std.testing.allocator, workspace_root, "old/runtime"); defer app.deinit(); app.fail_fetch = true; app.post_commit_resolution_error = error.InjectedPostCommitResolutionFailure;
try Commands(FakeApp).handleModel(&app, "user/new");
try std.testing.expectEqual(@as(usize, 1), app.post_commit_resolution_count); try expectTranscriptContains(&app, "! model: saved to user settings (scope=user)"); try expectTranscriptContains(&app, "next-startup source unknown"); try expectTranscriptContains(&app, "normalized 1 legacy value across 1 workspace"); try expectTranscriptContains(&app, "settings.json.preference-migration.model.");
var settings = try config_runtime.loadMergedSettings( std.testing.allocator, workspace_root, ); defer settings.deinit(std.testing.allocator); try std.testing.expectEqualStrings("user/new", settings.models.get(.gateway).?);}
test "session_commands durable user save survives post-commit resolver diagnostic" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit(); var app = try FakeApp.init(std.testing.allocator, workspace_root, "old/runtime"); defer app.deinit(); app.fail_fetch = true; app.post_commit_resolution_diagnostic = .durable_path_unsafe;
try Commands(FakeApp).handleModel(&app, "user/new");
try std.testing.expectEqual(@as(usize, 1), app.post_commit_resolution_count); try expectTranscriptContains(&app, "! model: saved to user settings (scope=user)"); try expectTranscriptContains(&app, "next-startup source unknown (DurablePathUnsafe)");}
test "session_commands allowlist failures retain explicit scope and error" { const home = try SessionCommandTestHome.install(std.testing.allocator, null); defer home.deinit(); var app = try FakeApp.init(std.testing.allocator, "/tmp/workspace", "model"); defer app.deinit();
try Commands(FakeApp).handleAllowlist( &app, "user add command \"git status *\"", );
try expectTranscriptContains( &app, "✗ allowlist: failed to add rule to settings (scope=user, error=HomeNotSet)", ); try std.testing.expectEqual(types.NoticeTone.@"error", app.last_tone.?);}
test "session_commands allowlist durable save survives post-commit resolver diagnostic" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit(); var app = try FakeApp.init(std.testing.allocator, workspace_root, "model"); defer app.deinit(); app.post_commit_resolution_diagnostic = .durable_path_unsafe;
try Commands(FakeApp).handleAllowlist( &app, "user add command \"git status *\"", );
try expectTranscriptContains(&app, "! allowlist: added command"); try expectTranscriptContains(&app, "(scope=user)"); try expectTranscriptContains( &app, "saved but effective source unknown and runtime reload failed (DurablePathUnsafe)", ); try std.testing.expectEqual(types.NoticeTone.warning, app.last_tone.?);}
test "session_commands runtime-first model keeps runtime state when both durable targets fail" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "old/model"); defer app.deinit(); app.fail_fetch = true; app.preference_settings_error = error.SettingsWriteFailed; app.preference_session_error = error.SessionPersistenceDegraded;
try Commands(FakeApp).handleModel(&app, "new/model");
try std.testing.expectEqualStrings("new/model", app.selected_model.items); try std.testing.expectEqualStrings("new/model", app.worker.synced_model.?); try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try expectTranscriptContains( &app, "✗ model: active for this process but not saved to user settings", ); try expectTranscriptContains( &app, "✗ model: failed to persist current session", );}
test "session_commands report indeterminate settings and session failures independently" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "old/model"); defer app.deinit(); app.fail_fetch = true; app.preference_settings_error = error.SettingsCommitIndeterminate; app.preference_session_error = error.SessionPersistenceDegraded; const recovery_paths = try alloc.alloc([]const u8, 1); recovery_paths[0] = try alloc.dupe(u8, "/tmp/settings.json.preference-migration.model.json"); app.preference_failure_cleanup = .{ .fields_removed = 1, .workspaces_changed = 1, .recovery_paths = recovery_paths, };
try Commands(FakeApp).handleModel(&app, "new/model");
try expectTranscriptContains(&app, "user settings persistence uncertain"); try expectTranscriptContains(&app, "normalized 1 legacy value across 1 workspace"); try expectTranscriptContains(&app, "recovery=/tmp/settings.json.preference-migration.model.json"); try expectTranscriptContains(&app, "✗ model: failed to persist current session");}
test "session_commands no-op model still attempts its durable targets" { const alloc = std.testing.allocator; var app = try FakeApp.init(alloc, "/tmp/workspace", "same/model"); defer app.deinit(); app.fail_fetch = true;
try Commands(FakeApp).handleModel(&app, "same/model");
try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count); try std.testing.expectEqualStrings( "same/model", app.last_preference_model.items, );}
test "session_commands model controls remain catalog validated and clear unsupported fast state" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
const home = try SessionCommandTestHome.install(std.testing.allocator, home_root); defer home.deinit();
var app = try FakeApp.init(std.testing.allocator, workspace_root, "anthropic/claude-opus-4.6"); defer app.deinit(); app.fast_mode = true; app.worker.synced_fast_mode = true;
try Commands(FakeApp).selectModelFromPicker(&app, "openai/gpt-4o", types.ReasoningEffort.literal("low"), false);
try std.testing.expect(!app.fast_mode); try std.testing.expectEqual(@as(usize, 1), app.worker.fast_sync_count); try std.testing.expectEqual(@as(?bool, false), app.worker.synced_fast_mode); const unsupported_options = model_capabilities.resolveProviderOptionsForCapabilities( app.resolvedModelCapabilities(app.selected_model.items), app.effort, app.worker.synced_fast_mode.?, ); try std.testing.expect(unsupported_options.reasoning == null); try std.testing.expect(!unsupported_options.fast);
const efforts = [_]types.ReasoningEffort{types.ReasoningEffort.literal("high")}; app.gateway_metadata_model = "anthropic/claude-opus-4.6"; app.gateway_metadata = .{ .reasoning_efforts = .fromSlice(&efforts), .supports_fast_mode = true, };
try Commands(FakeApp).selectModelFromPicker(&app, "anthropic/claude-opus-4.6", types.ReasoningEffort.literal("high"), true);
try std.testing.expectEqual(@as(?bool, true), app.worker.synced_fast_mode); try std.testing.expectEqual(@as(usize, 2), app.worker.fast_sync_count); const supported_options = model_capabilities.resolveProviderOptionsForCapabilities( app.resolvedModelCapabilities(app.selected_model.items), app.effort, app.worker.synced_fast_mode.?, ); try std.testing.expectEqualStrings("high", supported_options.reasoning.?.label()); try std.testing.expect(supported_options.fast);}
test "session_commands fuzzyModelScore returns high score for exact substring" { const score = fuzzyModelScore("anthropic/claude-sonnet-4-20250514", "claude-sonnet"); try std.testing.expect(score >= 100);}
test "session_commands fuzzyModelScore returns higher score for prefix match" { const prefix = fuzzyModelScore("openai/gpt-4o", "openai/gpt-4o"); const substr = fuzzyModelScore("openai/gpt-4o-mini", "gpt-4o"); try std.testing.expect(prefix > substr);}
test "session_commands fuzzyModelScore returns positive for multi-token match" { const score = fuzzyModelScore("anthropic/claude-sonnet-4-20250514", "claude sonnet"); try std.testing.expect(score >= 80);}
test "session_commands fuzzyModelScore returns positive for subsequence match" { const score = fuzzyModelScore("anthropic/claude-sonnet-4-20250514", "anth son"); try std.testing.expect(score > 0);}
test "session_commands fuzzyModelScore returns zero for no match" { const score = fuzzyModelScore("openai/gpt-4o", "zzzzz"); try std.testing.expectEqual(@as(i32, 0), score);}
test "session_commands fuzzyModelScore returns zero for empty query" { try std.testing.expectEqual(@as(i32, 0), fuzzyModelScore("openai/gpt-4o", ""));}
test "session_commands splitQueryTokens splits on separators" { var buf: [16][]const u8 = undefined; const count = splitQueryTokens("claude-sonnet/fast", &buf); try std.testing.expectEqual(@as(usize, 3), count); try std.testing.expectEqualStrings("claude", buf[0]); try std.testing.expectEqualStrings("sonnet", buf[1]); try std.testing.expectEqualStrings("fast", buf[2]);}
test "session_commands isSplitter recognizes separator characters" { try std.testing.expect(isSplitter(' ')); try std.testing.expect(isSplitter('-')); try std.testing.expect(isSplitter('/')); try std.testing.expect(isSplitter('_')); try std.testing.expect(!isSplitter('a')); try std.testing.expect(!isSplitter('0'));}
test "session_commands startsWithWord matches word boundary" { try std.testing.expect(startsWithWord("add bash gh", "add")); try std.testing.expect(startsWithWord("remove bash gh", "remove")); try std.testing.expect(startsWithWord("add\tbash", "add")); try std.testing.expect(!startsWithWord("addition", "add")); try std.testing.expect(!startsWithWord("ad", "add")); try std.testing.expect(startsWithWord("add", "add")); try std.testing.expect(startsWithWord("Add bash", "add"));}