Compare commits

..

8 Commits

Author SHA1 Message Date
dayuan.jiang
d210d0e44e fix: add trailing edge to scroll throttle
Ensures the chat scrolls to the bottom after streaming ends, even if
the last messages update arrives during the 150ms throttle window.
2026-04-10 10:56:52 +09:00
dayuan.jiang
b5abbd8e6f fix: reduce CPU usage during large XML streaming
Skip Prism syntax highlighting while tool call is streaming — use plain
<pre> during streaming and only run Prism once at completion. Also throttle
scrollIntoView to once per 150ms to avoid layout thrashing.

Profiled with Playwright + CDP: for 200-cell diagrams the longest
browser task dropped from 6.7s to ~450ms and total long-task time
fell from ~10.9s to ~1.1s.
2026-04-10 10:52:44 +09:00
Octopus
622aa8683d fix: remove redundant status(modified:false) call to restore undo/redo (fixes #779) (#780) 2026-04-10 10:30:05 +09:00
Octopus
43ddb7a999 fix: allow Qwen3.5 models to use image input (fixes #799) (#800)
Qwen3.5 models deployed via vLLM natively support image input, but the
supportsImageInput() check was incorrectly blocking them. The function
only exempted qwen3.5-plus and qwen3.5-flash variants, missing the base
qwen3.5 model name.

Simplify the exception to cover all qwen3.5 variants with a single
substring check on "qwen3.5", since it is a common prefix of all three.

Co-authored-by: octo-patch <octo-patch@github.com>
2026-04-10 10:23:39 +09:00
Dayuan Jiang
b9fdf9538c chore: remove MCP preview labels (#790)
MCP server is no longer in preview. Remove "(Preview)" headings from
READMEs, the purple PREVIEW badge from the UI, and the preview i18n keys.
2026-04-06 10:17:38 +09:00
Dayuan Jiang
1f31692701 chore: always bundle latest draw.io version in Electron builds (#792)
Remove pinned v29.3.5 tag so the build always clones the latest draw.io.
This adds the Animated GIF export and other new features to the Electron app.

Closes #770
2026-04-06 10:14:50 +09:00
Dayuan Jiang
31819f413c fix: add 10MB body size limit to MCP HTTP endpoints (#791)
All three POST handlers (/api/state, /api/restore, /api/history-svg)
now use a shared readBody() helper that enforces a 10MB limit and
returns 413 if exceeded, preventing memory exhaustion from oversized
requests.

Bumps @next-ai-drawio/mcp-server to 0.1.19.
2026-04-06 09:14:20 +09:00
Dayuan Jiang
41c410c2ba fix: bind MCP server HTTP to 127.0.0.1 only (#787)
The embedded HTTP sidecar was using server.listen(port) without a host
argument, which defaults to 0.0.0.0 (all interfaces). This exposed the
server to the local network. Now explicitly binds to 127.0.0.1.

Also excludes release/ from tsconfig to fix pre-existing TS errors.

Bumps @next-ai-drawio/mcp-server to 0.1.18.
2026-04-06 09:04:38 +09:00
29 changed files with 113 additions and 833 deletions

View File

@@ -37,7 +37,7 @@ jobs:
- name: Download draw.io static files for offline use - name: Download draw.io static files for offline use
run: | run: |
rm -rf public/drawio rm -rf public/drawio
git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio git clone --depth 1 https://github.com/jgraph/drawio.git /tmp/drawio
mkdir -p public/drawio mkdir -p public/drawio
cp -r /tmp/drawio/src/main/webapp/* public/drawio/ cp -r /tmp/drawio/src/main/webapp/* public/drawio/
rm -rf public/drawio/WEB-INF rm -rf public/drawio/WEB-INF
@@ -70,7 +70,7 @@ jobs:
shell: bash shell: bash
run: | run: |
rm -rf public/drawio rm -rf public/drawio
git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio git clone --depth 1 https://github.com/jgraph/drawio.git /tmp/drawio
mkdir -p public/drawio mkdir -p public/drawio
cp -r /tmp/drawio/src/main/webapp/* public/drawio/ cp -r /tmp/drawio/src/main/webapp/* public/drawio/
rm -rf public/drawio/WEB-INF rm -rf public/drawio/WEB-INF
@@ -105,14 +105,9 @@ jobs:
wait-for-completion: true wait-for-completion: true
output-artifact-directory: release-signed output-artifact-directory: release-signed
- name: Fix latest.yml with signed exe hashes - name: Upload signed artifacts to release
run: node scripts/fix-latest-yml.mjs
- name: Upload signed artifacts and update metadata to release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: | files: release-signed/*.exe
release-signed/*.exe
release/latest*.yml
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -31,7 +31,7 @@ https://github.com/user-attachments/assets/9d60a3e8-4a1c-4b5e-acbb-26af2d3eabd1
- [Table of Contents](#table-of-contents) - [Table of Contents](#table-of-contents)
- [Examples](#examples) - [Examples](#examples)
- [Features](#features) - [Features](#features)
- [MCP Server (Preview)](#mcp-server-preview) - [MCP Server](#mcp-server)
- [Claude Code CLI](#claude-code-cli) - [Claude Code CLI](#claude-code-cli)
- [Getting Started](#getting-started) - [Getting Started](#getting-started)
- [Try it Online](#try-it-online) - [Try it Online](#try-it-online)
@@ -99,9 +99,7 @@ Here are some example prompts and their generated diagrams:
- **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure) - **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure)
- **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization - **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
## MCP Server (Preview) ## MCP Server
> **Preview Feature**: This feature is experimental and may not be stable.
Use Next AI Draw.io with AI agents like Claude Desktop, Cursor, and VS Code via MCP (Model Context Protocol). Use Next AI Draw.io with AI agents like Claude Desktop, Cursor, and VS Code via MCP (Model Context Protocol).

View File

@@ -100,12 +100,8 @@ export default function Home() {
const handleDrawioAutoSave = useCallback( const handleDrawioAutoSave = useCallback(
(data: { xml?: string }) => { (data: { xml?: string }) => {
handleDiagramAutoSave(data) handleDiagramAutoSave(data)
// Only suppress modified state when persistence is available
if (canPersist) {
drawioRef.current?.status({ message: "", modified: false })
}
}, },
[canPersist, drawioRef, handleDiagramAutoSave], [handleDiagramAutoSave],
) )
const handleDarkModeChange = () => { const handleDarkModeChange = () => {

View File

@@ -141,9 +141,6 @@ export default function ExamplePanel({
<span className="text-sm font-medium text-foreground group-hover:text-purple-500 transition-colors"> <span className="text-sm font-medium text-foreground group-hover:text-purple-500 transition-colors">
{dict.examples.mcpServer} {dict.examples.mcpServer}
</span> </span>
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-purple-500 text-white rounded">
{dict.examples.preview}
</span>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{dict.examples.mcpDescription} {dict.examples.mcpDescription}

View File

@@ -417,6 +417,7 @@ export function ChatMessageDisplay({
// Track previous message count to detect bulk loads vs streaming // Track previous message count to detect bulk loads vs streaming
const prevMessageCountRef = useRef(0) const prevMessageCountRef = useRef(0)
const scrollThrottleRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => { useEffect(() => {
if (messagesEndRef.current && messages.length > 0) { if (messagesEndRef.current && messages.length > 0) {
@@ -430,8 +431,17 @@ export function ChatMessageDisplay({
return return
} }
// Single message added - smooth scroll // Throttle scroll during streaming to avoid layout thrashing
messagesEndRef.current.scrollIntoView({ behavior: "smooth" }) // Leading + trailing: scroll immediately, then once more after cooldown
if (!scrollThrottleRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
scrollThrottleRef.current = setTimeout(() => {
scrollThrottleRef.current = null
messagesEndRef.current?.scrollIntoView({
behavior: "smooth",
})
}, 150)
}
} }
}, [messages]) }, [messages])

View File

@@ -25,7 +25,6 @@ import Image from "@/components/image-with-basepath"
import { ModelConfigDialog } from "@/components/model-config-dialog" import { ModelConfigDialog } from "@/components/model-config-dialog"
import { SettingsDialog } from "@/components/settings-dialog" import { SettingsDialog } from "@/components/settings-dialog"
import { useDiagram } from "@/contexts/diagram-context" import { useDiagram } from "@/contexts/diagram-context"
import { useAutoUpdate } from "@/hooks/use-auto-update"
import { useDiagramToolHandlers } from "@/hooks/use-diagram-tool-handlers" import { useDiagramToolHandlers } from "@/hooks/use-diagram-tool-handlers"
import { useDictionary } from "@/hooks/use-dictionary" import { useDictionary } from "@/hooks/use-dictionary"
import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config" import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config"
@@ -1254,22 +1253,10 @@ export default function ChatPanel({
sendChatMessage(newParts, savedXml, previousXml, sessionId) sendChatMessage(newParts, savedXml, previousXml, sessionId)
} }
// Auto-update listener (must be before early return so it's always active)
useAutoUpdate()
// Collapsed view (desktop only) // Collapsed view (desktop only)
if (!isVisible && !isMobile) { if (!isVisible && !isMobile) {
return ( return (
<div className="h-full flex flex-col items-center pt-4 bg-card border border-border/30 rounded-xl"> <div className="h-full flex flex-col items-center pt-4 bg-card border border-border/30 rounded-xl">
<Toaster
position="bottom-left"
richColors
expand
toastOptions={{
style: { maxWidth: "480px" },
duration: 2000,
}}
/>
<ButtonWithTooltip <ButtonWithTooltip
tooltipContent={dict.nav.showPanel} tooltipContent={dict.nav.showPanel}
variant="ghost" variant="ghost"

View File

@@ -195,7 +195,22 @@ export function ToolCallCard({
{input && isExpanded && ( {input && isExpanded && (
<div className="px-4 py-3 border-t border-border/40 bg-muted/20"> <div className="px-4 py-3 border-t border-border/40 bg-muted/20">
{typeof input === "object" && input.xml ? ( {typeof input === "object" && input.xml ? (
<CodeBlock code={input.xml} language="xml" /> state === "input-streaming" ||
state === "input-available" ? (
<pre
className="text-[11px] leading-relaxed overflow-x-auto overflow-y-auto max-h-48 scrollbar-thin break-all whitespace-pre-wrap"
style={{
fontFamily:
"var(--font-mono), ui-monospace, monospace",
margin: 0,
padding: 0,
}}
>
{input.xml}
</pre>
) : (
<CodeBlock code={input.xml} language="xml" />
)
) : typeof input === "object" && ) : typeof input === "object" &&
input.operations && input.operations &&
Array.isArray(input.operations) ? ( Array.isArray(input.operations) ? (

View File

@@ -1,124 +0,0 @@
"use client"
import type React from "react"
interface UpdateToastAvailableProps {
variant: "download"
version: string
onDownload: () => void
onDismiss: () => void
}
interface UpdateToastManualProps {
variant: "manual"
version: string
url: string
onDismiss: () => void
}
interface UpdateToastDownloadingProps {
variant: "downloading"
percent: number
onDismiss: () => void
}
type UpdateToastProps =
| UpdateToastAvailableProps
| UpdateToastManualProps
| UpdateToastDownloadingProps
export function UpdateToast(props: UpdateToastProps) {
const { variant, onDismiss } = props
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault()
onDismiss()
}
}
return (
<div
role="alert"
aria-live="polite"
tabIndex={0}
onKeyDown={handleKeyDown}
className="flex items-center gap-3 bg-card border border-border/50 px-4 py-3 rounded-xl shadow-sm"
>
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 flex-shrink-0">
<svg
className="w-4 h-4 text-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
</div>
<div className="flex-1 min-w-0">
{variant === "downloading" ? (
<span className="text-sm text-foreground">
Downloading update... {Math.round(props.percent)}%
</span>
) : (
<span className="text-sm text-foreground">
Version {props.version} is available
</span>
)}
</div>
{variant === "download" && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
props.onDownload()
}}
className="text-xs font-medium px-3 py-1.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors flex-shrink-0"
>
Download
</button>
)}
{variant === "manual" && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
window.open(props.url, "_blank")
}}
className="text-xs font-medium px-3 py-1.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors flex-shrink-0"
>
Download
</button>
)}
<button
type="button"
onClick={onDismiss}
className="text-muted-foreground hover:text-foreground transition-colors flex-shrink-0"
aria-label="Dismiss"
>
<svg
className="w-4 h-4"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
)
}

View File

@@ -30,7 +30,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
- [目录](#目录) - [目录](#目录)
- [示例](#示例) - [示例](#示例)
- [功能特性](#功能特性) - [功能特性](#功能特性)
- [MCP服务器(预览)](#mcp服务器预览) - [MCP服务器](#mcp服务器)
- [Claude Code CLI](#claude-code-cli) - [Claude Code CLI](#claude-code-cli)
- [快速开始](#快速开始) - [快速开始](#快速开始)
- [在线试用](#在线试用) - [在线试用](#在线试用)
@@ -98,9 +98,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
- **云架构图支持**专门支持生成云架构图AWS、GCP、Azure - **云架构图支持**专门支持生成云架构图AWS、GCP、Azure
- **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果 - **动画连接器**:在图表元素之间创建动态动画连接器,实现更好的可视化效果
## MCP服务器(预览) ## MCP服务器
> **预览功能**:此功能为实验性功能,可能不稳定。
通过MCP模型上下文协议在Claude Desktop、Cursor和VS Code等AI代理中使用Next AI Draw.io。 通过MCP模型上下文协议在Claude Desktop、Cursor和VS Code等AI代理中使用Next AI Draw.io。

View File

@@ -28,7 +28,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
- [目次](#目次) - [目次](#目次)
- [](#例) - [](#例)
- [機能](#機能) - [機能](#機能)
- [MCPサーバー(プレビュー)](#mcpサーバープレビュー) - [MCPサーバー](#mcpサーバー)
- [Claude Code CLI](#claude-code-cli) - [Claude Code CLI](#claude-code-cli)
- [はじめに](#はじめに) - [はじめに](#はじめに)
- [オンラインで試す](#オンラインで試す) - [オンラインで試す](#オンラインで試す)
@@ -96,9 +96,7 @@ https://github.com/user-attachments/assets/b2eef5f3-b335-4e71-a755-dc2e80931979
- **クラウドアーキテクチャダイアグラムサポート**クラウドアーキテクチャダイアグラムの生成を専門的にサポートAWS、GCP、Azure - **クラウドアーキテクチャダイアグラムサポート**クラウドアーキテクチャダイアグラムの生成を専門的にサポートAWS、GCP、Azure
- **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成 - **アニメーションコネクタ**:より良い可視化のためにダイアグラム要素間に動的でアニメーション化されたコネクタを作成
## MCPサーバー(プレビュー) ## MCPサーバー
> **プレビュー機能**:この機能は実験的であり、安定しない可能性があります。
MCPModel Context Protocolを介して、Claude Desktop、Cursor、VS CodeなどのAIエージェントでNext AI Draw.ioを使用できます。 MCPModel Context Protocolを介して、Claude Desktop、Cursor、VS CodeなどのAIエージェントでNext AI Draw.ioを使用できます。

View File

@@ -2,14 +2,6 @@
* Type declarations for Electron API exposed via preload script * Type declarations for Electron API exposed via preload script
*/ */
/** Update status data sent from main process */
type UpdateStatusData =
| { status: "available"; version: string }
| { status: "available-manual"; version: string; url: string }
| { status: "downloading"; percent: number }
| { status: "downloaded" }
| { status: "error"; message: string }
/** Configuration preset interface */ /** Configuration preset interface */
interface ConfigPreset { interface ConfigPreset {
id: string id: string
@@ -82,12 +74,6 @@ declare global {
> >
/** Set user's preferred locale */ /** Set user's preferred locale */
setUserLocale: (locale: string) => Promise<SetUserLocaleResult> setUserLocale: (locale: string) => Promise<SetUserLocaleResult>
/** Listen for update status events from main process */
onUpdateStatus: (
callback: (data: UpdateStatusData) => void,
) => () => void
/** Start downloading the available update */
startDownload: () => Promise<void>
} }
/** Settings window Electron API */ /** Settings window Electron API */

View File

@@ -6,7 +6,6 @@ import {
type MenuItemConstructorOptions, type MenuItemConstructorOptions,
shell, shell,
} from "electron" } from "electron"
import { checkForUpdatesManual } from "./auto-updater"
import { import {
applyPresetToEnv, applyPresetToEnv,
getAllPresets, getAllPresets,
@@ -157,11 +156,6 @@ function getMenuTemplate(): MenuItemConstructorOptions[] {
template.push({ template.push({
label: t.help, label: t.help,
submenu: [ submenu: [
{
label: t.checkForUpdates,
click: () => checkForUpdatesManual(),
},
{ type: "separator" },
{ {
label: t.documentation, label: t.documentation,
click: async () => { click: async () => {

View File

@@ -1,282 +0,0 @@
import https from "node:https"
import { app, dialog, ipcMain } from "electron"
import electronUpdater from "electron-updater"
import { getMainWindow } from "./window-manager"
const { autoUpdater } = electronUpdater
const CHECK_INTERVAL = 4 * 60 * 60 * 1000 // 4 hours
const STARTUP_DELAY = 10_000 // 10 seconds
const GITHUB_API_URL =
"https://api.github.com/repos/DayuanJiang/next-ai-draw-io/releases/latest"
let isChecking = false
let updateDownloaded = false
/**
* Whether this platform supports electron-updater auto-update.
* macOS: disabled because builds are ad-hoc signed (no Apple Developer cert).
*/
function supportsAutoUpdate(): boolean {
if (process.platform === "darwin") return false
if (process.platform === "win32" && process.env.PORTABLE_EXECUTABLE_DIR)
return false
if (process.platform === "linux" && !process.env.APPIMAGE) return false
return true
}
/**
* Compare two semver-like version strings numerically.
* Returns true if remote > local.
* Strips prerelease/build metadata (e.g., "1.2.3-beta.1" → "1.2.3").
*/
function isNewerVersion(remote: string, local: string): boolean {
const strip = (v: string) => v.replace(/^v/, "").split("-")[0].split("+")[0]
const r = strip(remote).split(".").map(Number)
const l = strip(local).split(".").map(Number)
const len = Math.max(r.length, l.length)
for (let i = 0; i < len; i++) {
const rv = r[i] || 0
const lv = l[i] || 0
if (rv > lv) return true
if (rv < lv) return false
}
return false
}
/**
* Send update status to the renderer via IPC
*/
function sendStatus(data: Record<string, unknown>) {
const win = getMainWindow()
if (win && !win.isDestroyed()) {
win.webContents.send("update-status", data)
}
}
/**
* Check GitHub API for latest release (used on macOS and Linux DEB)
*/
function checkGitHubRelease(manual: boolean) {
const req = https.get(
GITHUB_API_URL,
{
headers: { "User-Agent": "next-ai-draw-io" },
timeout: 15000,
},
(res) => {
if (res.statusCode !== 200) {
console.error(`GitHub API returned status ${res.statusCode}`)
if (manual) {
dialog.showMessageBox({
type: "error",
title: "Update Check Failed",
message:
"Could not check for updates. Please try again later.",
})
}
isChecking = false
return
}
res.setEncoding("utf8")
let body = ""
res.on("data", (chunk: string) => {
body += chunk
})
res.on("end", () => {
try {
const data = JSON.parse(body)
const remoteVersion = data.tag_name || ""
const localVersion = app.getVersion()
if (isNewerVersion(remoteVersion, localVersion)) {
sendStatus({
status: "available-manual",
version: remoteVersion.replace(/^v/, ""),
url: data.html_url,
})
} else if (manual) {
dialog.showMessageBox({
type: "info",
title: "No Updates",
message: "You're up to date!",
detail: `Version ${localVersion} is the latest version.`,
})
}
} catch (err) {
console.error("Failed to parse GitHub release:", err)
if (manual) {
dialog.showMessageBox({
type: "error",
title: "Update Check Failed",
message:
"Could not check for updates. Please try again later.",
})
}
}
isChecking = false
})
},
)
req.on("timeout", () => {
req.destroy()
isChecking = false
})
req.on("error", (err) => {
console.error("GitHub API request failed:", err)
if (manual) {
dialog.showMessageBox({
type: "error",
title: "Update Check Failed",
message: "Could not check for updates. Please try again later.",
})
}
isChecking = false
})
req.end()
}
/**
* Set up electron-updater event handlers (Windows NSIS / Linux AppImage)
*/
function setupAutoUpdater() {
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.on("update-available", (info) => {
console.log("Update available:", info.version)
sendStatus({ status: "available", version: info.version })
isChecking = false
})
autoUpdater.on("update-not-available", () => {
console.log("No update available")
isChecking = false
})
autoUpdater.on("download-progress", (progress) => {
sendStatus({
status: "downloading",
percent: progress.percent,
})
})
autoUpdater.on("update-downloaded", () => {
updateDownloaded = true
console.log("Update downloaded")
sendStatus({ status: "downloaded" })
showRestartDialog()
})
autoUpdater.on("error", (err) => {
console.error("Auto-update error:", err)
sendStatus({ status: "error", message: String(err) })
isChecking = false
})
// IPC: renderer requests download
ipcMain.handle("updater:start-download", () => {
return autoUpdater.downloadUpdate()
})
}
/**
* Show restart dialog after update downloaded
*/
async function showRestartDialog() {
const result = await dialog.showMessageBox({
type: "info",
buttons: ["Restart Now", "Later"],
defaultId: 0,
cancelId: 1,
title: "Update Ready",
message: "A new version has been downloaded.",
detail: "Restart the app to install the update.",
})
if (result.response === 0) {
autoUpdater.quitAndInstall()
}
}
/**
* Run a single update check
*/
function doCheck(manual: boolean) {
if (isChecking) return
isChecking = true
// If update already downloaded, just re-show restart dialog
if (updateDownloaded && supportsAutoUpdate()) {
isChecking = false
showRestartDialog()
return
}
if (supportsAutoUpdate()) {
// For manual check, register listeners BEFORE triggering check
// to avoid race where event fires before listeners are attached
if (manual) {
const onNotAvailable = () => {
dialog.showMessageBox({
type: "info",
title: "No Updates",
message: "You're up to date!",
detail: `Version ${app.getVersion()} is the latest version.`,
})
cleanup()
}
const onAvailable = () => cleanup()
const onError = () => cleanup()
const cleanup = () => {
autoUpdater.off("update-not-available", onNotAvailable)
autoUpdater.off("update-available", onAvailable)
autoUpdater.off("error", onError)
}
autoUpdater.once("update-not-available", onNotAvailable)
autoUpdater.once("update-available", onAvailable)
autoUpdater.once("error", onError)
}
autoUpdater.checkForUpdates().catch((err) => {
console.error("checkForUpdates failed:", err)
isChecking = false
})
} else {
checkGitHubRelease(manual)
}
}
/**
* Initialize auto-updater. Call once after createWindow().
*/
export function initAutoUpdater() {
if (!app.isPackaged) return
if (supportsAutoUpdate()) {
setupAutoUpdater()
} else {
// Register no-op handler so renderer doesn't get an unhandled error
ipcMain.handle("updater:start-download", () => {})
}
// First check after startup delay
setTimeout(() => doCheck(false), STARTUP_DELAY)
// Periodic checks
setInterval(() => doCheck(false), CHECK_INTERVAL)
}
/**
* Manual update check from menu item
*/
export function checkForUpdatesManual() {
if (!app.isPackaged) {
dialog.showMessageBox({
type: "info",
title: "Development Mode",
message: "Auto-update is not available in development mode.",
})
return
}
doCheck(true)
}

View File

@@ -1,6 +1,5 @@
import { app, BrowserWindow, dialog, shell } from "electron" import { app, BrowserWindow, dialog, shell } from "electron"
import { buildAppMenu } from "./app-menu" import { buildAppMenu } from "./app-menu"
import { initAutoUpdater } from "./auto-updater"
import { getCurrentPresetEnv } from "./config-manager" import { getCurrentPresetEnv } from "./config-manager"
import { loadEnvFile } from "./env-loader" import { loadEnvFile } from "./env-loader"
import { registerIpcHandlers } from "./ipc-handlers" import { registerIpcHandlers } from "./ipc-handlers"
@@ -59,11 +58,6 @@ if (!gotTheLock) {
// Create main window // Create main window
createWindow(serverUrl) createWindow(serverUrl)
// Initialize auto-updater (production only)
if (!isDev) {
initAutoUpdater()
}
} catch (error) { } catch (error) {
console.error("Failed to start application:", error) console.error("Failed to start application:", error)
dialog.showErrorBox( dialog.showErrorBox(

View File

@@ -33,7 +33,6 @@ export interface MenuTranslations {
help: string help: string
documentation: string documentation: string
reportIssue: string reportIssue: string
checkForUpdates: string
} }
const translations: Record<MenuLocale, MenuTranslations> = { const translations: Record<MenuLocale, MenuTranslations> = {
@@ -63,7 +62,6 @@ const translations: Record<MenuLocale, MenuTranslations> = {
help: "Help", help: "Help",
documentation: "Documentation", documentation: "Documentation",
reportIssue: "Report Issue", reportIssue: "Report Issue",
checkForUpdates: "Check for Updates...",
}, },
zh: { zh: {
@@ -92,7 +90,6 @@ const translations: Record<MenuLocale, MenuTranslations> = {
help: "帮助", help: "帮助",
documentation: "文档", documentation: "文档",
reportIssue: "报告问题", reportIssue: "报告问题",
checkForUpdates: "检查更新...",
}, },
ja: { ja: {
@@ -121,7 +118,6 @@ const translations: Record<MenuLocale, MenuTranslations> = {
help: "ヘルプ", help: "ヘルプ",
documentation: "ドキュメント", documentation: "ドキュメント",
reportIssue: "問題を報告", reportIssue: "問題を報告",
checkForUpdates: "アップデートを確認...",
}, },
"zh-Hant": { "zh-Hant": {
@@ -150,7 +146,6 @@ const translations: Record<MenuLocale, MenuTranslations> = {
help: "說明", help: "說明",
documentation: "文件", documentation: "文件",
reportIssue: "回報問題", reportIssue: "回報問題",
checkForUpdates: "檢查更新...",
}, },
} }

View File

@@ -1,12 +1,4 @@
import { contextBridge, type IpcRendererEvent, ipcRenderer } from "electron" import { contextBridge, ipcRenderer } from "electron"
// Locally defined to avoid dependency on electron.d.ts compilation scope
type UpdateStatus =
| { status: "available"; version: string }
| { status: "available-manual"; version: string; url: string }
| { status: "downloading"; percent: number }
| { status: "downloaded" }
| { status: "error"; message: string }
/** /**
* Expose safe APIs to the renderer process * Expose safe APIs to the renderer process
@@ -39,13 +31,4 @@ contextBridge.exposeInMainWorld("electronAPI", {
getUserLocale: () => ipcRenderer.invoke("get-user-locale"), getUserLocale: () => ipcRenderer.invoke("get-user-locale"),
setUserLocale: (locale: string) => setUserLocale: (locale: string) =>
ipcRenderer.invoke("set-user-locale", locale), ipcRenderer.invoke("set-user-locale", locale),
// Auto-update
onUpdateStatus: (callback: (data: UpdateStatus) => void) => {
const handler = (_event: IpcRendererEvent, data: UpdateStatus) =>
callback(data)
ipcRenderer.on("update-status", handler)
return () => ipcRenderer.removeListener("update-status", handler)
},
startDownload: () => ipcRenderer.invoke("updater:start-download"),
}) })

View File

@@ -1,86 +0,0 @@
import { createElement, useEffect, useRef } from "react"
import { toast } from "sonner"
import { UpdateToast } from "@/components/update-toast"
export function useAutoUpdate() {
const downloadToastId = useRef<string | number | null>(null)
useEffect(() => {
const api = (window as Window).electronAPI
if (!api?.onUpdateStatus) return
const cleanup = api.onUpdateStatus((data) => {
switch (data.status) {
case "available":
toast.custom(
(t) =>
createElement(UpdateToast, {
variant: "download",
version: data.version,
onDownload: () => {
toast.dismiss(t)
api.startDownload().catch(() => {
// Error will come through update-status channel
})
},
onDismiss: () => toast.dismiss(t),
}),
{ duration: 15000 },
)
break
case "available-manual":
toast.custom(
(t) =>
createElement(UpdateToast, {
variant: "manual",
version: data.version,
url: data.url,
onDismiss: () => toast.dismiss(t),
}),
{ duration: Number.POSITIVE_INFINITY },
)
break
case "downloading": {
const id =
downloadToastId.current ??
`download-progress-${Date.now()}`
downloadToastId.current = id
toast.custom(
(t) =>
createElement(UpdateToast, {
variant: "downloading",
percent: data.percent,
onDismiss: () => toast.dismiss(t),
}),
{ id, duration: Number.POSITIVE_INFINITY },
)
break
}
case "downloaded":
// Dismiss progress toast — native restart dialog handles the rest
if (downloadToastId.current) {
toast.dismiss(downloadToastId.current)
downloadToastId.current = null
}
break
case "error":
if (downloadToastId.current) {
// Error during active download — inform the user
toast.dismiss(downloadToastId.current)
downloadToastId.current = null
toast.error(
"Update download failed. Please try again later.",
)
}
// Otherwise: silent — background check failure, logged in main process
break
}
})
return cleanup
}, [])
}

View File

@@ -1356,12 +1356,11 @@ export function supportsImageInput(modelId: string): boolean {
} }
// Qwen text models (not vision variants like qwen-vl) // Qwen text models (not vision variants like qwen-vl)
// qwen3.5-plus is a vision model // Qwen3.5 series (qwen3.5, qwen3.5-plus, qwen3.5-flash) natively support image input
if ( if (
lowerModelId.includes("qwen") && lowerModelId.includes("qwen") &&
!hasVisionIndicator && !hasVisionIndicator &&
!lowerModelId.includes("qwen3.5-plus") && !lowerModelId.includes("qwen3.5")
!lowerModelId.includes("qwen3.5-flash")
) { ) {
return false return false
} }

View File

@@ -76,8 +76,7 @@
"creativeDescription": "Draw something fun and creative", "creativeDescription": "Draw something fun and creative",
"cachedNote": "Examples are cached for instant response", "cachedNote": "Examples are cached for instant response",
"mcpServer": "MCP Server", "mcpServer": "MCP Server",
"mcpDescription": "Use in Claude Desktop, VS Code & Cursor", "mcpDescription": "Use in Claude Desktop, VS Code & Cursor"
"preview": "PREVIEW"
}, },
"settings": { "settings": {
"title": "Settings", "title": "Settings",

View File

@@ -76,8 +76,7 @@
"creativeDescription": "楽しくてクリエイティブなものを描く", "creativeDescription": "楽しくてクリエイティブなものを描く",
"cachedNote": "例はキャッシュされ、即座に応答します", "cachedNote": "例はキャッシュされ、即座に応答します",
"mcpServer": "MCP サーバー", "mcpServer": "MCP サーバー",
"mcpDescription": "Claude Desktop、VS Code、Cursor で使用", "mcpDescription": "Claude Desktop、VS Code、Cursor で使用"
"preview": "プレビュー"
}, },
"settings": { "settings": {
"title": "設定", "title": "設定",

View File

@@ -76,8 +76,7 @@
"creativeDescription": "繪製有趣且富有創意的內容", "creativeDescription": "繪製有趣且富有創意的內容",
"cachedNote": "範例已快取,可即時回應", "cachedNote": "範例已快取,可即時回應",
"mcpServer": "MCP 伺服器", "mcpServer": "MCP 伺服器",
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用", "mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用"
"preview": "預覽"
}, },
"settings": { "settings": {
"title": "設定", "title": "設定",

View File

@@ -76,8 +76,7 @@
"creativeDescription": "绘制有趣且富有创意的内容", "creativeDescription": "绘制有趣且富有创意的内容",
"cachedNote": "示例已缓存,可即时响应", "cachedNote": "示例已缓存,可即时响应",
"mcpServer": "MCP 服务器", "mcpServer": "MCP 服务器",
"mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用", "mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用"
"preview": "预览"
}, },
"settings": { "settings": {
"title": "设置", "title": "设置",

118
package-lock.json generated
View File

@@ -48,9 +48,10 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"electron-updater": "^6.8.3",
"idb": "^8.0.3", "idb": "^8.0.3",
"jsonrepair": "^3.13.1", "jsonrepair": "^3.13.1",
"lightningcss": "^1.32.0",
"lightningcss-linux-x64-gnu": "^1.32.0",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",
"motion": "^12.23.25", "motion": "^12.23.25",
"nanoid": "^5.0.0", "nanoid": "^5.0.0",
@@ -2225,6 +2226,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2242,6 +2246,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2259,6 +2266,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2276,6 +2286,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -8246,6 +8259,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -10046,6 +10062,7 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/aria-hidden": { "node_modules/aria-hidden": {
@@ -10681,6 +10698,7 @@
"version": "9.5.1", "version": "9.5.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz",
"integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -12779,69 +12797,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/electron-updater": {
"version": "6.8.3",
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz",
"integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==",
"license": "MIT",
"dependencies": {
"builder-util-runtime": "9.5.1",
"fs-extra": "^10.1.0",
"js-yaml": "^4.1.0",
"lazy-val": "^1.0.5",
"lodash.escaperegexp": "^4.1.2",
"lodash.isequal": "^4.5.0",
"semver": "~7.7.3",
"tiny-typed-emitter": "^2.1.0"
}
},
"node_modules/electron-updater/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/electron-updater/node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/electron-updater/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/electron-updater/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/electron-winstaller": { "node_modules/electron-winstaller": {
"version": "5.4.0", "version": "5.4.0",
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
@@ -14960,6 +14915,7 @@
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/gzip-size": { "node_modules/gzip-size": {
@@ -16192,6 +16148,7 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@@ -16409,6 +16366,7 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/levn": { "node_modules/levn": {
@@ -16609,6 +16567,9 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -16792,6 +16753,9 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -16812,6 +16776,9 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -16832,6 +16799,9 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -17099,19 +17069,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.escaperegexp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
"license": "MIT"
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -21098,6 +21055,7 @@
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
"dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"engines": { "engines": {
"node": ">=11.0.0" "node": ">=11.0.0"
@@ -22517,12 +22475,6 @@
"semver": "bin/semver" "semver": "bin/semver"
} }
}, },
"node_modules/tiny-typed-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
"license": "MIT"
},
"node_modules/tinybench": { "node_modules/tinybench": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",

View File

@@ -70,7 +70,6 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"electron-updater": "^6.8.3",
"idb": "^8.0.3", "idb": "^8.0.3",
"jsonrepair": "^3.13.1", "jsonrepair": "^3.13.1",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@next-ai-drawio/mcp-server", "name": "@next-ai-drawio/mcp-server",
"version": "0.1.17", "version": "0.1.19",
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview", "description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -4,6 +4,29 @@
*/ */
import http from "node:http" import http from "node:http"
const MAX_BODY_BYTES = 10 * 1024 * 1024 // 10 MiB
function readBody(
req: http.IncomingMessage,
res: http.ServerResponse,
cb: (body: string) => void,
): void {
let body = ""
let size = 0
req.on("data", (chunk: Buffer) => {
size += chunk.length
if (size > MAX_BODY_BYTES) {
res.writeHead(413, { "Content-Type": "application/json" })
res.end(JSON.stringify({ error: "Payload too large" }))
req.destroy()
return
}
body += chunk
})
req.on("end", () => cb(body))
}
import { import {
addHistory, addHistory,
clearHistory, clearHistory,
@@ -155,7 +178,7 @@ export function startHttpServer(port = 6002): Promise<number> {
} }
}) })
server.listen(port, () => { server.listen(port, "127.0.0.1", () => {
serverPort = port serverPort = port
log.info(`HTTP server running on http://localhost:${port}`) log.info(`HTTP server running on http://localhost:${port}`)
resolve(port) resolve(port)
@@ -266,11 +289,7 @@ function handleStateApi(
}), }),
) )
} else if (req.method === "POST") { } else if (req.method === "POST") {
let body = "" readBody(req, res, (body) => {
req.on("data", (chunk) => {
body += chunk
})
req.on("end", () => {
try { try {
const data = JSON.parse(body) const data = JSON.parse(body)
const { sessionId } = data const { sessionId } = data
@@ -347,11 +366,7 @@ function handleRestoreApi(
return return
} }
let body = "" readBody(req, res, (body) => {
req.on("data", (chunk) => {
body += chunk
})
req.on("end", () => {
try { try {
const { sessionId, index } = JSON.parse(body) const { sessionId, index } = JSON.parse(body)
if (!sessionId || index === undefined) { if (!sessionId || index === undefined) {
@@ -393,11 +408,7 @@ function handleHistorySvgApi(
return return
} }
let body = "" readBody(req, res, (body) => {
req.on("data", (chunk) => {
body += chunk
})
req.on("end", () => {
try { try {
const { sessionId, svg } = JSON.parse(body) const { sessionId, svg } = JSON.parse(body)
if (!sessionId || !svg) { if (!sessionId || !svg) {

View File

@@ -1,134 +0,0 @@
/**
* Fix latest.yml metadata after Windows code signing.
*
* electron-builder generates latest.yml with hashes of unsigned executables.
* After SignPath signs them, the hashes change. This script reads each
* latest*.yml in release/, finds the matching signed exe in release-signed/,
* and rewrites the sha512 and size fields.
*
* Usage: node scripts/fix-latest-yml.mjs
*/
import { createHash } from "node:crypto"
import {
createReadStream,
existsSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
} from "node:fs"
import { join } from "node:path"
const RELEASE_DIR = "release"
const SIGNED_DIR = "release-signed"
// Verify directories exist
if (!existsSync(RELEASE_DIR)) {
console.error(`Error: ${RELEASE_DIR}/ directory does not exist`)
process.exit(1)
}
if (!existsSync(SIGNED_DIR)) {
console.error(`Error: ${SIGNED_DIR}/ directory does not exist`)
process.exit(1)
}
// Find all latest*.yml files
const ymlFiles = readdirSync(RELEASE_DIR).filter(
(f) => f.startsWith("latest") && f.endsWith(".yml"),
)
if (ymlFiles.length === 0) {
console.log("No latest*.yml files found in release/")
process.exit(0)
}
/**
* Compute SHA-512 hash of a file using streaming (avoids loading large exe into memory)
*/
function hashFile(filePath) {
return new Promise((resolve, reject) => {
const hash = createHash("sha512")
const stream = createReadStream(filePath)
stream.on("data", (chunk) => hash.update(chunk))
stream.on("end", () => resolve(hash.digest("base64")))
stream.on("error", reject)
})
}
// Build a map of signed exe filenames to their hash and size
const signedFiles = new Map()
for (const f of readdirSync(SIGNED_DIR)) {
if (!f.endsWith(".exe")) continue
const filePath = join(SIGNED_DIR, f)
const sha512 = await hashFile(filePath)
const size = statSync(filePath).size
signedFiles.set(f, { sha512, size })
}
if (signedFiles.size === 0) {
console.error("No signed .exe files found in release-signed/")
process.exit(1)
}
console.log(
`Found ${signedFiles.size} signed exe(s):`,
[...signedFiles.keys()].join(", "),
)
for (const ymlFile of ymlFiles) {
const ymlPath = join(RELEASE_DIR, ymlFile)
const content = readFileSync(ymlPath, "utf-8")
const lines = content.split("\n")
const outputLines = []
// Track the current file entry being processed (by url field)
let currentExeName = null
for (const line of lines) {
// Match " url: SomeFile.exe" inside the files array
const urlMatch = line.match(/^\s+url:\s+(.+\.exe)\s*$/)
if (urlMatch) {
currentExeName = urlMatch[1]
outputLines.push(line)
continue
}
// Match top-level "path: SomeFile.exe"
const pathMatch = line.match(/^path:\s+(.+\.exe)\s*$/)
if (pathMatch) {
currentExeName = pathMatch[1]
outputLines.push(line)
continue
}
// Replace sha512 lines
const sha512Match = line.match(/^(\s*)sha512:\s+/)
if (sha512Match && currentExeName && signedFiles.has(currentExeName)) {
const indent = sha512Match[1]
outputLines.push(
`${indent}sha512: ${signedFiles.get(currentExeName).sha512}`,
)
continue
}
// Replace size lines
const sizeMatch = line.match(/^(\s*)size:\s+\d+/)
if (sizeMatch && currentExeName && signedFiles.has(currentExeName)) {
const indent = sizeMatch[1]
outputLines.push(
`${indent}size: ${signedFiles.get(currentExeName).size}`,
)
// Reset current exe after processing both sha512 and size
currentExeName = null
continue
}
outputLines.push(line)
}
writeFileSync(ymlPath, outputLines.join("\n"))
console.log(`Updated ${ymlFile}`)
}
console.log("Done!")

View File

@@ -200,6 +200,8 @@ describe("supportsImageInput", () => {
it("returns true for Qwen vision models", () => { it("returns true for Qwen vision models", () => {
expect(supportsImageInput("qwen-vl")).toBe(true) expect(supportsImageInput("qwen-vl")).toBe(true)
expect(supportsImageInput("Qwen3.5")).toBe(true)
expect(supportsImageInput("qwen3.5")).toBe(true)
expect(supportsImageInput("qwen3.5-plus")).toBe(true) expect(supportsImageInput("qwen3.5-plus")).toBe(true)
expect(supportsImageInput("qwen3.5-flash")).toBe(true) expect(supportsImageInput("qwen3.5-flash")).toBe(true)
expect(supportsImageInput("qwen3-vl-plus")).toBe(true) expect(supportsImageInput("qwen3-vl-plus")).toBe(true)

View File

@@ -35,6 +35,7 @@
"packages", "packages",
"electron", "electron",
"electron-standalone", "electron-standalone",
"dist-electron" "dist-electron",
"release"
] ]
} }