Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Empty file added packages/mtmcanvas/LICENSE
Empty file.
32 changes: 32 additions & 0 deletions packages/mtmcanvas/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# mtmcanvas

Experimental independent DSH Web Canvas plugin extracted from the Canvas domain currently hosted by `mtmharness`.

The package publishes two DSH faces:

- Host half: `lib/index.js`, which mounts the existing file-backed Canvas RPC through DSH's abstract `ctx.fs` service.
- Browser half: `lib/client.cjs`, a `window.__ModuleLoader__.load` bundle that registers the Canvas sidebar action.

This package is the independent Canvas implementation for the dynamic-loading experiment. The published `mtmharness` package no longer statically mounts Canvas; install both packages when the Canvas surface is required.

## Install Into DSH Web

```bash
dsh plugin --profile web add mtmcanvas
dsh --profile web --dump-config

# Restart the Web profile after changing Bundle membership.
```

The package patch inserts the `mtmcanvas` Loader row. The official Web client module table discovers the `dsh.client` declaration and serves the exported `./client` bundle.

The separate `standalone/` client in `mtmharness` is not part of this plugin. It is the cloud multi-user client and does not receive the file-backed Host implementation.

## Development

```bash
pnpm --filter mtmcanvas run check
pnpm --filter mtmcanvas run pack:check
```

The browser artifact uses DSH's React singleton and dynamic module table. It does not create a React root, router, authentication flow, or local filesystem. `ctx.fs` is the Host capability boundary and can be backed by a remote implementation later.
4 changes: 4 additions & 0 deletions packages/mtmcanvas/cordis.patch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Installable profile layer for the experimental mtmcanvas Host/Client plugin.
- insert:
- id: mtmcanvas
name: mtmcanvas
99 changes: 99 additions & 0 deletions packages/mtmcanvas/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"name": "mtmcanvas",
"version": "0.1.0",
"description": "Experimental Canvas DSH Web plugin extracted from mtmharness.",
"type": "module",
"engines": {
"node": ">=22.19.0",
"pnpm": ">=11.7.0"
},
"main": "./lib/index.js",
"types": "./lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./client": {
"types": "./lib/types/client/index.d.ts",
"default": "./lib/client.cjs"
},
"./cordis.patch.yml": "./cordis.patch.yml",
"./package.json": "./package.json"
},
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
},
"client": {
"platform": "web",
"inject": [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-primitives",
"@deepseek-ai/dsh-client-ui-sidebar",
"@deepseek-ai/dsh-client-ui-layout",
"@deepseek-ai/dsh-client-ui-slots"
]
}
},
"files": [
"lib/index.js",
"lib/client.js",
"lib/client.cjs",
"lib/types/**/*.d.ts",
"cordis.patch.yml",
"README.md",
"package.json",
"LICENSE"
],
"scripts": {
"build": "node scripts/build.mjs",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"verify:package": "node scripts/verify-package.mjs",
"check": "pnpm run typecheck && pnpm run test && pnpm run build && pnpm run verify:package",
"pack:check": "node scripts/pack-check.mjs",
"prepack": "pnpm run build && pnpm run verify:package"
},
"license": "MIT",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/codeh007/mtmdsh.git",
"directory": "packages/mtmcanvas"
},
"peerDependencies": {
"@deepseek-ai/cordis": "^4.0.1",
"@deepseek-ai/dsh-client-connection": "^0.1.1-rc.2",
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-layout": "^0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
"@deepseek-ai/dsh-fs": "^0.1.1-rc.2",
"@deepseek-ai/dsh-host-directory-picker": "^0.1.1-rc.2",
"react": ">=18.2.0 <20"
},
"devDependencies": {
"@deepseek-ai/cordis": "4.0.1",
"@deepseek-ai/dsh-client-connection": "0.1.1-rc.2",
"@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-primitives": "0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-sidebar": "0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-layout": "0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-slots": "0.1.1-rc.2",
"@deepseek-ai/dsh-fs": "0.1.1-rc.2",
"@deepseek-ai/dsh-host-directory-picker": "0.1.1-rc.2",
"@types/node": "22.20.1",
"@types/react": "~18.3.1",
"@types/react-dom": "~18.3.0",
"esbuild": "0.28.2",
"react": "18.3.1",
"react-dom": "18.3.1",
"typescript": "6.0.3",
"vitest": "4.1.11"
}
}
64 changes: 64 additions & 0 deletions packages/mtmcanvas/scripts/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env node

import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { build } from "esbuild";

const packageRoot = fileURLToPath(new URL("..", import.meta.url));
const libRoot = resolve(packageRoot, "lib");
const tsc = resolve(packageRoot, "node_modules/.bin/tsc");
const clientTemp = resolve(libRoot, "client.bundle.cjs");
const packageName = "mtmcanvas";

rmSync(libRoot, { recursive: true, force: true });
mkdirSync(libRoot, { recursive: true });
if (!existsSync(tsc)) throw new Error("mtmcanvas build: local TypeScript executable is missing");
execFileSync(tsc, ["--project", resolve(packageRoot, "tsconfig.json")], { cwd: packageRoot, stdio: "inherit" });

