diff --git a/docs-site/src/content/docs/fr/guides/integrations.md b/docs-site/src/content/docs/fr/guides/integrations.md index c7f653c885..d61ea1c2f8 100644 --- a/docs-site/src/content/docs/fr/guides/integrations.md +++ b/docs-site/src/content/docs/fr/guides/integrations.md @@ -145,6 +145,16 @@ ocx integration client history --client hermes ocx integration client restore --op [--confirm-drift] ``` +`--overwrite-conflict` est la forme terminale de **Replace** : + +```bash +ocx integration client enable --client zcode --overwrite-conflict +``` + +Comme `--confirm-drift`, il n'est jamais supposé : sans lui, un conflit reste refusé. +Il ne s'applique qu'à `enable` ; forcer un *disable* sur un conflit supprimerait un bloc +que nous n'avons jamais écrit, donc cette combinaison est rejetée. + Pour MiniMax Code, connectez une fois le fournisseur puis utilisez l’enveloppe qui vérifie la connexion : ```bash diff --git a/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md index 8e5957b4c1..a5125911eb 100644 --- a/docs-site/src/content/docs/guides/integrations.md +++ b/docs-site/src/content/docs/guides/integrations.md @@ -124,6 +124,14 @@ written as whole documents), or whenever our own entries were edited, the switch locks and disable refuses rather than guessing which edits were yours. +That lock is no longer a dead end. A conflicted client shows **Replace** next to its +switch, on both the overview card and the client's own page. It replaces whatever +holds our settings with the block opencodex would write, and it asks first: the +dialog names the file, says what is lost, and points at the snapshot that makes it +undoable. The switch itself stays locked, because the switch cannot know which edits +you meant to keep — only you can say so. Nothing else is relaxed: a file we cannot +parse, or one whose structure we cannot reason about, still refuses. + ## What to expect, honestly **Formatting is generally not preserved.** Applying parses a config and writes it back @@ -175,6 +183,16 @@ ocx integration client history --client hermes ocx integration client restore --op [--confirm-drift] ``` +`--overwrite-conflict` is the terminal form of **Replace**: + +```bash +ocx integration client enable --client zcode --overwrite-conflict +``` + +Like `--confirm-drift`, it is never assumed — without it a conflict is still refused. +It applies only to `enable`; forcing a *disable* over a conflict would delete a block +we never wrote, so that combination is rejected. + For MiniMax Code, connect the provider once and launch through the checked wrapper: ```bash diff --git a/docs-site/src/content/docs/tr/guides/integrations.md b/docs-site/src/content/docs/tr/guides/integrations.md index e0c1df8bb6..3c5a5c126a 100644 --- a/docs-site/src/content/docs/tr/guides/integrations.md +++ b/docs-site/src/content/docs/tr/guides/integrations.md @@ -168,6 +168,16 @@ ocx integration client history --client hermes ocx integration client restore --op [--confirm-drift] ``` +`--overwrite-conflict`, **Replace** eyleminin terminal karsiligidir: + +```bash +ocx integration client enable --client zcode --overwrite-conflict +``` + +`--confirm-drift` gibi asla varsayilmaz: bayrak yazilmadan catisma yine reddedilir. +Yalnizca `enable` icin gecerlidir; bir catismanin uzerine *disable* zorlamak hic +yazmadigimiz bir blogu silecegi icin bu birlesim reddedilir. + MiniMax Code için sağlayıcıyı bir kez bağlayın ve denetimli başlatıcı üzerinden çalıştırın: ```bash diff --git a/docs-site/src/content/docs/zh-tw/guides/integrations.md b/docs-site/src/content/docs/zh-tw/guides/integrations.md index 8a880fef06..1bcfa4214e 100644 --- a/docs-site/src/content/docs/zh-tw/guides/integrations.md +++ b/docs-site/src/content/docs/zh-tw/guides/integrations.md @@ -80,6 +80,15 @@ ocx integration client history --client hermes ocx integration client restore --op [--confirm-drift] ``` +`--overwrite-conflict` 是 **Replace** 的終端形式: + +```bash +ocx integration client enable --client zcode --overwrite-conflict +``` + +和 `--confirm-drift` 一樣,它永遠不會被預設:沒有這個旗標,衝突仍然會被拒絕。 +它只適用於 `enable`;對衝突強制 *disable* 會刪除我們從未寫入的區塊,因此這個組合會被拒絕。 + MiniMax Code 先連接一次 provider,再透過會檢查設定的 launcher 啟動: ```bash diff --git a/gui/tests/integration-marks.test.ts b/gui/tests/integration-marks.test.ts index 2d8c93bd9a..b964bc4ce1 100644 --- a/gui/tests/integration-marks.test.ts +++ b/gui/tests/integration-marks.test.ts @@ -177,3 +177,19 @@ test("the three newest marks are painted the way their artwork requires", () => expect(inksOf(bodyOf("/provider-icons/gajae-code.svg")).size).toBeGreaterThan(1); expect(/]/.test(bodyOf("/provider-icons/minimax.svg"))).toBe(true); }); + +/* + * The stylesheet rule the mobile dialog depends on. + * + * A config path is one long unbroken token and the dialog is 370px wide at a 390px + * viewport, so without an in-word break opportunity the path overflows and the one + * fact the user needs -- WHICH file is about to change -- goes off screen. This is + * a CSS declaration with no type or render coverage in a DOM-less suite, so it is + * asserted as text. + */ +test("the consequence dialog lets a long path break mid-token", () => { + const css = readFileSync(join(import.meta.dir, "..", "src", "styles-integrations.css"), "utf8"); + const rule = css.match(/\.integration-consequence-body code \{[^}]*\}/); + expect(rule).not.toBeNull(); + expect(rule![0]).toMatch(/overflow-wrap:\s*anywhere/); +}); diff --git a/gui/tests/integrations-surfaces.test.tsx b/gui/tests/integrations-surfaces.test.tsx index 45359026d5..d392db27ff 100644 --- a/gui/tests/integrations-surfaces.test.tsx +++ b/gui/tests/integrations-surfaces.test.tsx @@ -356,6 +356,37 @@ test("a foreign edit and an unowned block get different dialog copy", async () = .toContain("A block we did not write"); }); +test("the dialog's config path can break mid-string, so it cannot overflow a phone", async () => { + /* + * The dialog is 370px wide at a 390px viewport and the path it names is a long + * unbroken token -- a real one is `~/.zcode/v2/config.json` and worse. Without a + * break opportunity inside the word that token overflows its own container, + * which is how the one piece of information the user needs (WHICH file) ends up + * off screen. + * + * happy-dom does no layout, so measured geometry is not available here; what is + * checkable is that the path renders inside an element the stylesheet allows to + * break. Rendered geometry was measured separately at 390px in both themes + * (dialog 370px wide at left:10, code element 212px, no overflow). + */ + // A synthetic home, not a real one: privacy:scan rejects a committed /Users//. + const longPath = "/home/dev/Library/Application Support/SomeVendor/deeply/nested/config.json"; + stateResponse = () => json(status({ + state: "conflict", + reason: "unowned-key", + configPath: longPath, + })); + await remountClient(); + await act(async () => { buttonByText("Replace")!.click(); }); + + const dialog = container.querySelector(".integration-consequence-dialog")!; + const code = dialog.querySelector("code"); + // A element, not bare text: `.integration-consequence-body code` is what + // carries `overflow-wrap: anywhere`. + expect(code).not.toBeNull(); + expect(code!.textContent).toBe(longPath); +}); + test("unsafe locks the switch instead of guessing", async () => { stateResponse = () => json(status({ state: "unsafe", reason: "unparseable" })); await mountClient(); diff --git a/src/cli/integrations.ts b/src/cli/integrations.ts index a654bf8cac..3b417632ff 100644 --- a/src/cli/integrations.ts +++ b/src/cli/integrations.ts @@ -29,7 +29,7 @@ const GROK_USAGE = `Usage: const CLIENT_USAGE = `Usage: ocx integration client [status] [--client ] [--json] - ocx integration client --client [--json] + ocx integration client --client [--overwrite-conflict] [--json] ocx integration client history [--client ] [--json] ocx integration client restore --op [--confirm-drift] [--json]`; @@ -212,11 +212,33 @@ export async function handleClientIntegrationCommand( throw new CliUsageError(`unknown client integration command ${action}`, CLIENT_USAGE); } const client = takeOption(args, "--client"); + /* + * The conflict escape hatch, spelled the way `restore --confirm-drift` is: the + * refusal is the default and the waiver has to be typed. + * + * Without it the dashboard could resolve a conflict and the CLI could not, + * which strands exactly the user who cannot open a browser -- an SSH session, + * or an agent driving the proxy. That dead end is the reason the overwrite + * path exists at all. + */ + const overwriteConflict = takeFlag(args, "--overwrite-conflict"); rejectArgs(args, CLIENT_USAGE); if (!client) throw new CliUsageError("--client is required", CLIENT_USAGE); + /* + * Refused here rather than forwarded. The route answers 400 for this pair, but + * a local usage error names the flag that is wrong, where the route's reply + * arrives as a generic failed request. + */ + if (overwriteConflict && action === "disable") { + throw new CliUsageError("--overwrite-conflict applies only to enable", CLIENT_USAGE); + } const result = await runtimeRequest(`/api/client-integrations/${encodeURIComponent(client)}`, { method: "PUT", - body: JSON.stringify({ enabled: action === "enable" }), + // Sent only when asked for, so a proxy on an older build sees the request it + // has always seen rather than an unknown field. + body: JSON.stringify(overwriteConflict + ? { enabled: true, overwriteConflict: true } + : { enabled: action === "enable" }), }, deps); printData(result, wantsJson, [String((result as Record).message ?? `${client} ${action}d.`)]); }); diff --git a/tests/cli-headless-parity.test.ts b/tests/cli-headless-parity.test.ts index 3900af2d7a..dcd05d295b 100644 --- a/tests/cli-headless-parity.test.ts +++ b/tests/cli-headless-parity.test.ts @@ -554,6 +554,41 @@ describe("headless GUI parity CLI", () => { ]); }); + test("enable can waive a conflict, and only when the flag is typed", async () => { + /* + * The parity this closes: the dashboard could resolve a conflict and the CLI + * could not, which strands the user who has no browser -- an SSH session, or + * an agent driving the proxy. That dead end is the reason the overwrite path + * exists, so leaving it GUI-only reproduces it for half the users. + */ + const runtime = fakeRuntime(); + expect(await handleClientIntegrationCommand(["enable", "--client", "hermes", "--json"], runtime.deps)).toBe(0); + expect(await handleClientIntegrationCommand( + ["enable", "--client", "hermes", "--overwrite-conflict", "--json"], + runtime.deps, + )).toBe(0); + expect(runtime.requests.map(row => row.body)).toEqual([ + // Absent rather than false: an older proxy sees the request it always saw. + { enabled: true }, + { enabled: true, overwriteConflict: true }, + ]); + }); + + test("a conflict waiver cannot ride along with disable", async () => { + /* + * Forcing a DISABLE over a conflict deletes a block we do not own, which is + * the one thing the refusal exists to prevent. The route answers 400; failing + * locally names the offending flag instead of surfacing a generic request + * failure, and sends nothing. + */ + const runtime = fakeRuntime(); + expect(await handleClientIntegrationCommand( + ["disable", "--client", "hermes", "--overwrite-conflict", "--json"], + runtime.deps, + )).not.toBe(0); + expect(runtime.requests).toEqual([]); + }); + test("a client integration command without its required target fails instead of guessing", async () => { const runtime = fakeRuntime(); // No `--client`: picking one for the user would write a config they never named.