From beb4599090dea7d18e49ba90a99adb9dffebaac6 Mon Sep 17 00:00:00 2001 From: local Date: Fri, 15 May 2026 09:30:31 +0700 Subject: [PATCH] Fix Cowork model selection and Windows CLI packaging (#1129) Cherry-picked from upstream PR #1129 + local improvements: - dedupe inline remove-model handler -> use handleRemoveModel - add .next-cli-build/ and cli/.build-home/ to .gitignore --- .gitignore | 2 + cli/package.json | 1 + cli/scripts/build-cli.js | 69 ++++++++++++++----- next.config.mjs | 1 + package.json | 8 +-- postcss.config.mjs | 10 ++- .../cli-tools/components/CoworkToolCard.js | 46 ++++++++++++- 7 files changed, 111 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 9123977..328c2e4 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,9 @@ # next.js /.next/ +/.next-cli-build/ /out/ +cli/.build-home/ product # production /build diff --git a/cli/package.json b/cli/package.json index d18c26b..aad50da 100644 --- a/cli/package.json +++ b/cli/package.json @@ -42,6 +42,7 @@ ], "license": "MIT", "devDependencies": { + "esbuild": "^0.25.12", "nodemon": "^3.1.14" } } diff --git a/cli/scripts/build-cli.js b/cli/scripts/build-cli.js index 649cfff..4b5443f 100644 --- a/cli/scripts/build-cli.js +++ b/cli/scripts/build-cli.js @@ -8,6 +8,17 @@ const cliDir = path.resolve(__dirname, ".."); const appDir = path.resolve(cliDir, ".."); const rootDir = path.resolve(appDir, ".."); const cliAppDir = path.join(cliDir, "app"); +const buildHomeDir = path.join(cliDir, ".build-home"); +const buildDistDirName = ".next-cli-build"; +const buildDistDir = path.join(appDir, buildDistDirName); + +function shouldUseWorkspaceTracingRoot() { + const appNodeModules = path.join(appDir, "node_modules"); + const rootNodeModules = path.join(rootDir, "node_modules"); + + // Only widen tracing when dependencies are actually hoisted above appDir. + return !fs.existsSync(appNodeModules) && fs.existsSync(rootNodeModules); +} // Exclude patterns for files/folders we don't want to copy const EXCLUDE_PATTERNS = [ @@ -81,14 +92,22 @@ function copyRecursive(src, dest) { console.log("📦 Building 9Router CLI package with Next.js...\n"); +fs.mkdirSync(buildHomeDir, { recursive: true }); +fs.mkdirSync(path.join(buildHomeDir, "AppData", "Roaming"), { recursive: true }); +fs.mkdirSync(path.join(buildHomeDir, "AppData", "Local"), { recursive: true }); + // Step 0: Sync version from app/cli/package.json to app/package.json console.log("0️⃣ Syncing version to app/package.json..."); const cliPkg = JSON.parse(fs.readFileSync(path.join(cliDir, "package.json"), "utf8")); const appPkgPath = path.join(appDir, "package.json"); const appPkg = JSON.parse(fs.readFileSync(appPkgPath, "utf8")); -appPkg.version = cliPkg.version; -fs.writeFileSync(appPkgPath, JSON.stringify(appPkg, null, 2) + "\n"); -console.log(`✅ Version synced: ${cliPkg.version}\n`); +if (appPkg.version !== cliPkg.version) { + appPkg.version = cliPkg.version; + fs.writeFileSync(appPkgPath, JSON.stringify(appPkg, null, 2) + "\n"); + console.log(`✅ Version synced: ${cliPkg.version}\n`); +} else { + console.log(`✅ Version already synced: ${cliPkg.version}\n`); +} // Step 1: Build app with Next.js (workspace tracing root → traced node_modules in standalone). console.log("1️⃣ Building Next.js app..."); @@ -96,7 +115,15 @@ try { execSync("npm run build", { stdio: "inherit", cwd: appDir, - env: { ...process.env, NEXT_TRACING_ROOT_MODE: "workspace" } + env: { + ...process.env, + HOME: buildHomeDir, + USERPROFILE: buildHomeDir, + APPDATA: path.join(buildHomeDir, "AppData", "Roaming"), + LOCALAPPDATA: path.join(buildHomeDir, "AppData", "Local"), + NEXT_DIST_DIR: buildDistDirName, + NEXT_TRACING_ROOT_MODE: shouldUseWorkspaceTracingRoot() ? "workspace" : "project", + } }); console.log("✅ Next.js build completed\n"); } catch (error) { @@ -112,21 +139,25 @@ if (fs.existsSync(cliAppDir)) { console.log("✅ Cleaned\n"); // Step 3: Copy Next.js standalone build to app/cli/app. -// With workspace tracing root, Next places app files under .next/standalone/app/ and traced -// node_modules under .next/standalone/node_modules/ (slim, tracing-pruned). +// Newer Next.js standalone output writes server.js/package.json plus .next/, src/, and +// node_modules/ directly under .next/standalone. Older builds may still use a nested app/. console.log("3️⃣ Copying Next.js standalone build to app/cli/app..."); const standaloneRoot = path.join(appDir, ".next", "standalone"); -const standaloneApp = path.join(standaloneRoot, "app"); +const standaloneRootResolved = path.join(buildDistDir, "standalone"); +const standaloneRootToUse = fs.existsSync(standaloneRootResolved) ? standaloneRootResolved : standaloneRoot; +const standaloneApp = fs.existsSync(path.join(standaloneRootToUse, "server.js")) + ? standaloneRootToUse + : path.join(standaloneRootToUse, "app"); if (!fs.existsSync(standaloneApp)) { - console.error("❌ Next.js standalone build not found at .next/standalone/app"); - console.error("Make sure output: 'standalone' is set in next.config.js"); + console.error("❌ Next.js standalone build not found under .next/standalone"); + console.error("Expected either .next/standalone/server.js or .next/standalone/app/"); process.exit(1); } copyRecursive(standaloneApp, cliAppDir); -// Copy traced node_modules from standalone root into CLI bundle -const standaloneNodeModules = path.join(standaloneRoot, "node_modules"); -if (fs.existsSync(standaloneNodeModules)) { +// Older nested-app layout stores traced node_modules at standalone root. +const standaloneNodeModules = path.join(standaloneRootToUse, "node_modules"); +if (standaloneApp !== standaloneRootToUse && fs.existsSync(standaloneNodeModules)) { copyRecursive(standaloneNodeModules, path.join(cliAppDir, "node_modules")); } console.log("✅ Copied standalone build\n"); @@ -166,9 +197,10 @@ console.log(""); // Step 4: Copy static files console.log("4️⃣ Copying static files..."); const staticSrc = path.join(appDir, ".next", "static"); -const staticDest = path.join(cliAppDir, ".next", "static"); -if (fs.existsSync(staticSrc)) { - copyRecursive(staticSrc, staticDest); +const staticSrcResolved = path.join(buildDistDir, "static"); +const staticDest = path.join(cliAppDir, buildDistDirName, "static"); +if (fs.existsSync(staticSrcResolved) || fs.existsSync(staticSrc)) { + copyRecursive(fs.existsSync(staticSrcResolved) ? staticSrcResolved : staticSrc, staticDest); console.log("✅ Copied static files\n"); } else { console.log("⏭️ No static files found\n"); @@ -188,9 +220,10 @@ if (fs.existsSync(publicSrc)) { // Step 6: Copy vendor-chunks (required for production) console.log("6️⃣ Copying vendor-chunks..."); const vendorChunksSrc = path.join(appDir, ".next", "server", "vendor-chunks"); -const vendorChunksDest = path.join(cliAppDir, ".next", "server", "vendor-chunks"); -if (fs.existsSync(vendorChunksSrc)) { - copyRecursive(vendorChunksSrc, vendorChunksDest); +const vendorChunksSrcResolved = path.join(buildDistDir, "server", "vendor-chunks"); +const vendorChunksDest = path.join(cliAppDir, buildDistDirName, "server", "vendor-chunks"); +if (fs.existsSync(vendorChunksSrcResolved) || fs.existsSync(vendorChunksSrc)) { + copyRecursive(fs.existsSync(vendorChunksSrcResolved) ? vendorChunksSrcResolved : vendorChunksSrc, vendorChunksDest); console.log("✅ Copied vendor-chunks\n"); } else { console.log("⏭️ No vendor-chunks found\n"); diff --git a/next.config.mjs b/next.config.mjs index 0121ea3..85c7a25 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -10,6 +10,7 @@ const tracingRoot = process.env.NEXT_TRACING_ROOT_MODE === "workspace" /** @type {import('next').NextConfig} */ const nextConfig = { + distDir: process.env.NEXT_DIST_DIR || ".next", output: "standalone", serverExternalPackages: ["better-sqlite3", "sql.js", "node:sqlite", "bun:sqlite"], turbopack: { diff --git a/package.json b/package.json index 450ff7d..79b91f8 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,11 @@ "private": true, "scripts": { "dev": "next dev --webpack --port 20128", - "build": "NODE_ENV=production next build --webpack", - "start": "NODE_ENV=production next start", + "build": "next build --webpack", + "start": "next start", "dev:bun": "bun --bun next dev --webpack --port 20128", - "build:bun": "NODE_ENV=production bun --bun next build --webpack", - "start:bun": "NODE_ENV=production bun ./.next/standalone/server.js" + "build:bun": "bun --bun next build --webpack", + "start:bun": "bun ./.next/standalone/server.js" }, "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/postcss.config.mjs b/postcss.config.mjs index fa4a1da..17fa423 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,6 +1,12 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const projectRoot = path.dirname(fileURLToPath(import.meta.url)); + export default { plugins: { - "@tailwindcss/postcss": {}, + "@tailwindcss/postcss": { + base: projectRoot, + }, }, }; - diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js index 3e6eb44..01d1aaa 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect } from "react"; -import { Card, Button, ManualConfigModal, ComboFormModal, McpMarketplaceModal } from "@/shared/components"; +import { Card, Button, ManualConfigModal, ComboFormModal, McpMarketplaceModal, ModelSelectModal } from "@/shared/components"; import Image from "next/image"; import BaseUrlSelect from "./BaseUrlSelect"; import ApiKeySelect from "./ApiKeySelect"; @@ -43,7 +43,9 @@ export default function CoworkToolCard({ const [plugins, setPlugins] = useState([]); const [localPlugins, setLocalPlugins] = useState([]); const [customPlugins, setCustomPlugins] = useState([]); + const [modelAliases, setModelAliases] = useState({}); const [comboModalOpen, setComboModalOpen] = useState(false); + const [modelSelectOpen, setModelSelectOpen] = useState(false); const [marketplaceOpen, setMarketplaceOpen] = useState(false); const [addMcpOpen, setAddMcpOpen] = useState(false); const [addMcpForm, setAddMcpForm] = useState({ type: "url", name: "", url: "", command: "", args: "" }); @@ -62,6 +64,16 @@ export default function CoworkToolCard({ if (isExpanded && !status) checkStatus(); }, [isExpanded]); + useEffect(() => { + if (!isExpanded) return; + fetch("/api/models/alias") + .then((r) => r.ok ? r.json() : null) + .then((data) => { + if (data) setModelAliases(data.aliases || {}); + }) + .catch(() => {}); + }, [isExpanded]); + useEffect(() => { if (status?.cowork?.models?.length) { setSelectedModels(status.cowork.models); @@ -170,6 +182,17 @@ export default function CoworkToolCard({ } }; + const handleAddModel = (model) => { + const value = model?.value || model?.name || model; + if (!value || selectedModels.includes(value)) return; + setSelectedModels((prev) => [...prev, value]); + }; + + const handleRemoveModel = (model) => { + const value = model?.value || model?.name || model; + setSelectedModels((prev) => prev.filter((item) => item !== value)); + }; + const handleReset = async () => { setRestoring(true); setMessage(null); @@ -313,13 +336,20 @@ export default function CoworkToolCard({ selectedModels.map((m) => ( {m} - )) )} + @@ -501,6 +531,18 @@ export default function CoworkToolCard({ title="Create Cowork Combo" /> + setModelSelectOpen(false)} + onSelect={handleAddModel} + onDeselect={handleRemoveModel} + activeProviders={activeProviders} + modelAliases={modelAliases} + title="Select Cowork Model" + addedModelValues={selectedModels} + closeOnSelect={false} + /> + setMarketplaceOpen(false)}