diff --git a/packages/mtmcanvas/LICENSE b/packages/mtmcanvas/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/packages/mtmcanvas/README.md b/packages/mtmcanvas/README.md new file mode 100644 index 0000000..80fa285 --- /dev/null +++ b/packages/mtmcanvas/README.md @@ -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. diff --git a/packages/mtmcanvas/cordis.patch.yml b/packages/mtmcanvas/cordis.patch.yml new file mode 100644 index 0000000..69b6206 --- /dev/null +++ b/packages/mtmcanvas/cordis.patch.yml @@ -0,0 +1,4 @@ +# Installable profile layer for the experimental mtmcanvas Host/Client plugin. +- insert: + - id: mtmcanvas + name: mtmcanvas diff --git a/packages/mtmcanvas/package.json b/packages/mtmcanvas/package.json new file mode 100644 index 0000000..8e45825 --- /dev/null +++ b/packages/mtmcanvas/package.json @@ -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" + } +} diff --git a/packages/mtmcanvas/scripts/build.mjs b/packages/mtmcanvas/scripts/build.mjs new file mode 100644 index 0000000..a9fdddd --- /dev/null +++ b/packages/mtmcanvas/scripts/build.mjs @@ -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"); diff --git a/packages/mtmcanvas/scripts/pack-check.mjs b/packages/mtmcanvas/scripts/pack-check.mjs new file mode 100644 index 0000000..2142cda --- /dev/null +++ b/packages/mtmcanvas/scripts/pack-check.mjs @@ -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 }); +} diff --git a/packages/mtmcanvas/scripts/verify-package.mjs b/packages/mtmcanvas/scripts/verify-package.mjs new file mode 100644 index 0000000..e5b9a0e --- /dev/null +++ b/packages/mtmcanvas/scripts/verify-package.mjs @@ -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")); diff --git a/packages/mtmharness/src/features/canvas/client/CanvasView.tsx b/packages/mtmcanvas/src/client/CanvasView.tsx similarity index 100% rename from packages/mtmharness/src/features/canvas/client/CanvasView.tsx rename to packages/mtmcanvas/src/client/CanvasView.tsx diff --git a/packages/mtmharness/src/client/MtmCanvasAction.tsx b/packages/mtmcanvas/src/client/MtmCanvasAction.tsx similarity index 89% rename from packages/mtmharness/src/client/MtmCanvasAction.tsx rename to packages/mtmcanvas/src/client/MtmCanvasAction.tsx index ed6257d..2e35bee 100644 --- a/packages/mtmharness/src/client/MtmCanvasAction.tsx +++ b/packages/mtmcanvas/src/client/MtmCanvasAction.tsx @@ -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 = { diff --git a/packages/mtmharness/src/features/canvas/client/index.ts b/packages/mtmcanvas/src/client/index.ts similarity index 64% rename from packages/mtmharness/src/features/canvas/client/index.ts rename to packages/mtmcanvas/src/client/index.ts index 926715e..d8e466e 100644 --- a/packages/mtmharness/src/features/canvas/client/index.ts +++ b/packages/mtmcanvas/src/client/index.ts @@ -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); @@ -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)); } diff --git a/packages/mtmharness/src/features/canvas/client/runtime.test.ts b/packages/mtmcanvas/src/client/runtime.test.ts similarity index 100% rename from packages/mtmharness/src/features/canvas/client/runtime.test.ts rename to packages/mtmcanvas/src/client/runtime.test.ts diff --git a/packages/mtmharness/src/features/canvas/client/runtime.ts b/packages/mtmcanvas/src/client/runtime.ts similarity index 100% rename from packages/mtmharness/src/features/canvas/client/runtime.ts rename to packages/mtmcanvas/src/client/runtime.ts diff --git a/packages/mtmharness/src/features/canvas/client/styles.ts b/packages/mtmcanvas/src/client/styles.ts similarity index 100% rename from packages/mtmharness/src/features/canvas/client/styles.ts rename to packages/mtmcanvas/src/client/styles.ts diff --git a/packages/mtmharness/src/features/canvas/contract/canvas.test.ts b/packages/mtmcanvas/src/contract/canvas.test.ts similarity index 100% rename from packages/mtmharness/src/features/canvas/contract/canvas.test.ts rename to packages/mtmcanvas/src/contract/canvas.test.ts diff --git a/packages/mtmharness/src/features/canvas/contract/canvas.ts b/packages/mtmcanvas/src/contract/canvas.ts similarity index 100% rename from packages/mtmharness/src/features/canvas/contract/canvas.ts rename to packages/mtmcanvas/src/contract/canvas.ts diff --git a/packages/mtmharness/src/features/canvas/contract/rpc.ts b/packages/mtmcanvas/src/contract/rpc.ts similarity index 100% rename from packages/mtmharness/src/features/canvas/contract/rpc.ts rename to packages/mtmcanvas/src/contract/rpc.ts diff --git a/packages/mtmharness/src/features/canvas/index.test.ts b/packages/mtmcanvas/src/index.test.ts similarity index 100% rename from packages/mtmharness/src/features/canvas/index.test.ts rename to packages/mtmcanvas/src/index.test.ts diff --git a/packages/mtmharness/src/features/canvas/index.ts b/packages/mtmcanvas/src/index.ts similarity index 97% rename from packages/mtmharness/src/features/canvas/index.ts rename to packages/mtmcanvas/src/index.ts index 6c9052e..09a3b52 100644 --- a/packages/mtmharness/src/features/canvas/index.ts +++ b/packages/mtmcanvas/src/index.ts @@ -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"; @@ -73,6 +74,9 @@ async function readDocument(ctx: Context, directory: Awaited> => { if (endpoint !== "request") return failure(new Error("unknown canvas endpoint")); diff --git a/packages/mtmcanvas/tests/package-contract.test.ts b/packages/mtmcanvas/tests/package-contract.test.ts new file mode 100644 index 0000000..a2e0cec --- /dev/null +++ b/packages/mtmcanvas/tests/package-contract.test.ts @@ -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"); + }); +}); diff --git a/packages/mtmcanvas/tsconfig.json b/packages/mtmcanvas/tsconfig.json new file mode 100644 index 0000000..fcebf66 --- /dev/null +++ b/packages/mtmcanvas/tsconfig.json @@ -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" + ] +} diff --git a/packages/mtmcanvas/vitest.config.ts b/packages/mtmcanvas/vitest.config.ts new file mode 100644 index 0000000..a7856ab --- /dev/null +++ b/packages/mtmcanvas/vitest.config.ts @@ -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"], + }, +}); diff --git a/packages/mtmharness/README.md b/packages/mtmharness/README.md index a4b88f7..bcf982b 100644 --- a/packages/mtmharness/README.md +++ b/packages/mtmharness/README.md @@ -2,10 +2,10 @@ `mtmharness` is one public npm package with one unified DSH plugin and two explicit client identities: -- **DSH Web plugin**: the package root and `./client` export use the official `dsh.client` lazy-CJS contract. One installation provides MTM and Canvas sidebar actions, the Connect control panel, a file-backed Canvas editor over `ctx.fs`, and the settings-controlled Codebase Memory/Ponytail coding features. +- **DSH Web plugin**: the package root and `./client` export use the official `dsh.client` lazy-CJS contract. One installation provides the MTM sidebar action, the Connect control panel, and the settings-controlled Codebase Memory/Ponytail coding features. - **Independent web client**: the package also publishes a BrowserHistory static app and a MemoryHistory script/embed entry. These artifacts own their React root, router, styles, and teardown and never load the local coding runtime. -The DSH plugin is assembled from Connect, Canvas, and coding feature domains under one Host/Client lifecycle. Codebase Memory keeps its `codebase_memory` server namespace and `mcp__codebase_memory__*` tool names; Ponytail ships six skills inline, including `/ponytail` and its companion commands. The `mtm-coding` settings namespace remains the configuration contract inside the unified `mtmharness` package. +The DSH plugin is assembled from Connect and coding feature domains under one Host/Client lifecycle. Codebase Memory keeps its `codebase_memory` server namespace and `mcp__codebase_memory__*` tool names; Ponytail ships six skills inline, including `/ponytail` and its companion commands. The `mtm-coding` settings namespace remains the configuration contract inside the unified `mtmharness` package. RTK is an optional coding feature in the same `mtm-coding` settings namespace. `rtkMode` defaults to `auto`: it transparently rewrites Bash calls when the DSH `tools/pre-record-input` capability is available and falls back to inline guidance when it is not. Explicit `rewrite` is strict and reports `unavailable` on older DSH runtimes instead of changing frozen tool inputs. The pinned RTK `v0.45.0` binary is resolved from an explicit `rtkCommand` or lazily installed under the DSH home with checksum verification; RTK telemetry, tracking, and tee output are disabled for plugin-managed runs. @@ -16,24 +16,22 @@ Install the package into a web profile: dsh plugin --profile web add mtmharness dsh --profile web --dump-config -Restart the DSH Web host after changing profile composition. The `MTM` and `Canvas` actions appear in the sidebar footer. The host owns the React root, session connection, and lifecycle. Canvas initialization derives a `.mtmcanvas` child from the active `ctx.fs` workspace and requires the Host's `directoryPicker` to expose the `browse` capability so it can create that child; a native-only picker cannot create this workspace directory. +Restart the DSH Web host after changing profile composition. The `MTM` action appears in the sidebar footer. The host owns the React root, session connection, and lifecycle. Install the independent `mtmcanvas` package to add the Canvas action and its Host/Client entry. The plugin enables the Connect control panel by default and keeps its registry and `/mtm-connect` RPC on the DSH Host loopback boundary. The independent static/embed client remains a separate application surface and is not part of the DSH plugin. -### Migrating an existing profile +### Independent Canvas plugin -If the profile previously installed the retired packages, remove them before adding the unified package so their old rows do not remain alongside the `mtmharness` row: +Canvas is maintained as the separate `mtmcanvas` DSH package. Add it after installing or upgrading `mtmharness`: - dsh plugin --profile web remove mtmcanvas - dsh plugin --profile web remove mtm-connect - dsh plugin --profile web remove mtm-coding dsh plugin --profile web add mtmharness + dsh plugin --profile web add mtmcanvas -The same hard-cut sequence is covered by the committed isolated profile smoke, including removal of the retired `mtm-coding` row, duplicate install, removal, and reinstall: +Profiles created from an older `mtmharness` release should remove retired `mtm-connect` and `mtm-coding` rows before adding the current packages. The committed profile smoke still covers the old-row cleanup and duplicate install path: pnpm --filter mtmharness run profile:check -- /path/to/mtmharness.tgz -Historical `mtmcanvas`, `mtm-connect`, and `mtm-coding` npm versions remain available as history, but the mtmdsh workspace no longer publishes new versions of those package names. Imports from the retired package names are intentionally not compatibility aliases; this release is a hard cut to the unified plugin entry. +`mtmcanvas` now owns the Canvas Host/Client implementation; `mtm-connect` and `mtm-coding` remain historical package names and are not compatibility aliases. ## Static App diff --git a/packages/mtmharness/package.json b/packages/mtmharness/package.json index f841f10..b6d4717 100644 --- a/packages/mtmharness/package.json +++ b/packages/mtmharness/package.json @@ -1,7 +1,7 @@ { "name": "mtmharness", "version": "0.5.4", - "description": "Unified DeepSeek Harness Web plugin with Connect, Canvas, Codebase Memory, Ponytail, and independent static/embed clients.", + "description": "Unified DeepSeek Harness Web plugin with Connect, Codebase Memory, Ponytail, and independent static/embed clients.", "type": "module", "engines": { "node": ">=22.19.0", "pnpm": ">=11.7.0" }, "main": "./lib/index.js", @@ -69,8 +69,6 @@ "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-agent": "^0.1.1-rc.2", "@deepseek-ai/dsh-commands": "^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", "@deepseek-ai/dsh-llm": "^0.1.1-rc.2", "@deepseek-ai/dsh-mcp-client": "^0.1.1-rc.2", "@deepseek-ai/dsh-session": "^0.1.1-rc.2", @@ -95,8 +93,6 @@ "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-agent": "0.1.1-rc.2", "@deepseek-ai/dsh-commands": "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", "@deepseek-ai/dsh-llm": "0.1.1-rc.2", "@deepseek-ai/dsh-mcp-client": "0.1.1-rc.2", "@deepseek-ai/dsh-session": "0.1.1-rc.2", diff --git a/packages/mtmharness/src/client/MtmCanvasAction.test.tsx b/packages/mtmharness/src/client/MtmCanvasAction.test.tsx deleted file mode 100644 index 6de5800..0000000 --- a/packages/mtmharness/src/client/MtmCanvasAction.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -// @vitest-environment jsdom -import { act } from "react"; -import { createRoot } from "react-dom/client"; -import { afterEach, describe, expect, it } from "vitest"; -import { CanvasRuntime } from "../features/canvas/client/runtime.ts"; -import { MtmCanvasAction, type MtmCanvasActionProps } from "./MtmCanvasAction.tsx"; - -(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; - -let root: ReturnType | undefined; -let container: HTMLDivElement | undefined; -let runtime: CanvasRuntime | undefined; - -afterEach(() => { - act(() => { root?.unmount(); }); - runtime?.dispose(); - container?.remove(); - root = undefined; - runtime = undefined; - container = undefined; - document.body.replaceChildren(); -}); - -describe("MtmCanvasAction", () => { - it("opens the file-backed Canvas editor", async () => { - container = document.createElement("div"); - document.body.append(container); - root = createRoot(container); - runtime = new CanvasRuntime({ call: async () => ({ ok: true, value: [] }) }); - const useCanvas: MtmCanvasActionProps["useCanvas"] = (selector) => selector(runtime!.getSnapshot()); - act(() => { root?.render(); }); - - const trigger = document.querySelector('button[aria-label="Open Canvas"]'); - expect(trigger).not.toBeNull(); - act(() => { trigger?.click(); }); - expect(document.body.textContent).toContain("Canvas"); - expect(document.body.textContent).toContain("Loading files..."); - - const close = document.querySelector('button[aria-label="Close Canvas"]'); - expect(close).not.toBeNull(); - act(() => { close?.click(); }); - expect(document.body.textContent).not.toContain("No canvas files yet."); - }); -}); diff --git a/packages/mtmharness/src/client/index.test.ts b/packages/mtmharness/src/client/index.test.ts index e98c9ed..e9ff7c3 100644 --- a/packages/mtmharness/src/client/index.test.ts +++ b/packages/mtmharness/src/client/index.test.ts @@ -172,9 +172,8 @@ describe("mtmharness browser half", () => { expect(registered).toEqual(expect.arrayContaining([ expect.objectContaining({ name: "sidebar.footer.action", options: expect.objectContaining({ id: "mtmharness", order: 10 }) }), ])); - expect(registered.filter((entry) => entry.name === "sidebar.footer.action")).toHaveLength(3); + expect(registered.filter((entry) => entry.name === "sidebar.footer.action")).toHaveLength(2); expect(registered).toEqual(expect.arrayContaining([ - expect.objectContaining({ name: "sidebar.footer.action", options: expect.objectContaining({ id: "mtmcanvas", order: 11 }) }), expect.objectContaining({ name: "sidebar.footer.action", options: expect.objectContaining({ id: "mtmdsh-launcher", order: 12 }) }), expect.objectContaining({ name: "shell.overlay", options: expect.objectContaining({ id: "mtmdsh-launcher-overlay", order: 100 }) }), ])); diff --git a/packages/mtmharness/src/client/index.ts b/packages/mtmharness/src/client/index.ts index 63d6c66..485be8f 100644 --- a/packages/mtmharness/src/client/index.ts +++ b/packages/mtmharness/src/client/index.ts @@ -7,10 +7,8 @@ import type {} from "@deepseek-ai/dsh-client-ui-sidebar/client"; import type {} from "@deepseek-ai/dsh-client-ui-layout/client"; import { apply as applyCoding } from "../features/coding/client/index.tsx"; import { apply as applyConnect } from "../features/connect/client/index.ts"; -import { apply as applyCanvas } from "../features/canvas/client/index.ts"; import type { MtmConnectPanelActions } from "../features/connect/client/MtmConnectPanel.tsx"; import { MtmHarnessAction, type MtmHarnessActionInjected } from "./MtmHarnessAction.tsx"; -import { MtmCanvasAction } from "./MtmCanvasAction.tsx"; import { MtmHarnessLauncherAction, MtmHarnessLauncherOverlay } from "./launcher.tsx"; import { disposeMtmHarnessLauncher } from "./launcher-state.ts"; @@ -22,7 +20,6 @@ export function apply(ctx: ClientContext): void { if (ctx.get("connection") === undefined) throw new Error("mtmharness: DSH connection service is unavailable"); applyCoding(ctx); const runtime = applyConnect(ctx); - const canvasRuntime = applyCanvas(ctx); const actions: MtmConnectPanelActions = { selectConnection: (connectionId) => { runtime.selectConnection(connectionId); }, refresh: () => { runtime.refresh(); }, @@ -42,12 +39,6 @@ export function apply(ctx: ClientContext): void { order: 10, inject: (): MtmHarnessActionInjected => ({ actions, hooks: { connect: runtime } }), }, MtmHarnessAction)); - ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({ - name: "sidebar.footer.action", - id: "mtmcanvas", - order: 11, - inject: () => ({ actions: canvasRuntime, hooks: { canvas: canvasRuntime } }), - }, MtmCanvasAction)); // The launcher loads the latest stable app directly from the package CDN. ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({ name: "sidebar.footer.action", diff --git a/packages/mtmharness/src/index.ts b/packages/mtmharness/src/index.ts index 23ce316..d93e672 100644 --- a/packages/mtmharness/src/index.ts +++ b/packages/mtmharness/src/index.ts @@ -2,7 +2,6 @@ import type { Context } from "@deepseek-ai/cordis"; import { apply as applyCodingHost } from "./features/coding/index.ts"; import { apply as applyConnectHost } from "./features/connect/index.ts"; -import { apply as applyCanvasHost } from "./features/canvas/index.ts"; export { buildMcpConfig, resolveConfig } from "./features/coding/index.ts"; export { PONYTAIL_SKILLS } from "./features/coding/ponytail-skills.ts"; @@ -36,12 +35,11 @@ export { export type { MtmCodingConfig, MtmCodingSettings, PonytailMode, RtkMode } from "./features/coding/types.ts"; export const name = "mtmharness"; -export const inject = ["connection", "fs", "directoryPicker", "settings"]; +export const inject = ["connection", "settings"]; /** Mount the Host-owned MTM and coding control planes. */ export async function apply(ctx: Context, config: Record = {}): Promise { if (ctx.connection === undefined) throw new Error("mtmharness: DSH connection service is unavailable"); applyConnectHost(ctx); - applyCanvasHost(ctx); await applyCodingHost(ctx, config); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84aba85..bfc1de5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,60 @@ importers: specifier: 6.0.3 version: 6.0.3 + packages/mtmcanvas: + devDependencies: + '@deepseek-ai/cordis': + specifier: 4.0.1 + version: 4.0.1(@deepseek-ai/cordis-plugin-include@1.0.6)(@deepseek-ai/cordis-plugin-loader@1.0.2) + '@deepseek-ai/dsh-client-connection': + specifier: 0.1.1-rc.2 + version: 0.1.1-rc.2(9b40f28fccfb9ee3f86188afc9586c29) + '@deepseek-ai/dsh-client-runtime': + specifier: 0.1.1-rc.2 + version: 0.1.1-rc.2(601e9129357dc48738583e18d75d19b2) + '@deepseek-ai/dsh-client-ui-layout': + specifier: 0.1.1-rc.2 + version: 0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-client-runtime@0.1.1-rc.2(601e9129357dc48738583e18d75d19b2))(@deepseek-ai/dsh-client-ui-theme@0.1.1-rc.2(a6c39110759121c55549be9a5dac7bf7))(@deepseek-ai/dsh-invariants@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-client-ui-primitives': + specifier: 0.1.1-rc.2 + version: 0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-client-ui-sidebar': + specifier: 0.1.1-rc.2 + version: 0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-client-locale@0.1.1-rc.2(048e7c438545728704e82d9acbc5c221))(@deepseek-ai/dsh-client-runtime@0.1.1-rc.2(601e9129357dc48738583e18d75d19b2))(@deepseek-ai/dsh-client-ui-layout@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-client-runtime@0.1.1-rc.2(601e9129357dc48738583e18d75d19b2))(@deepseek-ai/dsh-client-ui-theme@0.1.1-rc.2(a6c39110759121c55549be9a5dac7bf7))(@deepseek-ai/dsh-invariants@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-client-ui-slots': + specifier: 0.1.1-rc.2 + version: 0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-fs': + specifier: 0.1.1-rc.2 + version: 0.1.1-rc.2(369bb7c3e084bdd2eec52747b4523f36) + '@deepseek-ai/dsh-host-directory-picker': + specifier: 0.1.1-rc.2 + version: 0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)) + '@types/node': + specifier: 22.20.1 + version: 22.20.1 + '@types/react': + specifier: ~18.3.1 + version: 18.3.31 + '@types/react-dom': + specifier: ~18.3.0 + version: 18.3.7(@types/react@18.3.31) + esbuild: + specifier: 0.28.2 + version: 0.28.2 + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vitest: + specifier: 4.1.11 + version: 4.1.11(@types/node@22.20.1)(jsdom@30.0.1)(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)) + packages/mtmharness: dependencies: '@deepseek-ai/schemastery': @@ -60,12 +114,6 @@ importers: '@deepseek-ai/dsh-commands': specifier: 0.1.1-rc.2 version: 0.1.1-rc.2(0ecd3a66a950f0a38490382753aa1d93) - '@deepseek-ai/dsh-fs': - specifier: 0.1.1-rc.2 - version: 0.1.1-rc.2(369bb7c3e084bdd2eec52747b4523f36) - '@deepseek-ai/dsh-host-directory-picker': - specifier: 0.1.1-rc.2 - version: 0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)) '@deepseek-ai/dsh-llm': specifier: 0.1.1-rc.2 version: 0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.1-rc.2(@deepseek-ai/cordis@4.0.1)))