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
This commit is contained in:
parent
fe3ce25ae3
commit
beb4599090
7 changed files with 111 additions and 26 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -15,7 +15,9 @@
|
|||
|
||||
# next.js
|
||||
/.next/
|
||||
/.next-cli-build/
|
||||
/out/
|
||||
cli/.build-home/
|
||||
product
|
||||
# production
|
||||
/build
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.12",
|
||||
"nodemon": "^3.1.14"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
<span key={m} className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-black/5 dark:bg-white/5 text-text-muted border border-transparent hover:border-border">
|
||||
{m}
|
||||
<button onClick={() => setSelectedModels((prev) => prev.filter((x) => x !== m))} className="ml-0.5 hover:text-red-500">
|
||||
<button onClick={() => handleRemoveModel(m)} className="ml-0.5 hover:text-red-500">
|
||||
<span className="material-symbols-outlined text-[12px]">close</span>
|
||||
</button>
|
||||
</span>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setModelSelectOpen(true)}
|
||||
disabled={!hasActiveProviders}
|
||||
className={`shrink-0 px-2 py-1.5 rounded border text-xs whitespace-nowrap transition-colors ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
>
|
||||
Select Model
|
||||
</button>
|
||||
<button onClick={() => setComboModalOpen(true)} disabled={!hasActiveProviders} className={`shrink-0 px-2 py-1.5 rounded border text-xs whitespace-nowrap transition-colors ${hasActiveProviders ? "bg-primary/10 border-primary/40 text-primary hover:bg-primary/20 cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>+ Combo</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -501,6 +531,18 @@ export default function CoworkToolCard({
|
|||
title="Create Cowork Combo"
|
||||
/>
|
||||
|
||||
<ModelSelectModal
|
||||
isOpen={modelSelectOpen}
|
||||
onClose={() => setModelSelectOpen(false)}
|
||||
onSelect={handleAddModel}
|
||||
onDeselect={handleRemoveModel}
|
||||
activeProviders={activeProviders}
|
||||
modelAliases={modelAliases}
|
||||
title="Select Cowork Model"
|
||||
addedModelValues={selectedModels}
|
||||
closeOnSelect={false}
|
||||
/>
|
||||
|
||||
<McpMarketplaceModal
|
||||
isOpen={marketplaceOpen}
|
||||
onClose={() => setMarketplaceOpen(false)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue