mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
f3d108a5a1
Adds a UI for proposing and choosing between multiple agent-generated variants on a running app. **MCP tools** (`packages/tool-server/src/tools/variants/`) - `propose_variant` — non-blocking; agent stages a variant for an on-screen element (text/label/identifier/role matcher). - `await_user_selection` — one blocking call; parks until the human submits, then returns the picks + free-form annotations + a global comment. **Preview UI** (`packages/ui`) - Floating cards anchored to live element frames via the existing `describe` tree, with connector lines. - Comment-mode inspector: hover spotlight (blur+dim everything else), right-click any element to pin a comment. - Sticky padded hitbox + trajectory-aware dismissal so focus doesn't drop on cursor jitter. - Footbar [Complete] [Clear], confirm modal for destructive actions, green toast on submit. - Single source of feel constants in `theme.css`. **Electron host** (`packages/preview-window`) - New workspace package. Frameless transparent BrowserWindow at fixed size; the squeeze open/close is a CSS `transform: scaleY` on `<html>`, GPU-composited — no OS-level resize. - Tool-server spawns it on `await_user_selection` park, reuses it across re-awaits, and asks it to animate-close on submit (1s grace so the toast is visible). - Stdio-line IPC (no extra ports). Ephemeral throughout — the renderer hits the tool-server's already-ephemeral `/preview/*` HTTP. --------- Co-authored-by: Filip131311 <contact@filipkaminski.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
95 lines
3.4 KiB
TypeScript
95 lines
3.4 KiB
TypeScript
/**
|
|
* Regression test for the argent-lens feature-flag gate.
|
|
*
|
|
* The gate lives at the HTTP exposure layer (http.ts), keyed off each tool's
|
|
* `featureFlag` field, and is re-evaluated on EVERY request. The earlier
|
|
* implementation sampled the flag ONCE at registry construction (tool-server
|
|
* startup), so `argent enable argent-lens` against an already-running
|
|
* server had no effect until a restart — these tests pin the fixed behavior:
|
|
* the SAME running app reflects the flag the instant it flips, with no restart.
|
|
*/
|
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import request from "supertest";
|
|
import { Registry } from "@argent/registry";
|
|
import { z } from "zod";
|
|
|
|
vi.mock("@argent/configuration-core", () => ({ isFlagEnabled: vi.fn() }));
|
|
import { isFlagEnabled } from "@argent/configuration-core";
|
|
import { createHttpApp } from "../src/http";
|
|
import { createProposeVariantTool } from "../src/tools/variants/propose-variant";
|
|
import { awaitUserSelectionTool } from "../src/tools/variants/await-user-selection";
|
|
|
|
const mockFlag = vi.mocked(isFlagEnabled);
|
|
|
|
function buildApp() {
|
|
const registry = new Registry();
|
|
registry.registerTool({
|
|
id: "gated_tool",
|
|
featureFlag: "argent-lens",
|
|
zodSchema: z.object({}),
|
|
services: () => ({}),
|
|
async execute() {
|
|
return { ran: true };
|
|
},
|
|
});
|
|
registry.registerTool({
|
|
id: "ungated_tool",
|
|
zodSchema: z.object({}),
|
|
services: () => ({}),
|
|
async execute() {
|
|
return { ran: true };
|
|
},
|
|
});
|
|
return createHttpApp(registry).app;
|
|
}
|
|
|
|
async function toolNames(app: ReturnType<typeof buildApp>): Promise<string[]> {
|
|
const res = await request(app).get("/tools");
|
|
return res.body.tools.map((t: { name: string }) => t.name);
|
|
}
|
|
|
|
describe("argent-lens feature-flag gate (dynamic, HTTP layer)", () => {
|
|
// Built ONCE: the gate must be dynamic per-request, not sampled at startup.
|
|
const app = buildApp();
|
|
beforeEach(() => mockFlag.mockReset());
|
|
|
|
it("the real variant tools declare the argent-lens flag", () => {
|
|
expect(createProposeVariantTool(new Registry()).featureFlag).toBe("argent-lens");
|
|
expect(awaitUserSelectionTool.featureFlag).toBe("argent-lens");
|
|
});
|
|
|
|
it("hides a feature-flagged tool from /tools when the flag is off", async () => {
|
|
mockFlag.mockReturnValue(false);
|
|
const names = await toolNames(app);
|
|
expect(names).toContain("ungated_tool");
|
|
expect(names).not.toContain("gated_tool");
|
|
expect(mockFlag).toHaveBeenCalledWith("argent-lens");
|
|
});
|
|
|
|
it("shows it on the SAME running app once the flag flips on — no restart", async () => {
|
|
mockFlag.mockReturnValue(true);
|
|
const names = await toolNames(app);
|
|
expect(names).toContain("gated_tool");
|
|
expect(names).toContain("ungated_tool");
|
|
});
|
|
|
|
it("rejects invoking a gated tool with 404 when the flag is off", async () => {
|
|
mockFlag.mockReturnValue(false);
|
|
const res = await request(app).post("/tools/gated_tool").send({});
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it("allows invoking the gated tool when the flag is on", async () => {
|
|
mockFlag.mockReturnValue(true);
|
|
const res = await request(app).post("/tools/gated_tool").send({});
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.data).toEqual({ ran: true });
|
|
});
|
|
|
|
it("never gates a tool that has no featureFlag", async () => {
|
|
mockFlag.mockReturnValue(false);
|
|
const res = await request(app).post("/tools/ungated_tool").send({});
|
|
expect(res.status).toBe(200);
|
|
});
|
|
});
|