From ee53decf2407f2817b1ad397121a85a7024fac7b Mon Sep 17 00:00:00 2001 From: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:03:22 -0700 Subject: [PATCH] fix(client): preserve OMP operation capabilities --- packages/client/src/desktop-runtime.ts | 9 +++- packages/client/test/desktop-runtime.test.ts | 57 +++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/packages/client/src/desktop-runtime.ts b/packages/client/src/desktop-runtime.ts index 8bd24e2a..9c095f2f 100644 --- a/packages/client/src/desktop-runtime.ts +++ b/packages/client/src/desktop-runtime.ts @@ -773,7 +773,14 @@ export class DesktopRuntimeController { if (command === "session.list" || command === "host.list") { this.applySessionListResult(targetId, hostValue, result, expectedIdentity); } else if (command === "catalog.get") { - const catalog = decodeCatalog({ v: "omp-app/1", type: "catalog", hostId: hostValue, revision: result.revision, items: result.items }); + const catalog = decodeCatalog({ + v: "omp-app/1", + type: "catalog", + hostId: hostValue, + revision: result.revision, + items: result.items, + ...(result.operations === undefined ? {} : { operations: result.operations }), + }); if (catalog.type !== "catalog") throw new DesktopRuntimeError("protocol", "catalog response decoded as settings"); this.replace({ catalogs: mapValue(new Map(this.current.catalogs).set(hostValue, catalog)) }); } else if (command === "settings.read") { diff --git a/packages/client/test/desktop-runtime.test.ts b/packages/client/test/desktop-runtime.test.ts index 2c909dc0..e45f080b 100644 --- a/packages/client/test/desktop-runtime.test.ts +++ b/packages/client/test/desktop-runtime.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vite-plus/test"; -import { hostId, revision, sessionId, type WelcomeFrame } from "@t4-code/protocol"; +import { hostId, operationId, revision, sessionId, type WelcomeFrame } from "@t4-code/protocol"; import { rendererServerEventFromFrame } from "@t4-code/protocol/desktop-ipc"; import type { BootstrapResult, @@ -340,6 +340,61 @@ describe("desktop runtime projection", () => { }); expect(runtime.getSnapshot().targetHosts.get("local")).toBe("host-a"); }); + it("preserves operation capabilities from catalog responses and live catalog frames", async () => { + const shell = new FakeShell(); + shell.catalogResult = { + revision: "catalog-response", + items: [], + operations: [{ + operationId: "slash.compact", + label: "/compact", + execution: "headless", + supported: true, + }], + }; + const runtime = createDesktopRuntimeController({ shell }); + await runtime.start(); + shell.emitState({ targetId: "local", state: "connected" }); + shell.emitFrame({ targetId: "local", frame: welcome("host-a", [], []) }); + + await runtime.command("local", { + hostId: hostId("host-a"), + command: "catalog.get", + args: {}, + }); + expect(runtime.getSnapshot().catalogs.get("host-a")?.operations).toMatchObject([ + { operationId: "slash.compact", execution: "headless", supported: true }, + ]); + + shell.emitFrame({ + targetId: "local", + frame: { + v: "omp-app/1", + type: "catalog", + hostId: hostId("host-a"), + revision: revision("catalog-live"), + items: [], + operations: [{ + operationId: operationId("slash.plan"), + label: "/plan", + execution: "terminal-only", + supported: false, + disabledReason: { + code: "terminal_only", + message: "This command requires the OMP terminal interface.", + }, + }], + }, + }); + expect(runtime.getSnapshot().catalogs.get("host-a")?.operations).toMatchObject([ + { + operationId: "slash.plan", + execution: "terminal-only", + supported: false, + disabledReason: { code: "terminal_only" }, + }, + ]); + }); it("refreshes inventory on cadence so a session started after bootstrap appears without reconnect", async () => { const timers = new FakeTimerScheduler(); const shell = new FakeShell();