Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
18 changes: 18 additions & 0 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,21 @@ jobs:
for run in 1 2 3; do
zig build run-bench-ui-activity -Doptimize=ReleaseSafe
done

- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: Run interactive terminal performance gate
working-directory: tests/e2e
env:
FX_TUI_PERFORMANCE: "1"
FX_TUI_PERFORMANCE_REPORT: ${{ runner.temp }}/tui-performance.json
run: bun test tui-performance.test.ts

- name: Upload interactive terminal performance evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: tui-performance
path: ${{ runner.temp }}/tui-performance.json
if-no-files-found: warn
1 change: 1 addition & 0 deletions scripts/pgso/corpus.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"tui-command-permissions.test.ts": "The file contains a sound scenario and cannot be run safely as a whole.",
"tui-direct-write-audit.test.ts": "The suite validates a repository audit tool rather than the candidate runtime.",
"tui-keybindings.test.ts": "The file-wide guard requires a real model credential.",
"tui-performance.test.ts": "The suite is an opt-in percentile benchmark with a live-provider smoke; the Benchmarks workflow owns its deterministic gate.",
"tui-render-lab.test.ts": "The suite validates render-lab infrastructure and owns separate native opt-in scenarios.",
"tui-render-live-stress.test.ts": "The suite requires a real model credential and explicit live stress opt-in.",
"web-fetch-live.test.ts": "The suite requires explicit live-network opt-in.",
Expand Down
1 change: 1 addition & 0 deletions scripts/pgso/tests/test_corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"tui-command-permissions.test.ts",
"tui-direct-write-audit.test.ts",
"tui-keybindings.test.ts",
"tui-performance.test.ts",
"tui-render-lab.test.ts",
"tui-render-live-stress.test.ts",
"web-fetch-live.test.ts",
Expand Down
6 changes: 4 additions & 2 deletions src/acp/server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1411,9 +1411,11 @@ fn handleInitialize(state: *ServerState, alloc: Allocator, msg: *jsonrpc.Message
state.context_enabled = startup.context_enabled;

if (comptime !host_target.is_wasm) {
const loaded_skills = try app_runtime_setup.loadSkills(alloc, state.workspace_root, builtin_skills.root_policy);
var loaded_skills = try app_runtime_setup.loadSkills(alloc, state.workspace_root, builtin_skills.root_policy);
errdefer loaded_skills.deinit(alloc);
skill_runtime.traceDiagnostics("acp_startup", loaded_skills.diagnostics);
state.skills.replaceLoaded(alloc, loaded_skills.dir, loaded_skills.skills, loaded_skills.diagnostics);
try state.skills.replaceLoaded(alloc, loaded_skills.dir, loaded_skills.skills, loaded_skills.diagnostics);
loaded_skills = .{};
}

var catalog_cancel_flag = std.atomic.Value(bool).init(false);
Expand Down
18 changes: 14 additions & 4 deletions src/core/app/app_agent_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -910,7 +910,10 @@ pub fn Runtime(comptime App: type) type {
try appendClaimedContextNotice(app, &preflight_context_notices.writer, notice);
}

var bounded_skills = try app.skills.buildRoutedSystemPromptSection(
var skill_catalog = app.skills.acquireCatalog();
var skill_catalog_owned = true;
defer if (skill_catalog_owned) skill_catalog.deinit();
var bounded_skills = try skill_catalog.buildRoutedSystemPromptSection(
std.heap.c_allocator,
job.prompt,
if (comptime @hasField(App, "context_limits")) app.context_limits else .{},
Expand All @@ -930,12 +933,14 @@ pub fn Runtime(comptime App: type) type {
}
var explicit_skills = try skill_invocation.buildExplicitPromptSection(
std.heap.c_allocator,
.{ .skills = app.skills.items, .diagnostics = app.skills.diagnostics },
.{ .skills = skill_catalog.items, .diagnostics = skill_catalog.diagnostics },
job.prompt,
explicit_bindings,
if (comptime @hasField(App, "context_limits")) app.context_limits else .{},
);
defer explicit_skills.deinit(std.heap.c_allocator);
skill_catalog.deinit();
skill_catalog_owned = false;
if (explicit_skills.notice) |notice| {
try appendClaimedContextNotice(app, &postflight_context_notices.writer, notice);
}
Expand Down Expand Up @@ -1031,20 +1036,25 @@ pub fn Runtime(comptime App: type) type {
) catch
return error.OutOfMemory;
defer child_projection.deinit(alloc);
var bounded_skills = app.skills.buildRoutedSystemPromptSection(
var skill_catalog = app.skills.acquireCatalog();
var skill_catalog_owned = true;
defer if (skill_catalog_owned) skill_catalog.deinit();
var bounded_skills = skill_catalog.buildRoutedSystemPromptSection(
alloc,
message.content,
if (comptime @hasField(App, "context_limits")) app.context_limits else .{},
) catch return error.OutOfMemory;
defer bounded_skills.deinit(alloc);
var explicit_skills = skill_invocation.buildExplicitPromptSection(
alloc,
.{ .skills = app.skills.items, .diagnostics = app.skills.diagnostics },
.{ .skills = skill_catalog.items, .diagnostics = skill_catalog.diagnostics },
message.content,
&.{},
if (comptime @hasField(App, "context_limits")) app.context_limits else .{},
) catch return error.OutOfMemory;
defer explicit_skills.deinit(alloc);
skill_catalog.deinit();
skill_catalog_owned = false;
const prompt_policy = app.promptPolicy();
const tool_context = childToolContext(app.subagentToolContextForAdmission(admission));
const providers = if (comptime @hasDecl(App, "providerSet"))
Expand Down
116 changes: 109 additions & 7 deletions src/core/app/app_auth_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,21 @@ pub fn Runtime(comptime App: type) type {
try beginSignIn(app, false);
return;
}
try app.auth.refreshSourceInventory(app.alloc);
app.auth.openPickerForProvider(app.alloc, provider_runtime.provider(app));
app.shell.render_requests.request(.footer);
switch (app.auth.beginSourceInventoryRefresh(app.alloc, .{
.provider = provider_runtime.provider(app),
})) {
.started => {},
.busy => try writeAuthNotice(app, .{
.topic = "auth",
.tone = .warning,
.body = "Authentication inventory refresh is already in progress.",
}),
.failed => try writeAuthNotice(app, .{
.topic = "auth",
.tone = .@"error",
.body = "Authentication sources could not be checked. The picker remains closed.",
}),
}
}

pub fn runLogoutCommand(app: *App, target: []const u8) !void {
Expand Down Expand Up @@ -143,7 +155,7 @@ pub fn Runtime(comptime App: type) type {
else
.gateway;
const provider_inventory = if (comptime @hasDecl(@TypeOf(app.auth), "pickerView")) inventory: {
try app.auth.refreshSourceInventory(app.alloc);
try app.auth.refreshSourceInventoryForLogout(app.alloc);
break :inventory app.auth.pickerView().available_sources;
} else @as(auth_runtime.SourceSet, .empty);
const logout_provider = auth_transition.decideLogoutProvider(.{
Expand Down Expand Up @@ -223,9 +235,38 @@ pub fn Runtime(comptime App: type) type {
}, true);
return;
}
try app.auth.refreshSourceInventory(app.alloc);
app.auth.openPickerForProvider(app.alloc, provider_runtime.provider(app));
app.shell.render_requests.request(.footer);
switch (app.auth.beginSourceInventoryRefresh(app.alloc, .{
.provider = provider_runtime.provider(app),
})) {
.started => {},
.busy => try writeAuthNotice(app, .{
.topic = "auth",
.tone = .warning,
.body = "Authentication inventory refresh is already in progress.",
}),
.failed => try writeAuthNotice(app, .{
.topic = "auth",
.tone = .@"error",
.body = "Authentication sources could not be checked. The picker remains closed.",
}),
}
}

pub fn collectSourceInventoryFacts(app: *App) !void {
const result = app.auth.takeSourceInventoryRefresh() orelse return;
switch (result) {
.ready => |action| {
app.auth.openPickerForProvider(app.alloc, action.provider);
app.shell.render_requests.request(.footer);
},
.failed => {
try writeAuthNotice(app, .{
.topic = "auth",
.tone = .@"error",
.body = "Authentication sources could not be checked. The picker was not opened with stale data.",
});
},
}
}

fn applyLogoutResult(app: *App, result: login_flow.LogoutResult) !void {
Expand Down Expand Up @@ -1466,6 +1507,8 @@ const TestAuth = struct {
sign_in_code_toggle_succeeds: bool = true,
sign_in_code_submit_count: usize = 0,
sign_in_code_submit_succeeds: bool = true,
inventory_refresh_action: ?auth_runtime.InventoryRefreshAction = null,
inventory_refresh_fails: bool = false,

fn credentialSource(self: *const TestAuth) ?credentials.Source {
return self.active_source;
Expand Down Expand Up @@ -1585,6 +1628,32 @@ const TestAuth = struct {
self.source_inventory_refresh_count += 1;
}

fn refreshSourceInventoryForLogout(self: *TestAuth, _: std.mem.Allocator) !void {
self.source_inventory_refresh_count += 1;
}

fn beginSourceInventoryRefresh(
self: *TestAuth,
_: std.mem.Allocator,
action: auth_runtime.InventoryRefreshAction,
) auth_runtime.InventoryRefreshStart {
if (self.inventory_refresh_action != null) return .busy;
self.source_inventory_refresh_count += 1;
self.inventory_refresh_action = action;
return .started;
}

fn takeSourceInventoryRefresh(
self: *TestAuth,
) ?auth_runtime.InventoryRefreshResult {
const action = self.inventory_refresh_action orelse return null;
self.inventory_refresh_action = null;
return if (self.inventory_refresh_fails)
.{ .failed = action }
else
.{ .ready = action };
}

fn recordCredentialRefreshFailure(self: *TestAuth, source: credentials.Source) void {
self.refresh_failure_source = source;
}
Expand Down Expand Up @@ -1737,10 +1806,43 @@ test "setup hub projects the selected provider into the auth picker" {

try Runtime(TestApp).openSetupHub(&app);

try std.testing.expect(!app.auth.picker_opened);
try Runtime(TestApp).collectSourceInventoryFacts(&app);
try std.testing.expect(app.auth.picker_opened);
try std.testing.expectEqual(model_provider.ProviderId.codex, app.auth.picker_provider);
}

test "login opens only after its asynchronous inventory refresh completes" {
var app: TestApp = .{ .selected_provider = .grok };
defer app.deinit();

try Runtime(TestApp).runLoginCommand(&app);

try std.testing.expectEqual(@as(usize, 1), app.auth.source_inventory_refresh_count);
try std.testing.expect(!app.auth.picker_opened);
try Runtime(TestApp).collectSourceInventoryFacts(&app);
try std.testing.expect(app.auth.picker_opened);
try std.testing.expectEqual(model_provider.ProviderId.grok, app.auth.picker_provider);
try std.testing.expect(app.shell.render_requests.footer_requested);
}

test "login inventory failure leaves the picker closed and reports one error" {
var app: TestApp = .{ .selected_provider = .gateway };
defer app.deinit();
app.auth.inventory_refresh_fails = true;

try Runtime(TestApp).runLoginCommand(&app);
try Runtime(TestApp).collectSourceInventoryFacts(&app);

try std.testing.expect(!app.auth.picker_opened);
try std.testing.expectEqual(@as(usize, 1), app.notice_write_count);
try std.testing.expect(std.mem.find(
u8,
app.transcript.items,
"picker was not opened with stale data",
) != null);
}

test "OAuth app gating accepts native auth or JS-host auth and rejects neither" {
const NativeApp = struct {
pub const host_profile = runtime_profile.native;
Expand Down
16 changes: 9 additions & 7 deletions src/core/app/app_bootstrap_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,14 @@ pub fn Runtime(comptime App: type) type {
startup.stored_key_status,
startup.credential_onboarding_skipped,
);
if (comptime @hasDecl(@TypeOf(app.auth), "refreshChatGptSourceInventory")) {
app.auth.refreshChatGptSourceInventory(app.alloc) catch |err| {
debug_trace.logf("auth", "startup ChatGPT inventory refresh failed err={s}", .{@errorName(err)});
};
} else {
if (comptime @hasDecl(@TypeOf(app.auth), "refreshSourceInventory")) {
app.auth.refreshSourceInventory(app.alloc) catch |err| {
debug_trace.logf("auth", "startup source inventory refresh failed err={s}", .{@errorName(err)});
};
} else if (comptime @hasDecl(@TypeOf(app.auth), "refreshChatGptSourceInventory")) {
app.auth.refreshChatGptSourceInventory(app.alloc) catch |err| {
debug_trace.logf("auth", "startup source inventory refresh failed err={s}", .{@errorName(err)});
};
}
const startup_auth_view = app.auth.view();
if (startup_auth_view.active_source == null and !startup_auth_view.onboarding_skipped) {
Expand Down Expand Up @@ -328,13 +328,15 @@ pub fn Runtime(comptime App: type) type {
app.mcp_runtime = profile_mcp;
}

const loaded = try deps.load_skills(
var loaded = try deps.load_skills(
std.heap.c_allocator,
app.workspace_root,
deps.skill_root_policy,
);
errdefer loaded.deinit(std.heap.c_allocator);
skill_runtime.traceDiagnostics("interactive_startup", loaded.diagnostics);
app.skills.replaceLoaded(std.heap.c_allocator, loaded.dir, loaded.skills, loaded.diagnostics);
try app.skills.replaceLoaded(std.heap.c_allocator, loaded.dir, loaded.skills, loaded.diagnostics);
loaded = .{};

if (app.requested_resume == null) {
const welcome_message = try deps.welcome_message(app.alloc);
Expand Down
Loading
Loading