Compare commits

..

1 Commits

Author SHA1 Message Date
dayuan jiang
e7d104671a chore: align biome schema version with CLI latest
Bumps biome.json $schema from 2.4.4 to 2.4.14 so the repo schema
matches the version the 'Auto Format' workflow installs via
@biomejs/biome@latest. Also applies the one auto-fix the newer
version produces (export ordering in electron/electron.d.ts).

Fixes the spurious 'This PR has formatting issues' CI failure that
was blocking fork PRs unrelated to formatting.
2026-05-07 22:36:32 +09:00
12 changed files with 54 additions and 187 deletions

View File

@@ -10,7 +10,6 @@ import {
ResizablePanelGroup, ResizablePanelGroup,
} from "@/components/ui/resizable" } from "@/components/ui/resizable"
import { useDiagram } from "@/contexts/diagram-context" import { useDiagram } from "@/contexts/diagram-context"
import { type DrawioTheme, isDrawioTheme } from "@/lib/drawio-themes"
import { i18n, type Locale } from "@/lib/i18n/config" import { i18n, type Locale } from "@/lib/i18n/config"
import { isIndexedDBUsable } from "@/lib/session-storage" import { isIndexedDBUsable } from "@/lib/session-storage"
@@ -28,7 +27,7 @@ export default function Home() {
const currentLang = (pathname.split("/")[1] || i18n.defaultLocale) as Locale const currentLang = (pathname.split("/")[1] || i18n.defaultLocale) as Locale
const [isMobile, setIsMobile] = useState(false) const [isMobile, setIsMobile] = useState(false)
const [isChatVisible, setIsChatVisible] = useState(true) const [isChatVisible, setIsChatVisible] = useState(true)
const [drawioUi, setDrawioUi] = useState<DrawioTheme>("kennedy") const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
const [darkMode, setDarkMode] = useState(false) const [darkMode, setDarkMode] = useState(false)
const [isLoaded, setIsLoaded] = useState(false) const [isLoaded, setIsLoaded] = useState(false)
const [isDrawioReady, setIsDrawioReady] = useState(false) const [isDrawioReady, setIsDrawioReady] = useState(false)
@@ -57,7 +56,7 @@ export default function Home() {
} }
const savedUi = localStorage.getItem("drawio-theme") const savedUi = localStorage.getItem("drawio-theme")
if (isDrawioTheme(savedUi)) { if (savedUi === "min" || savedUi === "sketch") {
setDrawioUi(savedUi) setDrawioUi(savedUi)
} }
@@ -114,9 +113,10 @@ export default function Home() {
resetDrawioReady() resetDrawioReady()
} }
const handleDrawioUiChange = (theme: DrawioTheme) => { const handleDrawioUiChange = () => {
localStorage.setItem("drawio-theme", theme) const newUi = drawioUi === "min" ? "sketch" : "min"
setDrawioUi(theme) localStorage.setItem("drawio-theme", newUi)
setDrawioUi(newUi)
setIsDrawioReady(false) setIsDrawioReady(false)
resetDrawioReady() resetDrawioReady()
} }
@@ -216,8 +216,7 @@ export default function Home() {
saveAndExit: false, saveAndExit: false,
noSaveBtn: true, noSaveBtn: true,
noExitBtn: true, noExitBtn: true,
dark: dark: darkMode,
darkMode || drawioUi === "dark",
lang: currentLang, lang: currentLang,
// Enable offline mode in Electron to disable external service calls // Enable offline mode in Electron to disable external service calls
...(isElectron && { ...(isElectron && {
@@ -265,7 +264,7 @@ export default function Home() {
isVisible={isChatVisible} isVisible={isChatVisible}
onToggleVisibility={toggleChatPanel} onToggleVisibility={toggleChatPanel}
drawioUi={drawioUi} drawioUi={drawioUi}
onDrawioUiChange={handleDrawioUiChange} onToggleDrawioUi={handleDrawioUiChange}
darkMode={darkMode} darkMode={darkMode}
onToggleDarkMode={handleDarkModeChange} onToggleDarkMode={handleDarkModeChange}
isMobile={isMobile} isMobile={isMobile}

View File

@@ -32,7 +32,6 @@ import { useSessionManager } from "@/hooks/use-session-manager"
import { useValidateDiagram } from "@/hooks/use-validate-diagram" import { useValidateDiagram } from "@/hooks/use-validate-diagram"
import { getApiEndpoint } from "@/lib/base-path" import { getApiEndpoint } from "@/lib/base-path"
import { findCachedResponse } from "@/lib/cached-responses" import { findCachedResponse } from "@/lib/cached-responses"
import type { DrawioTheme } from "@/lib/drawio-themes"
import { formatMessage } from "@/lib/i18n/utils" import { formatMessage } from "@/lib/i18n/utils"
import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
import { sanitizeMessages } from "@/lib/session-storage" import { sanitizeMessages } from "@/lib/session-storage"
@@ -69,8 +68,8 @@ interface ChatMessage {
interface ChatPanelProps { interface ChatPanelProps {
isVisible: boolean isVisible: boolean
onToggleVisibility: () => void onToggleVisibility: () => void
drawioUi: DrawioTheme drawioUi: "min" | "sketch"
onDrawioUiChange: (theme: DrawioTheme) => void onToggleDrawioUi: () => void
darkMode: boolean darkMode: boolean
onToggleDarkMode: () => void onToggleDarkMode: () => void
isMobile?: boolean isMobile?: boolean
@@ -111,7 +110,7 @@ export default function ChatPanel({
isVisible, isVisible,
onToggleVisibility, onToggleVisibility,
drawioUi, drawioUi,
onDrawioUiChange, onToggleDrawioUi,
darkMode, darkMode,
onToggleDarkMode, onToggleDarkMode,
isMobile = false, isMobile = false,
@@ -1443,7 +1442,7 @@ export default function ChatPanel({
open={showSettingsDialog} open={showSettingsDialog}
onOpenChange={setShowSettingsDialog} onOpenChange={setShowSettingsDialog}
drawioUi={drawioUi} drawioUi={drawioUi}
onDrawioUiChange={onDrawioUiChange} onToggleDrawioUi={onToggleDrawioUi}
darkMode={darkMode} darkMode={darkMode}
onToggleDarkMode={onToggleDarkMode} onToggleDarkMode={onToggleDarkMode}
minimalStyle={minimalStyle} minimalStyle={minimalStyle}

View File

@@ -25,7 +25,6 @@ import { Switch } from "@/components/ui/switch"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { useDictionary } from "@/hooks/use-dictionary" import { useDictionary } from "@/hooks/use-dictionary"
import { getApiEndpoint } from "@/lib/base-path" import { getApiEndpoint } from "@/lib/base-path"
import type { DrawioTheme } from "@/lib/drawio-themes"
import { i18n, type Locale } from "@/lib/i18n/config" import { i18n, type Locale } from "@/lib/i18n/config"
import { STORAGE_KEYS } from "@/lib/storage" import { STORAGE_KEYS } from "@/lib/storage"
@@ -64,8 +63,8 @@ const LANGUAGE_LABELS: Record<Locale, string> = {
interface SettingsDialogProps { interface SettingsDialogProps {
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
drawioUi: DrawioTheme drawioUi: "min" | "sketch"
onDrawioUiChange: (theme: DrawioTheme) => void onToggleDrawioUi: () => void
darkMode: boolean darkMode: boolean
onToggleDarkMode: () => void onToggleDarkMode: () => void
minimalStyle?: boolean minimalStyle?: boolean
@@ -91,7 +90,7 @@ function SettingsContent({
open, open,
onOpenChange, onOpenChange,
drawioUi, drawioUi,
onDrawioUiChange, onToggleDrawioUi,
darkMode, darkMode,
onToggleDarkMode, onToggleDarkMode,
minimalStyle = false, minimalStyle = false,
@@ -135,11 +134,8 @@ function SettingsContent({
const [isApplyingProxy, setIsApplyingProxy] = useState(false) const [isApplyingProxy, setIsApplyingProxy] = useState(false)
useEffect(() => { useEffect(() => {
// Re-fetch config whenever the dialog opens to ensure we always show // Only fetch if not cached in localStorage
// the access code input if the server requires it. This fixes the case if (getStoredAccessCodeRequired() !== null) return
// where a stale localStorage cache (from before ACCESS_CODE_LIST was
// configured) would hide the access code input.
if (!open) return
fetch(getApiEndpoint("/api/config")) fetch(getApiEndpoint("/api/config"))
.then((res) => { .then((res) => {
@@ -155,9 +151,10 @@ function SettingsContent({
setAccessCodeRequired(required) setAccessCodeRequired(required)
}) })
.catch(() => { .catch(() => {
// Keep existing cached value on error // Don't cache on error - allow retry on next mount
setAccessCodeRequired(false)
}) })
}, [open]) }, [])
// Detect current language from pathname // Detect current language from pathname
useEffect(() => { useEffect(() => {
@@ -433,40 +430,23 @@ function SettingsContent({
{/* Draw.io Style */} {/* Draw.io Style */}
<SettingItem <SettingItem
label={dict.settings.drawioStyle} label={dict.settings.drawioStyle}
description={dict.settings.drawioStyleDescription} description={`${dict.settings.drawioStyleDescription} ${
drawioUi === "min"
? dict.settings.minimal
: dict.settings.sketch
}`}
> >
<Select <Button
value={drawioUi} id="drawio-ui"
onValueChange={(v) => variant="outline"
onDrawioUiChange(v as DrawioTheme) onClick={onToggleDrawioUi}
} className="h-9 w-[120px] rounded-xl border-border-subtle hover:bg-interactive-hover font-normal"
> >
<SelectTrigger {dict.settings.switchTo}{" "}
id="drawio-ui-select" {drawioUi === "min"
aria-label={dict.settings.drawioStyle} ? dict.settings.sketch
className="w-[120px] h-9 rounded-xl" : dict.settings.minimal}
> </Button>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="kennedy">
{dict.settings.themeDefault}
</SelectItem>
<SelectItem value="atlas">Atlas</SelectItem>
<SelectItem value="dark">
{dict.settings.themeDark}
</SelectItem>
<SelectItem value="min">
{dict.settings.themeMinimal}
</SelectItem>
<SelectItem value="sketch">
{dict.settings.themeSketch}
</SelectItem>
<SelectItem value="simple">
{dict.settings.themeSimple}
</SelectItem>
</SelectContent>
</Select>
</SettingItem> </SettingItem>
{/* Diagram Style */} {/* Diagram Style */}

View File

@@ -60,13 +60,6 @@ export function createWindow(serverUrl: string): BrowserWindow {
mainWindow.webContents.openDevTools() mainWindow.webContents.openDevTools()
} }
// Override the draw.io iframe's beforeunload handler so the window can
// close after the user edits text in a shape (fixes #815). Diagrams are
// already persisted via autosave, so the prompt is unnecessary.
mainWindow.webContents.on("will-prevent-unload", (event) => {
event.preventDefault()
})
mainWindow.on("closed", () => { mainWindow.on("closed", () => {
mainWindow = null mainWindow = null
}) })

View File

@@ -1262,6 +1262,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "glm": case "glm":
case "qwen": case "qwen":
case "qiniu": case "qiniu":
case "kimi":
case "novita": { case "novita": {
const envVar = PROVIDER_ENV_VARS[provider] const envVar = PROVIDER_ENV_VARS[provider]
if (!envVar) { if (!envVar) {
@@ -1287,23 +1288,6 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
break break
} }
case "kimi": {
const apiKey = resolveApiKey(overrides, "KIMI_API_KEY")
const baseURL = resolveBaseURL(
overrides?.apiKey,
overrides?.baseUrl,
resolveBaseUrlEnv(overrides, "KIMI_BASE_URL"),
PROVIDER_INFO["kimi"]?.defaultBaseUrl,
)
// Use createDeepSeek to properly handle reasoning_content for Kimi
// thinking models (e.g., kimi-k2.6). Kimi's API uses the same
// reasoning_content field as DeepSeek, so this provider correctly
// captures and replays reasoning in multi-turn conversations.
const customProvider = createDeepSeek({ apiKey, baseURL })
model = customProvider(modelId)
break
}
default: default:
throw new Error( throw new Error(
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope, glm, qwen, qiniu, kimi, minimax, novita`, `Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope, glm, qwen, qiniu, kimi, minimax, novita`,

View File

@@ -1,17 +0,0 @@
export const DRAWIO_THEMES = [
"kennedy",
"atlas",
"dark",
"min",
"sketch",
"simple",
] as const
export type DrawioTheme = (typeof DRAWIO_THEMES)[number]
export function isDrawioTheme(value: unknown): value is DrawioTheme {
return (
typeof value === "string" &&
(DRAWIO_THEMES as readonly string[]).includes(value)
)
}

View File

@@ -102,12 +102,10 @@
"theme": "Theme", "theme": "Theme",
"themeDescription": "Dark/Light mode for interface and DrawIO canvas.", "themeDescription": "Dark/Light mode for interface and DrawIO canvas.",
"drawioStyle": "DrawIO Style", "drawioStyle": "DrawIO Style",
"drawioStyleDescription": "Canvas style", "drawioStyleDescription": "Canvas style:",
"themeDefault": "Default", "switchTo": "Switch to",
"themeDark": "Dark", "minimal": "Minimal",
"themeMinimal": "Minimal", "sketch": "Sketch",
"themeSketch": "Sketch",
"themeSimple": "Simple",
"diagramStyle": "Diagram Style", "diagramStyle": "Diagram Style",
"diagramStyleDescription": "Toggle between minimal and styled diagram output.", "diagramStyleDescription": "Toggle between minimal and styled diagram output.",
"sendShortcut": "Send Shortcut", "sendShortcut": "Send Shortcut",

View File

@@ -102,12 +102,10 @@
"theme": "テーマ", "theme": "テーマ",
"themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。", "themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。",
"drawioStyle": "DrawIO スタイル", "drawioStyle": "DrawIO スタイル",
"drawioStyleDescription": "キャンバススタイル", "drawioStyleDescription": "キャンバススタイル",
"themeDefault": "デフォルト", "switchTo": "切り替え",
"themeDark": "ダーク", "minimal": "ミニマル",
"themeMinimal": "ミニマル", "sketch": "スケッチ",
"themeSketch": "スケッチ",
"themeSimple": "シンプル",
"diagramStyle": "ダイアグラムスタイル", "diagramStyle": "ダイアグラムスタイル",
"diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。", "diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。",
"sendShortcut": "送信ショートカット", "sendShortcut": "送信ショートカット",

View File

@@ -102,12 +102,10 @@
"theme": "主題", "theme": "主題",
"themeDescription": "介面和 DrawIO 畫布的深色/淺色模式。", "themeDescription": "介面和 DrawIO 畫布的深色/淺色模式。",
"drawioStyle": "DrawIO 樣式", "drawioStyle": "DrawIO 樣式",
"drawioStyleDescription": "畫布樣式", "drawioStyleDescription": "畫布樣式",
"themeDefault": "預設", "switchTo": "切換到",
"themeDark": "深色", "minimal": "簡約",
"themeMinimal": "簡約", "sketch": "草圖",
"themeSketch": "草圖",
"themeSimple": "簡單",
"diagramStyle": "圖表樣式", "diagramStyle": "圖表樣式",
"diagramStyleDescription": "切換簡約與精緻圖表輸出模式。", "diagramStyleDescription": "切換簡約與精緻圖表輸出模式。",
"sendShortcut": "傳送快捷鍵", "sendShortcut": "傳送快捷鍵",

View File

@@ -102,12 +102,10 @@
"theme": "主题", "theme": "主题",
"themeDescription": "界面和 DrawIO 画布的深色/浅色模式。", "themeDescription": "界面和 DrawIO 画布的深色/浅色模式。",
"drawioStyle": "DrawIO 样式", "drawioStyle": "DrawIO 样式",
"drawioStyleDescription": "画布样式", "drawioStyleDescription": "画布样式",
"themeDefault": "默认", "switchTo": "切换到",
"themeDark": "深色", "minimal": "简约",
"themeMinimal": "简约", "sketch": "草图",
"themeSketch": "草图",
"themeSimple": "简单",
"diagramStyle": "图表样式", "diagramStyle": "图表样式",
"diagramStyleDescription": "切换简约与精致图表输出模式。", "diagramStyleDescription": "切换简约与精致图表输出模式。",
"sendShortcut": "发送快捷键", "sendShortcut": "发送快捷键",

View File

@@ -35,10 +35,8 @@ test.describe("Iframe Interaction", () => {
await expect( await expect(
frame frame
.locator('text="Diagram"') .locator('text="Diagram"')
.or(frame.locator('[title*="Diagram"]')) .or(frame.locator('[title*="Diagram"]')),
.filter({ visible: true }) ).toBeVisible({ timeout: 10000 })
.first(),
).toBeVisible({ timeout: 30000 })
}) })
test("diagram XML is rendered in iframe after generation", async ({ test("diagram XML is rendered in iframe after generation", async ({

View File

@@ -245,67 +245,6 @@ vi.mock("ollama-ai-provider-v2", () => {
return { createOllama: mockCreateOllama, ollama: mockOllama } return { createOllama: mockCreateOllama, ollama: mockOllama }
}) })
vi.mock("@ai-sdk/deepseek", () => {
const mockModel = { modelId: "test-model" }
const mockProviderFn = vi.fn(() => mockModel)
const mockCreateDeepSeek = vi.fn(() => mockProviderFn)
const mockDeepseek = vi.fn(() => mockModel)
return { createDeepSeek: mockCreateDeepSeek, deepseek: mockDeepseek }
})
describe("Kimi provider uses createDeepSeek for reasoning_content support", () => {
let createDeepSeekMock: ReturnType<typeof vi.fn>
const savedEnv: Record<string, string | undefined> = {}
beforeEach(async () => {
savedEnv.KIMI_API_KEY = process.env.KIMI_API_KEY
savedEnv.KIMI_BASE_URL = process.env.KIMI_BASE_URL
delete process.env.KIMI_BASE_URL
const mod = await import("@ai-sdk/deepseek")
createDeepSeekMock = mod.createDeepSeek as ReturnType<typeof vi.fn>
createDeepSeekMock.mockClear()
})
afterEach(() => {
process.env.KIMI_API_KEY = savedEnv.KIMI_API_KEY
process.env.KIMI_BASE_URL = savedEnv.KIMI_BASE_URL
})
it("uses createDeepSeek with Kimi default base URL for reasoning_content support", () => {
process.env.KIMI_API_KEY = "test-kimi-key"
getAIModel({
provider: "kimi",
apiKey: "test-kimi-key",
modelId: "moonshot-v1-8k",
})
expect(createDeepSeekMock).toHaveBeenCalledWith(
expect.objectContaining({
baseURL: "https://api.moonshot.cn/v1",
}),
)
})
it("uses custom base URL when provided for kimi provider", () => {
process.env.KIMI_API_KEY = "test-kimi-key"
getAIModel({
provider: "kimi",
apiKey: "test-kimi-key",
baseUrl: "https://custom-kimi-endpoint.com/v1",
modelId: "kimi-k2.6",
})
expect(createDeepSeekMock).toHaveBeenCalledWith(
expect.objectContaining({
baseURL: "https://custom-kimi-endpoint.com/v1",
}),
)
})
})
describe("Ollama API key security", () => { describe("Ollama API key security", () => {
let createOllamaMock: ReturnType<typeof vi.fn> let createOllamaMock: ReturnType<typeof vi.fn>
const savedEnv: Record<string, string | undefined> = {} const savedEnv: Record<string, string | undefined> = {}