Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/core/app/app_bootstrap_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ fn BootstrapDeps(comptime App: type) type {
[]const u8,
types.ReasoningEffort,
bool,
bool,
) anyerror!void;
const InitializePersistenceFn = *const fn (*App, bool) anyerror!void;
const StageRequestedResumeViewFn = *const fn (*App) app_session_runtime.ResumeViewStage;
Expand Down Expand Up @@ -142,6 +143,7 @@ pub fn Runtime(comptime App: type) type {
selected_model: []const u8,
effort: types.ReasoningEffort,
fast_mode: bool,
fast_mode_model_bound: bool,
) !void {
try app_session_runtime.Runtime(App).configureStartupPreferences(
app,
Expand All @@ -151,6 +153,7 @@ pub fn Runtime(comptime App: type) type {
selected_model,
effort,
fast_mode,
fast_mode_model_bound,
);
}

Expand Down Expand Up @@ -276,6 +279,7 @@ pub fn Runtime(comptime App: type) type {
active_model,
startup.effort,
startup.fast_mode,
startup.fast_mode_model_bound,
);
app.permission_engine.mode = startup.permission_mode;
app.permission_engine.replaceRules(app.alloc, startup.takePermissionRules());
Expand Down Expand Up @@ -486,6 +490,7 @@ const TestCapture = struct {
runtime_model_len: usize = 0,
configured_effort: types.ReasoningEffort = .auto,
configured_fast_mode: bool = false,
configured_fast_mode_model_bound: bool = false,
initialize_required: bool = false,
load_skills_workspace: []const u8 = "",
load_skills_workspace_root_count: usize = 0,
Expand Down Expand Up @@ -714,6 +719,7 @@ fn makeStartupState(alloc: Allocator) !app_lifecycle.StartupState {
state.permission_mode = .auto;
state.context_enabled = false;
state.fast_mode = true;
state.fast_mode_model_bound = true;
state.auto_upgrade = false;
state.update_channel = .dev;
state.effort = types.ReasoningEffort.literal("high");
Expand Down Expand Up @@ -791,6 +797,7 @@ fn configureSessionPreferencesForTest(
selected_model: []const u8,
effort: types.ReasoningEffort,
fast_mode: bool,
fast_mode_model_bound: bool,
) !void {
const capture = active_capture.?;
capture.configured_model_len = @min(
Expand All @@ -812,6 +819,7 @@ fn configureSessionPreferencesForTest(
);
capture.configured_effort = effort;
capture.configured_fast_mode = fast_mode;
capture.configured_fast_mode_model_bound = fast_mode_model_bound;
}

fn beginFreshPersistedSessionForTest(app: *TestApp) !void {
Expand Down Expand Up @@ -901,6 +909,7 @@ test "app_bootstrap_runtime transfers startup state and starts a fresh session"
capture.configured_effort,
);
try std.testing.expect(capture.configured_fast_mode);
try std.testing.expect(capture.configured_fast_mode_model_bound);
try std.testing.expectEqual(
update_target.Channel.dev,
app.upgrader.channel(),
Expand Down
8 changes: 4 additions & 4 deletions src/core/app/app_input_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6575,7 +6575,7 @@ test "active stream Enter commits a complete model choice for the next turn" {
try std.testing.expectEqualStrings(model, app.last_preference_model.items);
try std.testing.expectEqual(types.ReasoningEffort.auto, app.effort);
try std.testing.expectEqual(types.ReasoningEffort.auto, app.last_preference_effort.?);
try std.testing.expect(app.last_preference_fast_mode == null);
try std.testing.expectEqual(false, app.last_preference_fast_mode.?);
try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items);
try std.testing.expectEqual(@as(usize, 0), app.submitted_prompt_count);
try std.testing.expectEqualStrings(
Expand Down Expand Up @@ -7455,7 +7455,7 @@ test "app_input_runtime model picker commits a model without options directly" {
try std.testing.expectEqual(@as(usize, 1), app.preference_commit_count);
try std.testing.expectEqualStrings("openai/gpt-4o", app.selected_model.items);
try std.testing.expect(app.last_preference_effort == null);
try std.testing.expect(app.last_preference_fast_mode == null);
try std.testing.expectEqual(false, app.last_preference_fast_mode.?);
try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items);
}

Expand All @@ -7482,7 +7482,7 @@ test "app_input_runtime model picker skips effort stage for reasoning model with
try std.testing.expectEqual(ModelPickerStage.model, app.input_runtime.picker.model_picker_stage);
try std.testing.expect(!app.input_runtime.picker.hasPendingModelPickerSelection());
try std.testing.expect(app.last_preference_effort == null);
try std.testing.expect(app.last_preference_fast_mode == null);
try std.testing.expectEqual(false, app.last_preference_fast_mode.?);
try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items);
}

Expand Down Expand Up @@ -7514,7 +7514,7 @@ test "app_input_runtime model picker exposes opaque Gateway reasoning effort" {
try std.testing.expectEqual(types.ReasoningEffort.literal("future-tier"), app.effort);
try std.testing.expect(!app.fast_mode);
try std.testing.expectEqual(types.ReasoningEffort.literal("future-tier"), app.last_preference_effort.?);
try std.testing.expect(app.last_preference_fast_mode == null);
try std.testing.expectEqual(false, app.last_preference_fast_mode.?);
try std.testing.expectEqualStrings("", app.input_runtime.edit_state.input.items);
}

Expand Down
66 changes: 61 additions & 5 deletions src/core/app/app_lifecycle.zig
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ pub const StartupState = struct {
context_limits: config_runtime.context_limits.Values = .{},
context_enabled: bool = true,
fast_mode: bool = false,
fast_mode_model_bound: bool = false,
fast_mode_source: config_runtime.ConfigSource = .compiled_default,
slash_menu_categories: bool = true,
collapse_tool_calls: bool = false,
Expand Down Expand Up @@ -422,8 +423,16 @@ fn loadStartupStateFromOwnedWorkspace(
state.max_tool_result_bytes = tool_result_limits.resolveMaxToolResultBytes(settings.max_tool_result_bytes, tool_result_limits.default_max_tool_result_bytes);
state.context_limits = config_runtime.resolveContextLimits(settings, &.{});
state.context_enabled = settings.context orelse true;
state.fast_mode = settings.fast_mode orelse
(state.provider == .gateway and state.model_source == .compiled_default);
const fast_mode = resolveStartupFastMode(
state.provider,
state.model_source,
settings.fast_mode,
detailed.sources.fast_mode,
settings.fast_mode_model_bound,
detailed.sources.fast_mode_model_bound,
);
state.fast_mode = fast_mode.enabled;
state.fast_mode_model_bound = fast_mode.model_bound;
state.fast_mode_source = detailed.sources.fast_mode;
state.slash_menu_categories = settings.slash_menu_categories orelse true;
state.collapse_tool_calls = settings.collapse_tool_calls orelse false;
Expand All @@ -445,6 +454,32 @@ fn loadStartupStateFromOwnedWorkspace(
return state;
}

const StartupFastMode = struct {
enabled: bool,
model_bound: bool,
};

fn resolveStartupFastMode(
provider: model_provider.ProviderId,
model_source: config_runtime.ModelSource,
configured_fast_mode: ?bool,
fast_mode_source: config_runtime.ConfigSource,
model_bound: ?bool,
binding_source: config_runtime.ConfigSource,
) StartupFastMode {
if (configured_fast_mode) |enabled| {
return .{
.enabled = enabled,
.model_bound = enabled and
model_bound == true and
model_source == fast_mode_source and
fast_mode_source == binding_source,
};
}
const enabled = provider == .gateway and model_source == .compiled_default;
return .{ .enabled = enabled, .model_bound = enabled };
}

pub fn bootstrapInteractiveApp(cfg: BootstrapConfig) !StartupState {
try cfg.terminal.ensureInteractive();
try cfg.terminal.captureOriginalTermios();
Expand Down Expand Up @@ -2013,14 +2048,16 @@ test "loadStartupState applies core env overrides" {
try std.testing.expectEqual(@as(usize, 37), state.agent_step_limit);
}

test "loadStartupState defaults fast mode on only for the compiled Gateway default and preserves explicit preferences" {
test "loadStartupState defaults fast mode on only for the compiled Gateway default and requires bound explicit preferences" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();

try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx");
try tmp.dir.createDirPath(io_mod.getIo(), "absent");
try tmp.dir.createDirPath(io_mod.getIo(), "configured");
try tmp.dir.createDirPath(io_mod.getIo(), "disabled");
try tmp.dir.createDirPath(io_mod.getIo(), "legacy-fast");
try tmp.dir.createDirPath(io_mod.getIo(), "bound-fast");
try tmp.dir.createDirPath(io_mod.getIo(), "codex");

const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home");
Expand All @@ -2031,13 +2068,17 @@ test "loadStartupState defaults fast mode on only for the compiled Gateway defau
defer std.testing.allocator.free(configured_root);
const disabled_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "disabled");
defer std.testing.allocator.free(disabled_root);
const legacy_fast_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "legacy-fast");
defer std.testing.allocator.free(legacy_fast_root);
const bound_fast_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "bound-fast");
defer std.testing.allocator.free(bound_fast_root);
const codex_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "codex");
defer std.testing.allocator.free(codex_root);

const fixture = try std.fmt.allocPrint(
std.testing.allocator,
"{{\"workspaces\":{{\"{s}\":{{\"model\":\"openai/gpt-5\"}},\"{s}\":{{\"fast_mode\":false}},\"{s}\":{{\"provider\":\"codex\",\"codex_model\":\"gpt-5.4-mini\"}}}}}}\n",
.{ configured_root, disabled_root, codex_root },
"{{\"workspaces\":{{\"{s}\":{{\"model\":\"openai/gpt-5\"}},\"{s}\":{{\"fast_mode\":false}},\"{s}\":{{\"model\":\"zai/glm-5.3\",\"fast_mode\":true}},\"{s}\":{{\"model\":\"provider/fast-toggle\",\"fast_mode\":true,\"fast_mode_model_bound\":true}},\"{s}\":{{\"provider\":\"codex\",\"codex_model\":\"gpt-5.4-mini\"}}}}}}\n",
.{ configured_root, disabled_root, legacy_fast_root, bound_fast_root, codex_root },
);
defer std.testing.allocator.free(fixture);
try writeFixtureFile(tmp.dir, "home/.fx/settings.json", fixture);
Expand All @@ -2050,16 +2091,31 @@ test "loadStartupState defaults fast mode on only for the compiled Gateway defau
try std.testing.expectEqualStrings("zai/glm-5.2", absent.selected_model);
try std.testing.expectEqualStrings("zai/glm-5.2", absent.configured_model);
try std.testing.expect(absent.fast_mode);
try std.testing.expect(absent.fast_mode_model_bound);

var configured = try loadStartupStateForWorkspace(std.testing.allocator, configured_root, "zai/glm-5.2", 25);
defer configured.deinit(std.testing.allocator);
try std.testing.expectEqualStrings("openai/gpt-5", configured.selected_model);
try std.testing.expectEqualStrings("openai/gpt-5", configured.configured_model);
try std.testing.expect(!configured.fast_mode);
try std.testing.expect(!configured.fast_mode_model_bound);

var disabled = try loadStartupStateForWorkspace(std.testing.allocator, disabled_root, "zai/glm-5.2", 25);
defer disabled.deinit(std.testing.allocator);
try std.testing.expect(!disabled.fast_mode);
try std.testing.expect(!disabled.fast_mode_model_bound);

var legacy_fast = try loadStartupStateForWorkspace(std.testing.allocator, legacy_fast_root, "zai/glm-5.2", 25);
defer legacy_fast.deinit(std.testing.allocator);
try std.testing.expectEqualStrings("zai/glm-5.3", legacy_fast.selected_model);
try std.testing.expect(legacy_fast.fast_mode);
try std.testing.expect(!legacy_fast.fast_mode_model_bound);

var bound_fast = try loadStartupStateForWorkspace(std.testing.allocator, bound_fast_root, "zai/glm-5.2", 25);
defer bound_fast.deinit(std.testing.allocator);
try std.testing.expectEqualStrings("provider/fast-toggle", bound_fast.selected_model);
try std.testing.expect(bound_fast.fast_mode);
try std.testing.expect(bound_fast.fast_mode_model_bound);

var codex = try loadStartupStateForWorkspace(std.testing.allocator, codex_root, "zai/glm-5.2", 25);
defer codex.deinit(std.testing.allocator);
Expand Down
94 changes: 81 additions & 13 deletions src/core/app/app_render_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -597,8 +597,7 @@ pub fn Runtime(comptime App: type) type {
const visible_model = pending_model orelse provider_runtime.model(app);
const visible_capabilities = model_capabilities.resolveForApp(App, app, visible_model);
const active_capabilities_pending = pending_model == null and app.isModelCacheLoading();
const model_supports_fast = visible_capabilities.supports_fast_mode or
(active_capabilities_pending and app.fast_mode);
const model_supports_fast = visible_capabilities.supports_fast_mode;
const model_supports_effort = visible_capabilities.reasoning_efforts.len > 0 or
(active_capabilities_pending and !app.effort.isDefault());
const visible_effort = if (pending_model != null and model_supports_effort)
Expand All @@ -607,10 +606,16 @@ pub fn Runtime(comptime App: type) type {
app.effort
else
.auto;
const visible_fast_mode = if (pending_model != null and model_supports_fast)
pendingPickerFastMode(model_query, app.input_runtime.picker.model_picker_fast_index)
const active_fast_mode_model_bound = if (comptime @hasDecl(App, "fastModeModelBound"))
app.fastModeModelBound()
else
app.fast_mode;
true;
const fast_indicator_active = if (pending_model != null)
visible_capabilities.intrinsic_fast or
(model_supports_fast and pendingPickerFastMode(model_query, app.input_runtime.picker.model_picker_fast_index))
else
visible_capabilities.intrinsic_fast or
(app.fast_mode and active_fast_mode_model_bound);

const upgrade_label = app.upgrader.statusLabel(upgrade_status_buf);
const yolo_warning_active =
Expand Down Expand Up @@ -667,8 +672,7 @@ pub fn Runtime(comptime App: type) type {
.selected_subagent_status = null,
.selected_subagent_tool_calls = 0,
.selected_subagent_activity = null,
.fast_mode = visible_fast_mode,
.model_supports_fast = model_supports_fast,
.fast_indicator_active = fast_indicator_active,
.effort = visible_effort,
.model_supports_effort = model_supports_effort,
.ctrl_c_pending = app.input_runtime.gestures.ctrlCExitArmed(),
Expand Down Expand Up @@ -1273,8 +1277,7 @@ pub fn Runtime(comptime App: type) type {
ctx.subagent_view_active = false;
ctx.selected_subagent_label = display_name;
ctx.selected_subagent_status = chat.state;
ctx.fast_mode = false;
ctx.model_supports_fast = capabilities.supports_fast_mode;
ctx.fast_indicator_active = capabilities.intrinsic_fast;
ctx.effort = chat.configuration.effort orelse .auto;
ctx.model_supports_effort = capabilities.reasoning_efforts.len > 0;
ctx.ctrl_c_pending = view.editor.gestures.ctrlCExitArmed();
Expand Down Expand Up @@ -4609,6 +4612,7 @@ const CoordinatorTestApp = struct {
effort: types.ReasoningEffort = .auto,
statusline_context: bool = false,
total_input_tokens: u64 = 0,
intrinsic_fast_model: ?[]const u8 = null,
gateway_metadata_model: ?[]const u8 = null,
gateway_metadata: model_capabilities.GatewayMetadata = .{},
permission_state: app_permission_runtime.State = .{},
Expand Down Expand Up @@ -4658,10 +4662,13 @@ const CoordinatorTestApp = struct {
}

pub fn resolvedModelCapabilities(self: *CoordinatorTestApp, model: []const u8) model_capabilities.Capabilities {
const fallback = model_capabilities.Capabilities{
var fallback = model_capabilities.Capabilities{
.prompt_caching = true,
.context_window = 1_000_000,
};
if (self.intrinsic_fast_model) |intrinsic_model| {
fallback.intrinsic_fast = std.mem.eql(u8, intrinsic_model, model);
}
if (self.gateway_metadata_model) |metadata_model| {
if (std.mem.eql(u8, metadata_model, model)) {
return model_capabilities.mergeCapabilities(
Expand Down Expand Up @@ -4797,8 +4804,7 @@ test "core.app_render_runtime keeps configured controls visible while model capa
ctx.permission_mode,
ctx.queued_count,
null,
ctx.fast_mode,
ctx.model_supports_fast,
ctx.fast_indicator_active,
ctx.effort,
ctx.model_supports_effort,
ctx.statusline,
Expand All @@ -4811,6 +4817,69 @@ test "core.app_render_runtime keeps configured controls visible while model capa
);
}

test "core.app_render_runtime keeps Kimi fast indicator stable across catalog hydration" {
const cases = [_]struct {
model: []const u8,
fast_mode: bool,
intrinsic_fast: bool,
supports_fast_mode: bool,
expected_indicator: bool,
}{
.{ .model = "moonshotai/kimi-k3", .fast_mode = false, .intrinsic_fast = false, .supports_fast_mode = true, .expected_indicator = false },
.{ .model = "moonshotai/kimi-k3", .fast_mode = true, .intrinsic_fast = false, .supports_fast_mode = true, .expected_indicator = true },
.{ .model = "moonshotai/kimi-k3-fast", .fast_mode = false, .intrinsic_fast = true, .supports_fast_mode = false, .expected_indicator = true },
};

for (cases) |case| {
for ([_]bool{ true, false }) |catalog_loading| {
var app = CoordinatorTestApp{
.alloc = std.testing.allocator,
.shell = .{},
.model_cache_loading = catalog_loading,
.fast_mode = case.fast_mode,
.intrinsic_fast_model = if (case.intrinsic_fast) case.model else null,
.gateway_metadata_model = if (catalog_loading) null else case.model,
.gateway_metadata = .{ .supports_fast_mode = case.supports_fast_mode },
};
defer app.deinit();
try app.selected_model.appendSlice(std.testing.allocator, case.model);

var upgrade_status_buf: [64]u8 = undefined;
const queued_cards: QueuedCardProjection = .{};
const ctx = Runtime(CoordinatorTestApp).footerContext(
&app,
&upgrade_status_buf,
0,
&queued_cards,
);

try std.testing.expectEqual(case.expected_indicator, ctx.fast_indicator_active);
}
}
}

test "core.app_render_runtime keeps a bound fast preference stable after catalog hydration" {
var app = CoordinatorTestApp{
.alloc = std.testing.allocator,
.shell = .{},
.fast_mode = true,
.gateway_metadata_model = "anthropic/claude-fable-5",
};
defer app.deinit();
try app.selected_model.appendSlice(std.testing.allocator, "anthropic/claude-fable-5");

var upgrade_status_buf: [64]u8 = undefined;
const queued_cards: QueuedCardProjection = .{};
const ctx = Runtime(CoordinatorTestApp).footerContext(
&app,
&upgrade_status_buf,
0,
&queued_cards,
);

try std.testing.expect(ctx.fast_indicator_active);
}

test "core.app_render_runtime projects only the visible inline completion suffix" {
const alloc = std.testing.allocator;
var app = CoordinatorTestApp{
Expand Down Expand Up @@ -4893,7 +4962,6 @@ test "core.app_render_runtime projects Opus 4.8 one million token context to foo
0,
null,
false,
true,
.auto,
true,
statusline,
Expand Down
Loading
Loading