await build({
entryPoints: [resolve(packageRoot, "src/index.ts")],
outfile: resolve(libRoot, "index.js"),
bundle: true,
format: "esm",
platform: "node",
packages: "external",
target: "es2022",
logLevel: "info",
});

await build({
entryPoints: [resolve(packageRoot, "src/client/index.ts")],
outfile: clientTemp,
bundle: true,
format: "cjs",
platform: "browser",
target: "es2020",
external: ["react", "react/*", "@deepseek-ai/*"],
legalComments: "none",
logLevel: "info",
});

const clientSource = readFileSync(clientTemp, "utf8");
const indented = clientSource.split("\n").map((line) => " " + line).join("\n");
const artifact = [
"window.__ModuleLoader__.load({",
" id: " + JSON.stringify(packageName) + ",",
" factory: (require) => {",
" var module = { exports: {} };",
" var exports = module.exports;",
indented,
" return module.exports;",
" }",
"});",
"",
].join("\n");
if (!artifact.includes("window.__ModuleLoader__.load") || !artifact.includes("id: \"" + packageName + "\"")) {
throw new Error("mtmcanvas build: generated client artifact does not have the DSH loader contract");
}
writeFileSync(resolve(libRoot, "client.js"), artifact);
writeFileSync(resolve(libRoot, "client.cjs"), artifact);
rmSync(clientTemp, { force: true });

console.log("built mtmcanvas Host and Client artifacts");
18 changes: 18 additions & 0 deletions packages/mtmcanvas/scripts/pack-check.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env node

import { execFileSync } from "node:child_process";
import { mkdtempSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";

const packageRoot = fileURLToPath(new URL("..", import.meta.url));
const destination = mkdtempSync(join(tmpdir(), "mtmcanvas-pack-"));
try {
execFileSync("pnpm", ["pack", "--pack-destination", destination], { cwd: packageRoot, stdio: "inherit" });
const tarball = readdirSync(destination).find((name) => name.endsWith(".tgz"));
if (tarball === undefined) throw new Error("mtmcanvas pack: pnpm did not create a tarball");
execFileSync(process.execPath, [join(packageRoot, "scripts/verify-package.mjs"), join(destination, tarball)], { cwd: packageRoot, stdio: "inherit" });
} finally {
rmSync(destination, { recursive: true, force: true });
}
55 changes: 55 additions & 0 deletions packages/mtmcanvas/scripts/verify-package.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env node

import { execFileSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";

const packageRoot = fileURLToPath(new URL("..", import.meta.url));
const manifest = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8"));
const fail = (message) => { throw new Error("mtmcanvas package: " + message); };

if (manifest.private === true) fail("package must be publishable");
if (manifest.name !== "mtmcanvas") fail("unexpected package name");
if (!/^\d+\.\d+\.\d+$/.test(manifest.version)) fail("version must be stable SemVer");
if (manifest.dsh?.bundle?.patch !== "./cordis.patch.yml") fail("dsh.bundle.patch must point to cordis.patch.yml");
if (manifest.dsh?.client?.platform !== "web") fail("dsh.client.platform must be web");
if (!Array.isArray(manifest.dsh?.client?.inject)) fail("dsh.client.inject must be an array");
if (manifest.exports?.["."]?.default !== "./lib/index.js") fail("exports root must point to lib/index.js");
if (manifest.exports?.["./client"]?.default !== "./lib/client.cjs") fail("exports ./client must point to lib/client.cjs");
for (const path of ["cordis.patch.yml", "lib/index.js", "lib/client.js", "lib/client.cjs", "lib/types/index.d.ts", "lib/types/client/index.d.ts"]) {
if (!existsSync(resolve(packageRoot, path))) fail("missing build output " + path);
}
const patch = readFileSync(resolve(packageRoot, "cordis.patch.yml"), "utf8");
if (!patch.includes("id: mtmcanvas") || !patch.includes("name: mtmcanvas")) fail("profile patch must insert the mtmcanvas Loader row");
const host = readFileSync(resolve(packageRoot, "lib/index.js"), "utf8");
for (const required of ["mtmcanvas", "mtm-canvas: file-backed RPC", "ctx.fs"]) {
if (!host.includes(required)) fail("Host artifact is missing " + required);
}
const client = readFileSync(resolve(packageRoot, "lib/client.js"), "utf8");
for (const required of ["window.__ModuleLoader__.load", 'id: "mtmcanvas"', "sidebar.footer.action"]) {
if (!client.includes(required)) fail("Client artifact is missing " + required);
}

const tarball = process.argv[2];
if (tarball !== undefined) {
const entries = execFileSync("tar", ["-tzf", resolve(tarball)], { encoding: "utf8" })
.split("\n")
.filter((entry) => entry.startsWith("package/") && !entry.endsWith("/"));
for (const entry of [
"package/LICENSE",
"package/README.md",
"package/cordis.patch.yml",
"package/lib/index.js",
"package/lib/client.js",
"package/lib/client.cjs",
"package/lib/types/index.d.ts",
"package/lib/types/client/index.d.ts",
"package/package.json",
]) {
if (!entries.includes(entry)) fail("tarball is missing " + entry);
}
if (entries.some((entry) => entry.includes(".test."))) fail("tarball must not contain test declarations");
}

console.log("verified mtmcanvas@" + manifest.version + (tarball === undefined ? "" : " tarball"));
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { PropsRuntime, InjectFace } from "@deepseek-ai/dsh-client-ui-slots";
import { Button, Modal } from "@deepseek-ai/dsh-client-ui-primitives";
import { CanvasView } from "../features/canvas/client/CanvasView.tsx";
import type { CanvasRuntime, CanvasViewState } from "../features/canvas/client/runtime.ts";
import { CanvasView } from "./CanvasView.tsx";
import type { CanvasRuntime, CanvasViewState } from "./runtime.ts";
import { useState, type ReactElement } from "react";

type MtmCanvasActionInjected = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import type { ClientContext } from "@deepseek-ai/dsh-client-runtime/client";
import type { ConnectionHandle } from "@deepseek-ai/dsh-client-connection/client";
import type {} from "@deepseek-ai/dsh-client-ui-sidebar/client";
import type {} from "@deepseek-ai/dsh-client-ui-layout/client";
import { MtmCanvasAction } from "./MtmCanvasAction.tsx";
import { CanvasRuntime } from "./runtime.ts";
import { MTM_CANVAS_CSS } from "./styles.ts";

/** Mount the browser Canvas runtime and its plugin-owned styles. */
export function apply(ctx: ClientContext): CanvasRuntime {
export const inject = ["slots", "connection"];

export function apply(ctx: ClientContext): void {
const connection = ctx.get("connection") as ConnectionHandle | undefined;
if (connection === undefined) throw new Error("mtm-canvas: DSH connection service is unavailable");
const runtime = new CanvasRuntime(connection.rpc);
Expand All @@ -17,5 +22,10 @@ export function apply(ctx: ClientContext): CanvasRuntime {
document.head.append(style);
return () => { style.remove(); };
}, "mtm-canvas: styles");
return runtime;
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
name: "sidebar.footer.action",
id: "mtmcanvas",
order: 11,
inject: () => ({ actions: runtime, hooks: { canvas: runtime } }),
}, MtmCanvasAction));
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Context } from "@deepseek-ai/cordis";
import type {} from "@deepseek-ai/dsh-client-connection";
import { FsError, FsVersion } from "@deepseek-ai/dsh-fs";
import type {} from "@deepseek-ai/dsh-fs";
import type {} from "@deepseek-ai/dsh-host-directory-picker";
Expand Down Expand Up @@ -73,6 +74,9 @@ async function readDocument(ctx: Context, directory: Awaited<ReturnType<typeof c
return { version: String(after.version), document };
}

