@gtkx/cli 0.21.0 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -0
- package/bin/gtkx-dev-runner.js +9 -0
- package/dist/builder.d.ts +5 -52
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +24 -57
- package/dist/builder.js.map +1 -1
- package/dist/cli.d.ts +1 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +24 -122
- package/dist/cli.js.map +1 -1
- package/dist/codegen/freshness.d.ts +11 -0
- package/dist/codegen/freshness.d.ts.map +1 -0
- package/dist/codegen/freshness.js +45 -0
- package/dist/codegen/freshness.js.map +1 -0
- package/dist/codegen/report.d.ts +4 -0
- package/dist/codegen/report.d.ts.map +1 -0
- package/dist/codegen/report.js +18 -0
- package/dist/codegen/report.js.map +1 -0
- package/dist/codegen/run-codegen.d.ts +35 -0
- package/dist/codegen/run-codegen.d.ts.map +1 -0
- package/dist/codegen/run-codegen.js +162 -0
- package/dist/codegen/run-codegen.js.map +1 -0
- package/dist/codegen/store-resolver.d.ts +24 -0
- package/dist/codegen/store-resolver.d.ts.map +1 -0
- package/dist/codegen/store-resolver.js +72 -0
- package/dist/codegen/store-resolver.js.map +1 -0
- package/dist/commands/build.d.ts +10 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +34 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/codegen.d.ts +10 -0
- package/dist/commands/codegen.d.ts.map +1 -0
- package/dist/commands/codegen.js +42 -0
- package/dist/commands/codegen.js.map +1 -0
- package/dist/commands/dev.d.ts +6 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +22 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/docs.d.ts +20 -0
- package/dist/commands/docs.d.ts.map +1 -0
- package/dist/commands/docs.js +74 -0
- package/dist/commands/docs.js.map +1 -0
- package/dist/dev/icon-dir.d.ts +3 -0
- package/dist/dev/icon-dir.d.ts.map +1 -0
- package/dist/dev/icon-dir.js +16 -0
- package/dist/dev/icon-dir.js.map +1 -0
- package/dist/dev/refresh-tracker.d.ts +7 -0
- package/dist/dev/refresh-tracker.d.ts.map +1 -0
- package/dist/dev/refresh-tracker.js +19 -0
- package/dist/dev/refresh-tracker.js.map +1 -0
- package/dist/dev/runner-deps.d.ts +4 -0
- package/dist/dev/runner-deps.d.ts.map +1 -0
- package/dist/dev/runner-deps.js +55 -0
- package/dist/dev/runner-deps.js.map +1 -0
- package/dist/dev/runner-main.d.ts +3 -0
- package/dist/dev/runner-main.d.ts.map +1 -0
- package/dist/dev/runner-main.js +29 -0
- package/dist/dev/runner-main.js.map +1 -0
- package/dist/dev/runner.d.ts +24 -0
- package/dist/dev/runner.d.ts.map +1 -0
- package/dist/dev/runner.js +119 -0
- package/dist/dev/runner.js.map +1 -0
- package/dist/dev/schema-dir.d.ts +3 -0
- package/dist/dev/schema-dir.d.ts.map +1 -0
- package/dist/dev/schema-dir.js +12 -0
- package/dist/dev/schema-dir.js.map +1 -0
- package/dist/dev/supervisor.d.ts +18 -0
- package/dist/dev/supervisor.d.ts.map +1 -0
- package/dist/dev/supervisor.js +182 -0
- package/dist/dev/supervisor.js.map +1 -0
- package/dist/dev/vite-dev-server.d.ts +21 -0
- package/dist/dev/vite-dev-server.d.ts.map +1 -0
- package/dist/dev/vite-dev-server.js +13 -0
- package/dist/dev/vite-dev-server.js.map +1 -0
- package/dist/internal/banner.d.ts +4 -0
- package/dist/internal/banner.d.ts.map +1 -0
- package/dist/internal/banner.js +9 -0
- package/dist/internal/banner.js.map +1 -0
- package/dist/internal/data-dir.d.ts +4 -0
- package/dist/internal/data-dir.d.ts.map +1 -0
- package/dist/internal/data-dir.js +52 -0
- package/dist/internal/data-dir.js.map +1 -0
- package/dist/internal/entry-arg.d.ts +20 -0
- package/dist/internal/entry-arg.d.ts.map +1 -0
- package/dist/internal/entry-arg.js +36 -0
- package/dist/internal/entry-arg.js.map +1 -0
- package/dist/internal/errors.d.ts +5 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/errors.js +29 -0
- package/dist/internal/errors.js.map +1 -0
- package/dist/internal/list-files.d.ts +7 -0
- package/dist/internal/list-files.d.ts.map +1 -0
- package/dist/internal/list-files.js +15 -0
- package/dist/internal/list-files.js.map +1 -0
- package/dist/internal/resolve-cli-tool.d.ts +3 -0
- package/dist/internal/resolve-cli-tool.d.ts.map +1 -0
- package/dist/internal/resolve-cli-tool.js +27 -0
- package/dist/internal/resolve-cli-tool.js.map +1 -0
- package/dist/internal/staging-dir.d.ts +4 -0
- package/dist/internal/staging-dir.d.ts.map +1 -0
- package/dist/internal/staging-dir.js +17 -0
- package/dist/internal/staging-dir.js.map +1 -0
- package/dist/internal/xdg-data-dirs.d.ts +3 -0
- package/dist/internal/xdg-data-dirs.d.ts.map +1 -0
- package/dist/internal/xdg-data-dirs.js +10 -0
- package/dist/internal/xdg-data-dirs.js.map +1 -0
- package/dist/mcp/client.d.ts +28 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +182 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/handlers.d.ts +9 -0
- package/dist/mcp/handlers.d.ts.map +1 -0
- package/dist/mcp/handlers.js +147 -0
- package/dist/mcp/handlers.js.map +1 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +34 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/serialize-widget.d.ts +10 -0
- package/dist/mcp/serialize-widget.d.ts.map +1 -0
- package/dist/mcp/serialize-widget.js +23 -0
- package/dist/mcp/serialize-widget.js.map +1 -0
- package/dist/mcp/testing-loader.d.ts +5 -0
- package/dist/mcp/testing-loader.d.ts.map +1 -0
- package/dist/mcp/testing-loader.js +47 -0
- package/dist/mcp/testing-loader.js.map +1 -0
- package/dist/mcp/widget-registry.d.ts +14 -0
- package/dist/mcp/widget-registry.d.ts.map +1 -0
- package/dist/mcp/widget-registry.js +39 -0
- package/dist/mcp/widget-registry.js.map +1 -0
- package/dist/refresh-globals.d.ts +2 -0
- package/dist/refresh-globals.d.ts.map +1 -0
- package/dist/refresh-globals.js +8 -0
- package/dist/refresh-globals.js.map +1 -0
- package/dist/refresh-runtime.d.ts +10 -26
- package/dist/refresh-runtime.d.ts.map +1 -1
- package/dist/refresh-runtime.js +26 -48
- package/dist/refresh-runtime.js.map +1 -1
- package/dist/settings/compile.d.ts +3 -0
- package/dist/settings/compile.d.ts.map +1 -0
- package/dist/settings/compile.js +21 -0
- package/dist/settings/compile.js.map +1 -0
- package/dist/settings/parser.d.ts +24 -0
- package/dist/settings/parser.d.ts.map +1 -0
- package/dist/settings/parser.js +121 -0
- package/dist/settings/parser.js.map +1 -0
- package/dist/settings/render.d.ts +5 -0
- package/dist/settings/render.d.ts.map +1 -0
- package/dist/settings/render.js +122 -0
- package/dist/settings/render.js.map +1 -0
- package/dist/settings/schema.d.ts +13 -0
- package/dist/settings/schema.d.ts.map +1 -0
- package/dist/settings/schema.js +121 -0
- package/dist/settings/schema.js.map +1 -0
- package/dist/vite-plugins/asset-extensions.d.ts +5 -0
- package/dist/vite-plugins/asset-extensions.d.ts.map +1 -0
- package/dist/vite-plugins/asset-extensions.js +28 -0
- package/dist/vite-plugins/asset-extensions.js.map +1 -0
- package/dist/vite-plugins/built-url.d.ts +4 -0
- package/dist/vite-plugins/built-url.d.ts.map +1 -0
- package/dist/vite-plugins/built-url.js +36 -0
- package/dist/vite-plugins/built-url.js.map +1 -0
- package/dist/vite-plugins/css.d.ts +4 -0
- package/dist/vite-plugins/css.d.ts.map +1 -0
- package/dist/vite-plugins/css.js +38 -0
- package/dist/vite-plugins/css.js.map +1 -0
- package/dist/vite-plugins/fast-refresh/refresh-filter.d.ts +8 -0
- package/dist/vite-plugins/fast-refresh/refresh-filter.d.ts.map +1 -0
- package/dist/vite-plugins/fast-refresh/refresh-filter.js +19 -0
- package/dist/vite-plugins/fast-refresh/refresh-filter.js.map +1 -0
- package/dist/vite-plugins/fast-refresh/swc-refresh.d.ts +6 -0
- package/dist/vite-plugins/fast-refresh/swc-refresh.d.ts.map +1 -0
- package/dist/vite-plugins/fast-refresh/swc-refresh.js +70 -0
- package/dist/vite-plugins/fast-refresh/swc-refresh.js.map +1 -0
- package/dist/vite-plugins/icons.d.ts +4 -0
- package/dist/vite-plugins/icons.d.ts.map +1 -0
- package/dist/vite-plugins/icons.js +51 -0
- package/dist/vite-plugins/icons.js.map +1 -0
- package/dist/vite-plugins/index.d.ts +4 -0
- package/dist/vite-plugins/index.d.ts.map +1 -0
- package/dist/vite-plugins/index.js +22 -0
- package/dist/vite-plugins/index.js.map +1 -0
- package/dist/vite-plugins/native.d.ts +4 -0
- package/dist/vite-plugins/native.d.ts.map +1 -0
- package/dist/vite-plugins/native.js +62 -0
- package/dist/vite-plugins/native.js.map +1 -0
- package/dist/vite-plugins/react-compiler.d.ts +5 -0
- package/dist/vite-plugins/react-compiler.d.ts.map +1 -0
- package/dist/vite-plugins/react-compiler.js +62 -0
- package/dist/vite-plugins/react-compiler.js.map +1 -0
- package/dist/vite-plugins/react-dom-prebundle.d.ts +4 -0
- package/dist/vite-plugins/react-dom-prebundle.d.ts.map +1 -0
- package/dist/vite-plugins/react-dom-prebundle.js +12 -0
- package/dist/vite-plugins/react-dom-prebundle.js.map +1 -0
- package/dist/vite-plugins/resource-init-module.d.ts +6 -0
- package/dist/vite-plugins/resource-init-module.d.ts.map +1 -0
- package/dist/vite-plugins/resource-init-module.js +51 -0
- package/dist/vite-plugins/resource-init-module.js.map +1 -0
- package/dist/vite-plugins/resource-shared.d.ts +8 -0
- package/dist/vite-plugins/resource-shared.d.ts.map +1 -0
- package/dist/vite-plugins/resource-shared.js +28 -0
- package/dist/vite-plugins/resource-shared.js.map +1 -0
- package/dist/vite-plugins/resources.d.ts +5 -0
- package/dist/vite-plugins/resources.d.ts.map +1 -0
- package/dist/vite-plugins/resources.js +260 -0
- package/dist/vite-plugins/resources.js.map +1 -0
- package/dist/vite-plugins/settings-worker-env.d.ts +4 -0
- package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -0
- package/dist/vite-plugins/settings-worker-env.js +23 -0
- package/dist/vite-plugins/settings-worker-env.js.map +1 -0
- package/dist/vite-plugins/settings.d.ts +4 -0
- package/dist/vite-plugins/settings.d.ts.map +1 -0
- package/dist/vite-plugins/settings.js +214 -0
- package/dist/vite-plugins/settings.js.map +1 -0
- package/dist/vite-plugins/strip-query.d.ts +3 -0
- package/dist/vite-plugins/strip-query.d.ts.map +1 -0
- package/dist/vite-plugins/strip-query.js +6 -0
- package/dist/vite-plugins/strip-query.js.map +1 -0
- package/dist/vite-plugins/undeclared-library.d.ts +4 -0
- package/dist/vite-plugins/undeclared-library.d.ts.map +1 -0
- package/dist/vite-plugins/undeclared-library.js +50 -0
- package/dist/vite-plugins/undeclared-library.js.map +1 -0
- package/dist/vite-plugins/virtual-module.d.ts +22 -0
- package/dist/vite-plugins/virtual-module.d.ts.map +1 -0
- package/dist/vite-plugins/virtual-module.js +20 -0
- package/dist/vite-plugins/virtual-module.js.map +1 -0
- package/dist/vitest-plugin.d.ts +10 -0
- package/dist/vitest-plugin.d.ts.map +1 -0
- package/dist/vitest-plugin.js +12 -0
- package/dist/vitest-plugin.js.map +1 -0
- package/env.d.ts +91 -8
- package/package.json +30 -38
- package/src/babel-modules.d.ts +11 -0
- package/src/builder.ts +31 -86
- package/src/cli.ts +42 -0
- package/src/codegen/freshness.ts +65 -0
- package/src/codegen/report.ts +28 -0
- package/src/codegen/run-codegen.ts +260 -0
- package/src/codegen/store-resolver.ts +116 -0
- package/src/commands/build.ts +38 -0
- package/src/commands/codegen.ts +49 -0
- package/src/commands/dev.ts +24 -0
- package/src/commands/docs.ts +95 -0
- package/src/dev/icon-dir.ts +21 -0
- package/src/dev/refresh-tracker.ts +25 -0
- package/src/dev/runner-deps.ts +65 -0
- package/src/dev/runner-main.ts +34 -0
- package/src/dev/runner.ts +185 -0
- package/src/dev/schema-dir.ts +16 -0
- package/src/dev/supervisor.ts +276 -0
- package/src/dev/vite-dev-server.ts +36 -0
- package/src/internal/banner.ts +13 -0
- package/src/internal/data-dir.ts +70 -0
- package/src/internal/entry-arg.ts +46 -0
- package/src/internal/errors.ts +36 -0
- package/src/internal/list-files.ts +23 -0
- package/src/internal/resolve-cli-tool.ts +37 -0
- package/src/internal/staging-dir.ts +19 -0
- package/src/internal/xdg-data-dirs.ts +13 -0
- package/src/mcp/client.ts +235 -0
- package/src/mcp/handlers.ts +213 -0
- package/src/mcp/index.ts +46 -0
- package/src/mcp/serialize-widget.ts +41 -0
- package/src/mcp/testing-loader.ts +73 -0
- package/src/mcp/widget-registry.ts +49 -0
- package/src/refresh-globals.ts +10 -0
- package/src/refresh-runtime.ts +30 -55
- package/src/settings/compile.ts +22 -0
- package/src/settings/parser.ts +193 -0
- package/src/settings/render.ts +167 -0
- package/src/settings/schema.ts +169 -0
- package/src/vite-plugins/asset-extensions.ts +29 -0
- package/src/vite-plugins/built-url.ts +50 -0
- package/src/vite-plugins/css.ts +53 -0
- package/src/vite-plugins/fast-refresh/refresh-filter.ts +26 -0
- package/src/vite-plugins/fast-refresh/swc-refresh.ts +92 -0
- package/src/vite-plugins/icons.ts +74 -0
- package/src/vite-plugins/index.ts +25 -0
- package/src/vite-plugins/native.ts +83 -0
- package/src/vite-plugins/react-compiler.ts +84 -0
- package/src/vite-plugins/react-dom-prebundle.ts +17 -0
- package/src/vite-plugins/resource-init-module.ts +57 -0
- package/src/vite-plugins/resource-shared.ts +40 -0
- package/src/vite-plugins/resources.ts +366 -0
- package/src/vite-plugins/settings-worker-env.ts +30 -0
- package/src/vite-plugins/settings.ts +286 -0
- package/src/vite-plugins/strip-query.ts +7 -0
- package/src/vite-plugins/undeclared-library.ts +78 -0
- package/src/vite-plugins/virtual-module.ts +44 -0
- package/src/vitest-plugin.ts +13 -0
- package/dist/create.d.ts +0 -55
- package/dist/create.d.ts.map +0 -1
- package/dist/create.js +0 -236
- package/dist/create.js.map +0 -1
- package/dist/dev-server.d.ts +0 -37
- package/dist/dev-server.d.ts.map +0 -1
- package/dist/dev-server.js +0 -130
- package/dist/dev-server.js.map +0 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -4
- package/dist/index.js.map +0 -1
- package/dist/mcp-client.d.ts +0 -31
- package/dist/mcp-client.d.ts.map +0 -1
- package/dist/mcp-client.js +0 -430
- package/dist/mcp-client.js.map +0 -1
- package/dist/templates.d.ts +0 -9
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js +0 -16
- package/dist/templates.js.map +0 -1
- package/dist/vite-plugin-gtkx-assets.d.ts +0 -20
- package/dist/vite-plugin-gtkx-assets.d.ts.map +0 -1
- package/dist/vite-plugin-gtkx-assets.js +0 -57
- package/dist/vite-plugin-gtkx-assets.js.map +0 -1
- package/dist/vite-plugin-gtkx-built-url.d.ts +0 -18
- package/dist/vite-plugin-gtkx-built-url.d.ts.map +0 -1
- package/dist/vite-plugin-gtkx-built-url.js +0 -43
- package/dist/vite-plugin-gtkx-built-url.js.map +0 -1
- package/dist/vite-plugin-gtkx-gsettings.d.ts +0 -28
- package/dist/vite-plugin-gtkx-gsettings.d.ts.map +0 -1
- package/dist/vite-plugin-gtkx-gsettings.js +0 -147
- package/dist/vite-plugin-gtkx-gsettings.js.map +0 -1
- package/dist/vite-plugin-gtkx-native.d.ts +0 -14
- package/dist/vite-plugin-gtkx-native.d.ts.map +0 -1
- package/dist/vite-plugin-gtkx-native.js +0 -53
- package/dist/vite-plugin-gtkx-native.js.map +0 -1
- package/dist/vite-plugin-gtkx-refresh.d.ts +0 -8
- package/dist/vite-plugin-gtkx-refresh.d.ts.map +0 -1
- package/dist/vite-plugin-gtkx-refresh.js +0 -37
- package/dist/vite-plugin-gtkx-refresh.js.map +0 -1
- package/dist/vite-plugin-swc-ssr-refresh.d.ts +0 -8
- package/dist/vite-plugin-swc-ssr-refresh.d.ts.map +0 -1
- package/dist/vite-plugin-swc-ssr-refresh.js +0 -46
- package/dist/vite-plugin-swc-ssr-refresh.js.map +0 -1
- package/src/cli.tsx +0 -154
- package/src/create.ts +0 -326
- package/src/dev-server.tsx +0 -162
- package/src/index.ts +0 -3
- package/src/mcp-client.ts +0 -518
- package/src/templates.ts +0 -26
- package/src/vite-plugin-gtkx-assets.ts +0 -69
- package/src/vite-plugin-gtkx-built-url.ts +0 -48
- package/src/vite-plugin-gtkx-gsettings.ts +0 -168
- package/src/vite-plugin-gtkx-native.ts +0 -64
- package/src/vite-plugin-gtkx-refresh.ts +0 -54
- package/src/vite-plugin-swc-ssr-refresh.ts +0 -61
- package/templates/claude/EXAMPLES.md.ejs +0 -744
- package/templates/claude/SKILL.md.ejs +0 -93
- package/templates/claude/WIDGETS.md.ejs +0 -820
- package/templates/config/vitest.config.ts.ejs +0 -11
- package/templates/gitignore.ejs +0 -4
- package/templates/package.json.ejs +0 -16
- package/templates/src/app.tsx.ejs +0 -32
- package/templates/src/dev.tsx.ejs +0 -5
- package/templates/src/gtkx-env.d.ts.ejs +0 -1
- package/templates/src/index.tsx.ejs +0 -5
- package/templates/tests/app.test.tsx.ejs +0 -16
- package/templates/tsconfig.json.ejs +0 -14
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import * as Gio from "@gtkx/gi/gio";
|
|
2
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
3
|
+
import { DEFAULT_SOCKET_PATH, ErrorCode, ProtocolConnection, ProtocolError, type Request } from "@gtkx/mcp/internal";
|
|
4
|
+
import { error, errorMessage, info, normalizeError, warn } from "@gtkx/utils";
|
|
5
|
+
import * as net from "node:net";
|
|
6
|
+
import { dispatch } from "./handlers.js";
|
|
7
|
+
import { WidgetRegistry } from "./widget-registry.js";
|
|
8
|
+
|
|
9
|
+
type McpClientOptions = {
|
|
10
|
+
socketPath?: string;
|
|
11
|
+
applicationId: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ConnectCallbacks = {
|
|
15
|
+
onSuccess?: (() => void) | undefined;
|
|
16
|
+
onError?: ((error: Error) => void) | undefined;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ConnectSettler = {
|
|
20
|
+
succeed: () => void;
|
|
21
|
+
fail: (error: Error) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const RECONNECT_DELAY_MS = 2000;
|
|
25
|
+
const REGISTER_TIMEOUT_MS = 30_000;
|
|
26
|
+
const DISCONNECT_ERROR_CODES: Set<string> = new Set(["ENOENT", "ECONNREFUSED", "EPIPE", "ECONNRESET"]);
|
|
27
|
+
|
|
28
|
+
const toResponseError = (error: unknown): { code: number; message: string; data?: unknown } =>
|
|
29
|
+
error instanceof ProtocolError
|
|
30
|
+
? error.toErrorObject()
|
|
31
|
+
: { code: ErrorCode.INTERNAL_ERROR, message: errorMessage(error) };
|
|
32
|
+
|
|
33
|
+
const connectSettler = (callbacks: ConnectCallbacks): ConnectSettler => {
|
|
34
|
+
let isSettled = false;
|
|
35
|
+
|
|
36
|
+
const settle = (notify: () => void): void => {
|
|
37
|
+
if (isSettled) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
isSettled = true;
|
|
42
|
+
notify();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
succeed: () => {
|
|
47
|
+
settle(() => callbacks.onSuccess?.());
|
|
48
|
+
},
|
|
49
|
+
fail: (failure) => {
|
|
50
|
+
settle(() => callbacks.onError?.(failure));
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
class McpClient {
|
|
56
|
+
private socket: net.Socket | null = null;
|
|
57
|
+
private connection: ProtocolConnection | null = null;
|
|
58
|
+
private socketPath: string;
|
|
59
|
+
private applicationId: string;
|
|
60
|
+
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
61
|
+
private hasConnected = false;
|
|
62
|
+
private isStopping = false;
|
|
63
|
+
private pendingConnectReject: ((error: Error) => void) | null = null;
|
|
64
|
+
private registry = new WidgetRegistry();
|
|
65
|
+
|
|
66
|
+
constructor(options: McpClientOptions) {
|
|
67
|
+
this.socketPath = options.socketPath ?? DEFAULT_SOCKET_PATH;
|
|
68
|
+
this.applicationId = options.applicationId;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private handleClose(): void {
|
|
72
|
+
if (this.hasConnected) {
|
|
73
|
+
info("Disconnected from MCP server");
|
|
74
|
+
this.hasConnected = false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.socket = null;
|
|
78
|
+
this.connection = null;
|
|
79
|
+
this.scheduleReconnect();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private handleSocketError(socketError: Error): void {
|
|
83
|
+
const code = (socketError as NodeJS.ErrnoException).code;
|
|
84
|
+
|
|
85
|
+
if (code !== undefined && DISCONNECT_ERROR_CODES.has(code)) {
|
|
86
|
+
this.scheduleReconnect();
|
|
87
|
+
} else {
|
|
88
|
+
error("Socket error:", socketError.message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async registerWithServer(settle: ConnectSettler): Promise<void> {
|
|
93
|
+
try {
|
|
94
|
+
await this.register();
|
|
95
|
+
info("Registered with MCP server");
|
|
96
|
+
settle.succeed();
|
|
97
|
+
} catch (registerError) {
|
|
98
|
+
error("Failed to register with MCP server:", errorMessage(registerError));
|
|
99
|
+
settle.fail(normalizeError(registerError));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private attemptConnect(callbacks: ConnectCallbacks = {}): void {
|
|
104
|
+
const settle = connectSettler(callbacks);
|
|
105
|
+
|
|
106
|
+
const socket = net.createConnection(this.socketPath, () => {
|
|
107
|
+
info(`Connected to MCP server at ${this.socketPath}`);
|
|
108
|
+
this.hasConnected = true;
|
|
109
|
+
void this.registerWithServer(settle);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const connection = ProtocolConnection.fromSocket(socket, {
|
|
113
|
+
onClose: () => {
|
|
114
|
+
this.handleClose();
|
|
115
|
+
},
|
|
116
|
+
onError: (socketError) => {
|
|
117
|
+
this.handleSocketError(socketError);
|
|
118
|
+
settle.fail(socketError);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
connection.on("request", (request) => {
|
|
123
|
+
void this.handleRequest(request);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
connection.on("invalid", ({ error: parseError }) => {
|
|
127
|
+
warn(`Received invalid JSON from MCP server: ${parseError.message}`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
this.socket = socket;
|
|
131
|
+
this.connection = connection;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private scheduleReconnect(): void {
|
|
135
|
+
if (this.reconnectTimer || this.isStopping) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.reconnectTimer = setTimeout(() => {
|
|
140
|
+
this.reconnectTimer = null;
|
|
141
|
+
this.attemptConnect();
|
|
142
|
+
}, RECONNECT_DELAY_MS);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private register(): Promise<unknown> {
|
|
146
|
+
if (!this.connection) {
|
|
147
|
+
return Promise.reject(new Error("Connection not initialized"));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return this.connection.send(
|
|
151
|
+
"app.register",
|
|
152
|
+
{
|
|
153
|
+
applicationId: this.applicationId,
|
|
154
|
+
pid: process.pid,
|
|
155
|
+
projectRoot: process.cwd(),
|
|
156
|
+
},
|
|
157
|
+
REGISTER_TIMEOUT_MS,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private async respondToRequest(request: Request): Promise<void> {
|
|
162
|
+
const { id, method, params } = request;
|
|
163
|
+
const connection = this.connection;
|
|
164
|
+
|
|
165
|
+
if (!connection) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const defaultApp = Gio.Application.getDefault();
|
|
171
|
+
|
|
172
|
+
if (!(defaultApp instanceof Gtk.Application)) {
|
|
173
|
+
throw new TypeError("Application not initialized");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.registry.refresh();
|
|
177
|
+
const result = await dispatch(method, params, { app: defaultApp, registry: this.registry });
|
|
178
|
+
connection.write({ id, result });
|
|
179
|
+
} catch (dispatchError) {
|
|
180
|
+
connection.write({ id, error: toResponseError(dispatchError) });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private async handleRequest(request: Request): Promise<void> {
|
|
185
|
+
try {
|
|
186
|
+
await this.respondToRequest(request);
|
|
187
|
+
} catch (requestError) {
|
|
188
|
+
error("Error handling request:", requestError);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async connect(): Promise<void> {
|
|
193
|
+
return new Promise<void>((resolve, reject) => {
|
|
194
|
+
this.pendingConnectReject = reject;
|
|
195
|
+
|
|
196
|
+
this.attemptConnect({
|
|
197
|
+
onSuccess: () => {
|
|
198
|
+
this.pendingConnectReject = null;
|
|
199
|
+
resolve();
|
|
200
|
+
},
|
|
201
|
+
onError: (connectError) => {
|
|
202
|
+
this.pendingConnectReject = null;
|
|
203
|
+
reject(connectError);
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
disconnect(): void {
|
|
210
|
+
this.isStopping = true;
|
|
211
|
+
|
|
212
|
+
if (this.pendingConnectReject) {
|
|
213
|
+
this.pendingConnectReject(new Error("Client disconnected before connection registered"));
|
|
214
|
+
this.pendingConnectReject = null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (this.reconnectTimer) {
|
|
218
|
+
clearTimeout(this.reconnectTimer);
|
|
219
|
+
this.reconnectTimer = null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
this.connection?.rejectPending(new Error("Client disconnected"));
|
|
223
|
+
|
|
224
|
+
if (this.socket) {
|
|
225
|
+
this.connection?.write({ id: crypto.randomUUID(), method: "app.unregister" });
|
|
226
|
+
this.socket.destroy();
|
|
227
|
+
this.socket = null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.connection = null;
|
|
231
|
+
this.hasConnected = false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export { McpClient, type McpClientOptions };
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import {
|
|
3
|
+
invalidRequestError,
|
|
4
|
+
methodNotFoundError,
|
|
5
|
+
type ParamsSchema,
|
|
6
|
+
type ServerInitiatedMethod,
|
|
7
|
+
type ServerRequestParams,
|
|
8
|
+
ServerRequestParamsSchemas,
|
|
9
|
+
widgetNotFoundError,
|
|
10
|
+
} from "@gtkx/mcp/internal";
|
|
11
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { dirname } from "node:path";
|
|
13
|
+
import type { WidgetRegistry } from "./widget-registry.js";
|
|
14
|
+
import { serializeWidget } from "./serialize-widget.js";
|
|
15
|
+
import { loadTestingModule, type TestingModule } from "./testing-loader.js";
|
|
16
|
+
|
|
17
|
+
type HandlerContext = {
|
|
18
|
+
app: Gtk.Application;
|
|
19
|
+
registry: WidgetRegistry;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ValidatedHandler = (ctx: HandlerContext, params: unknown) => Promise<unknown>;
|
|
23
|
+
type WidgetTarget = { testing: TestingModule; widget: Gtk.Widget };
|
|
24
|
+
|
|
25
|
+
const HANDLERS: Record<ServerInitiatedMethod, ValidatedHandler> = {
|
|
26
|
+
"app.getWindows": validated(ServerRequestParamsSchemas["app.getWindows"], ({ registry }) =>
|
|
27
|
+
Promise.resolve({
|
|
28
|
+
windows: registry.toplevels().map((window) => ({
|
|
29
|
+
id: registry.getOrCreateId(window),
|
|
30
|
+
title: window.getTitle(),
|
|
31
|
+
})),
|
|
32
|
+
})),
|
|
33
|
+
"widget.getTree": validated(ServerRequestParamsSchemas["widget.getTree"], async ({ app, registry }, params) => {
|
|
34
|
+
const testing = await loadTestingModule();
|
|
35
|
+
const container = params.rootId === undefined ? app : requireWidget(registry, params.rootId);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
tree: testing.prettyWidget(container, {
|
|
39
|
+
getId: (w) => registry.getOrCreateId(w),
|
|
40
|
+
highlight: false,
|
|
41
|
+
...((params.maxDepth !== undefined) && { maxDepth: params.maxDepth }),
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
}),
|
|
45
|
+
"widget.query": validated(ServerRequestParamsSchemas["widget.query"], handleQuery),
|
|
46
|
+
"widget.getProps": validated(ServerRequestParamsSchemas["widget.getProps"], async ({ registry }, params) => {
|
|
47
|
+
const { testing, widget } = await widgetTarget(registry, params.widgetId);
|
|
48
|
+
|
|
49
|
+
return serializeWidget(widget, (target) => registry.getOrCreateId(target), testing);
|
|
50
|
+
}),
|
|
51
|
+
"widget.click": validated(ServerRequestParamsSchemas["widget.click"], async ({ registry }, params) => {
|
|
52
|
+
const { testing, widget } = await widgetTarget(registry, params.widgetId);
|
|
53
|
+
await testing.userEvent.click(widget);
|
|
54
|
+
|
|
55
|
+
return { success: true };
|
|
56
|
+
}),
|
|
57
|
+
"widget.type": validated(ServerRequestParamsSchemas["widget.type"], async ({ registry }, params) => {
|
|
58
|
+
const { testing, widget } = await widgetTarget(registry, params.widgetId);
|
|
59
|
+
|
|
60
|
+
if (params.clear) {
|
|
61
|
+
await testing.userEvent.clear(widget);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await testing.userEvent.type(widget, params.text);
|
|
65
|
+
|
|
66
|
+
return { success: true };
|
|
67
|
+
}),
|
|
68
|
+
"widget.fireEvent": validated(ServerRequestParamsSchemas["widget.fireEvent"], async ({ registry }, params) => {
|
|
69
|
+
const { testing, widget } = await widgetTarget(registry, params.widgetId);
|
|
70
|
+
const signalArgs = (params.args ?? []).map((arg) => extractSignalArg(arg));
|
|
71
|
+
await testing.fireEvent(widget, params.signal, ...signalArgs);
|
|
72
|
+
|
|
73
|
+
return { success: true };
|
|
74
|
+
}),
|
|
75
|
+
"widget.screenshot": validated(ServerRequestParamsSchemas["widget.screenshot"], handleScreenshot),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function validated<Params>(
|
|
79
|
+
schema: ParamsSchema<Params>,
|
|
80
|
+
handler: (ctx: HandlerContext, params: Params) => Promise<unknown>,
|
|
81
|
+
): ValidatedHandler {
|
|
82
|
+
return (ctx, params) => {
|
|
83
|
+
const parsed = schema.safeParse(params ?? {});
|
|
84
|
+
|
|
85
|
+
if (!parsed.success) {
|
|
86
|
+
throw invalidRequestError(parsed.error.message);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return handler(ctx, parsed.data);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const widgetTarget = async (registry: WidgetRegistry, widgetId: string | undefined): Promise<WidgetTarget> => ({
|
|
94
|
+
testing: await loadTestingModule(),
|
|
95
|
+
widget: requireWidget(registry, widgetId),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const requireWidget = (registry: WidgetRegistry, widgetId: string | undefined): Gtk.Widget => {
|
|
99
|
+
if (widgetId === undefined) {
|
|
100
|
+
throw widgetNotFoundError("undefined");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const widget = registry.get(widgetId);
|
|
104
|
+
|
|
105
|
+
if (!widget) {
|
|
106
|
+
throw widgetNotFoundError(widgetId);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return widget;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const extractSignalArg = (arg: unknown): unknown => {
|
|
113
|
+
const isTypedArg = typeof arg === "object" && arg !== null && "type" in arg && "value" in arg;
|
|
114
|
+
|
|
115
|
+
return isTypedArg ? (arg as { value: unknown }).value : arg;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const resolveRole = (value: string | number): Gtk.AccessibleRole | undefined => {
|
|
119
|
+
if (typeof value === "number") {
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const resolved = Gtk.AccessibleRole[value.toUpperCase() as keyof typeof Gtk.AccessibleRole];
|
|
124
|
+
|
|
125
|
+
return typeof resolved === "number" ? resolved : undefined;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const matchesOrEmpty = async (find: () => Promise<Gtk.Widget[]>): Promise<Gtk.Widget[]> => {
|
|
129
|
+
try {
|
|
130
|
+
return await find();
|
|
131
|
+
} catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
async function handleQuery(
|
|
137
|
+
{ app, registry }: HandlerContext,
|
|
138
|
+
params: ServerRequestParams<"widget.query">,
|
|
139
|
+
): Promise<unknown> {
|
|
140
|
+
const testing = await loadTestingModule();
|
|
141
|
+
let widgets: Gtk.Widget[] = [];
|
|
142
|
+
|
|
143
|
+
switch (params.by) {
|
|
144
|
+
case "role": {
|
|
145
|
+
const roleValue = resolveRole(params.value);
|
|
146
|
+
|
|
147
|
+
if (roleValue === undefined) {
|
|
148
|
+
throw invalidRequestError(
|
|
149
|
+
`Unknown accessible role "${String(params.value)}"; use the lowercase role shown in the ` +
|
|
150
|
+
"widget tree, e.g. \"button\", \"list\", \"list_item\", or \"checkbox\".",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
widgets = await matchesOrEmpty(() => testing.findAllByRole(app, roleValue, params.options));
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case "text": {
|
|
158
|
+
widgets = await matchesOrEmpty(() => testing.findAllByText(app, String(params.value), params.options));
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case "name": {
|
|
162
|
+
widgets = await matchesOrEmpty(() => testing.findAllByName(app, String(params.value), params.options));
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case "labelText": {
|
|
166
|
+
widgets = await matchesOrEmpty(() => testing.findAllByLabelText(app, String(params.value), params.options));
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { widgets: widgets.map((w) => serializeWidget(w, (widget) => registry.getOrCreateId(widget), testing, 0)) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const defaultScreenshotTarget = (registry: WidgetRegistry): Gtk.Widget => {
|
|
175
|
+
const [toplevel] = registry.toplevels();
|
|
176
|
+
|
|
177
|
+
if (!toplevel) {
|
|
178
|
+
throw new Error("No windows available for screenshot");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return toplevel;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
async function handleScreenshot(
|
|
185
|
+
{ registry }: HandlerContext,
|
|
186
|
+
params: ServerRequestParams<"widget.screenshot">,
|
|
187
|
+
): Promise<unknown> {
|
|
188
|
+
const testing = await loadTestingModule();
|
|
189
|
+
const target = params.windowId ? requireWidget(registry, params.windowId) : defaultScreenshotTarget(registry);
|
|
190
|
+
const result = await testing.screenshot(target);
|
|
191
|
+
|
|
192
|
+
if (params.path) {
|
|
193
|
+
mkdirSync(dirname(params.path), { recursive: true });
|
|
194
|
+
writeFileSync(params.path, Buffer.from(result.data, "base64"));
|
|
195
|
+
|
|
196
|
+
return { data: result.data, mimeType: result.mimeType, savedPath: params.path };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { data: result.data, mimeType: result.mimeType };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const isServerInitiatedMethod = (method: string): method is ServerInitiatedMethod =>
|
|
203
|
+
Object.hasOwn(ServerRequestParamsSchemas, method);
|
|
204
|
+
|
|
205
|
+
const dispatch = async (method: string, params: unknown, ctx: HandlerContext): Promise<unknown> => {
|
|
206
|
+
if (!isServerInitiatedMethod(method)) {
|
|
207
|
+
throw methodNotFoundError(method);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return HANDLERS[method](ctx, params);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export { dispatch, type HandlerContext };
|
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { McpClient } from "./client.js";
|
|
2
|
+
|
|
3
|
+
type McpClientController = {
|
|
4
|
+
start: (applicationId: string) => Promise<McpClient>;
|
|
5
|
+
stop: () => void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const { start: startMcpClient, stop: stopMcpClient } = createMcpClientController();
|
|
9
|
+
|
|
10
|
+
async function connectQuietly(client: McpClient): Promise<boolean> {
|
|
11
|
+
try {
|
|
12
|
+
await client.connect();
|
|
13
|
+
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createMcpClientController(): McpClientController {
|
|
21
|
+
let current: McpClient | null = null;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
start: async (applicationId: string): Promise<McpClient> => {
|
|
25
|
+
if (current) {
|
|
26
|
+
return current;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const client = new McpClient({ applicationId });
|
|
30
|
+
current = client;
|
|
31
|
+
await connectQuietly(client);
|
|
32
|
+
|
|
33
|
+
return client;
|
|
34
|
+
},
|
|
35
|
+
stop: (): void => {
|
|
36
|
+
if (!current) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
current.disconnect();
|
|
41
|
+
current = null;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { startMcpClient, stopMcpClient };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import type { SerializedWidget } from "@gtkx/mcp/internal";
|
|
3
|
+
|
|
4
|
+
type WidgetIdResolver = (widget: Gtk.Widget) => string;
|
|
5
|
+
|
|
6
|
+
type WidgetFormatting = {
|
|
7
|
+
formatRole(role: Gtk.AccessibleRole): string;
|
|
8
|
+
getWidgetNodeText(widget: Gtk.Widget): string | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const serializeWidget = (
|
|
12
|
+
widget: Gtk.Widget,
|
|
13
|
+
resolveId: WidgetIdResolver,
|
|
14
|
+
testing: WidgetFormatting,
|
|
15
|
+
maxDepth = Infinity,
|
|
16
|
+
): SerializedWidget => {
|
|
17
|
+
const children: SerializedWidget[] = [];
|
|
18
|
+
|
|
19
|
+
if (maxDepth > 0) {
|
|
20
|
+
let child = widget.getFirstChild();
|
|
21
|
+
|
|
22
|
+
while (child) {
|
|
23
|
+
children.push(serializeWidget(child, resolveId, testing, maxDepth - 1));
|
|
24
|
+
child = child.getNextSibling();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
id: resolveId(widget),
|
|
30
|
+
type: widget.constructor.name,
|
|
31
|
+
role: testing.formatRole(widget.getAccessibleRole()),
|
|
32
|
+
name: widget.getName() || null,
|
|
33
|
+
text: testing.getWidgetNodeText(widget),
|
|
34
|
+
sensitive: widget.getSensitive(),
|
|
35
|
+
visible: widget.getVisible(),
|
|
36
|
+
cssClasses: widget.getCssClasses(),
|
|
37
|
+
children,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export { serializeWidget, type WidgetFormatting };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
type TestingModule = typeof import("@gtkx/testing");
|
|
2
|
+
type TestingModuleLoader = () => Promise<TestingModule>;
|
|
3
|
+
|
|
4
|
+
type TestingModuleCache = {
|
|
5
|
+
setLoader: (next: TestingModuleLoader | null) => void;
|
|
6
|
+
load: () => Promise<TestingModule>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type TestingModuleState = {
|
|
10
|
+
loader: TestingModuleLoader;
|
|
11
|
+
testingModule: TestingModule | null;
|
|
12
|
+
testingLoadError: Error | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const { setLoader: setTestingModuleLoader, load: loadTestingModule } = createTestingModuleCache();
|
|
16
|
+
|
|
17
|
+
function defaultLoader(): Promise<TestingModule> {
|
|
18
|
+
return import("@gtkx/testing");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function missingTestingPackageError(cause: unknown): Error {
|
|
22
|
+
return new Error(
|
|
23
|
+
"@gtkx/testing is not installed, install it to enable MCP widget interactions: " +
|
|
24
|
+
`pnpm add -D @gtkx/testing (import failed: ${String(cause)})`,
|
|
25
|
+
{ cause },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function replaceLoader(state: TestingModuleState, next: TestingModuleLoader | null): void {
|
|
30
|
+
state.loader = next ?? defaultLoader;
|
|
31
|
+
state.testingModule = null;
|
|
32
|
+
state.testingLoadError = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function importTestingModule(state: TestingModuleState): Promise<TestingModule> {
|
|
36
|
+
try {
|
|
37
|
+
state.testingModule = await state.loader();
|
|
38
|
+
|
|
39
|
+
return state.testingModule;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
state.testingLoadError = missingTestingPackageError(error);
|
|
42
|
+
throw state.testingLoadError;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadCachedTestingModule(state: TestingModuleState): Promise<TestingModule> {
|
|
47
|
+
if (state.testingModule) {
|
|
48
|
+
return Promise.resolve(state.testingModule);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (state.testingLoadError) {
|
|
52
|
+
return Promise.reject(state.testingLoadError);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return importTestingModule(state);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createTestingModuleCache(): TestingModuleCache {
|
|
59
|
+
const state: TestingModuleState = {
|
|
60
|
+
loader: defaultLoader,
|
|
61
|
+
testingModule: null,
|
|
62
|
+
testingLoadError: null,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
setLoader: (next: TestingModuleLoader | null): void => {
|
|
67
|
+
replaceLoader(state, next);
|
|
68
|
+
},
|
|
69
|
+
load: (): Promise<TestingModule> => loadCachedTestingModule(state),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { setTestingModuleLoader, loadTestingModule, type TestingModule, type TestingModuleLoader };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
|
|
3
|
+
class WidgetRegistry {
|
|
4
|
+
private idByWidget: WeakMap<Gtk.Widget, string> = new WeakMap();
|
|
5
|
+
private nextId = 0;
|
|
6
|
+
private widgetById: Map<string, Gtk.Widget> = new Map();
|
|
7
|
+
private toplevelWindows: Gtk.Window[] = [];
|
|
8
|
+
|
|
9
|
+
refresh(): void {
|
|
10
|
+
this.widgetById.clear();
|
|
11
|
+
this.toplevelWindows = Gtk.Window.listToplevels() as Gtk.Window[];
|
|
12
|
+
|
|
13
|
+
for (const window of this.toplevelWindows) {
|
|
14
|
+
this.register(window);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
toplevels(): Gtk.Window[] {
|
|
19
|
+
return this.toplevelWindows;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
register(widget: Gtk.Widget): void {
|
|
23
|
+
const id = this.getOrCreateId(widget);
|
|
24
|
+
this.widgetById.set(id, widget);
|
|
25
|
+
let child = widget.getFirstChild();
|
|
26
|
+
|
|
27
|
+
while (child) {
|
|
28
|
+
this.register(child);
|
|
29
|
+
child = child.getNextSibling();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getOrCreateId(widget: Gtk.Widget): string {
|
|
34
|
+
let id = this.idByWidget.get(widget);
|
|
35
|
+
|
|
36
|
+
if (!id) {
|
|
37
|
+
id = String(this.nextId++);
|
|
38
|
+
this.idByWidget.set(widget, id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return id;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get(id: string): Gtk.Widget | undefined {
|
|
45
|
+
return this.widgetById.get(id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { WidgetRegistry };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import RefreshRuntime from "react-refresh/runtime";
|
|
2
|
+
|
|
3
|
+
const identity = (type: unknown): unknown => type;
|
|
4
|
+
|
|
5
|
+
RefreshRuntime.injectIntoGlobalHook(globalThis);
|
|
6
|
+
|
|
7
|
+
Object.assign(globalThis, {
|
|
8
|
+
$RefreshReg$: (): void => undefined,
|
|
9
|
+
$RefreshSig$: () => identity,
|
|
10
|
+
});
|