export const name = "mtmcanvas";
export const inject = ["connection", "fs", "directoryPicker"];

export function createCanvasRpcHandler(ctx: Context) {
return async (endpoint: string, payload: unknown, signal: AbortSignal): Promise<RpcResult<unknown>> => {
if (endpoint !== "request") return failure(new Error("unknown canvas endpoint"));
Expand Down
19 changes: 19 additions & 0 deletions packages/mtmcanvas/tests/package-contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";

const packageRoot = resolve(import.meta.dirname, "..");

describe("mtmcanvas package contract", () => {
it("declares an installable DSH Bundle and Web Client face", () => {
const manifest = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8")) as {
exports: { ".": { default?: string }; "./client": { default?: string } };
dsh?: { bundle?: { patch?: string }; client?: { platform?: string; inject?: string[] } };
};
expect(manifest.dsh?.bundle?.patch).toBe("./cordis.patch.yml");
expect(manifest.dsh?.client?.platform).toBe("web");
expect(manifest.dsh?.client?.inject).toContain("@deepseek-ai/dsh-client-ui-sidebar");
expect(manifest.exports["."]?.default).toBe("./lib/index.js");
expect(manifest.exports["./client"]?.default).toBe("./lib/client.cjs");
});
});
35 changes: 35 additions & 0 deletions packages/mtmcanvas/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"compilerOptions": {
"declaration": true,
"allowImportingTsExtensions": true,
"declarationMap": false,
"emitDeclarationOnly": true,
"jsx": "react-jsx",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "lib/types",
"rootDir": "src",
"skipLibCheck": true,
"strict": true,
"target": "ES2022",
"types": [
"node"
],
"verbatimModuleSyntax": true
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"src/**/*.test.ts",
"src/**/*.test.tsx",
"tests/**/*.ts",
"tests/**/*.tsx"
]
}
8 changes: 8 additions & 0 deletions packages/mtmcanvas/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
environment: "node",
include: ["src/**/*.test.ts", "src/**/*.test.tsx", "tests/**/*.test.ts", "tests/**/*.test.tsx"],
},
});
Loading
Loading