初始化web服务功能

This commit is contained in:
xintaofei
2026-03-25 14:26:26 +08:00
parent ae70f17d2e
commit ac09d3db9e
99 changed files with 3253 additions and 304 deletions

View File

@@ -1,7 +1,7 @@
"use client"
import { useCallback, useMemo, useState } from "react"
import { openUrl } from "@tauri-apps/plugin-opener"
import { openUrl } from "@/lib/platform"
import type { LinkSafetyConfig, LinkSafetyModalProps } from "streamdown"
import { toast } from "sonner"
import { useFolderContext } from "@/contexts/folder-context"

View File

@@ -2,7 +2,7 @@
import { useCallback, useEffect, useRef, useState } from "react"
import { useTranslations } from "next-intl"
import { acpListAgents } from "@/lib/tauri"
import { acpListAgents } from "@/lib/api"
import { disposeTauriListener } from "@/lib/tauri-listener"
import type { AgentType, AcpAgentInfo } from "@/lib/types"
import { AGENT_LABELS } from "@/lib/types"

View File

@@ -1,9 +1,7 @@
"use client"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { TauriEvent } from "@tauri-apps/api/event"
import { getCurrentWebview } from "@tauri-apps/api/webview"
import { open } from "@tauri-apps/plugin-dialog"
import { isDesktop } from "@/lib/platform"
import Image from "next/image"
import { useTranslations } from "next-intl"
import { Button } from "@/components/ui/button"
@@ -34,7 +32,8 @@ import {
import { cn } from "@/lib/utils"
import { matchShortcutEvent } from "@/lib/keyboard-shortcuts"
import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
import { readFileBase64 } from "@/lib/tauri"
import { readFileBase64 } from "@/lib/api"
import { openFileDialog } from "@/lib/platform"
import { disposeTauriListener } from "@/lib/tauri-listener"
import type {
AvailableCommandInfo,
@@ -751,10 +750,9 @@ export function MessageInput({
const handlePickFiles = useCallback(async () => {
if (disabled) return
try {
const selected = await open({
const selected = await openFileDialog({
multiple: true,
directory: false,
defaultPath: defaultPath || undefined,
})
if (!selected) return
const picked = Array.isArray(selected) ? selected : [selected]
@@ -846,6 +844,9 @@ export function MessageInput({
}
const setup = async () => {
if (!isDesktop()) return
const { getCurrentWebview } = await import("@tauri-apps/api/webview")
const { TauriEvent } = await import("@tauri-apps/api/event")
const webview = getCurrentWebview()
try {
const unlistenEnter = await webview.listen<{

View File

@@ -31,7 +31,7 @@ import {
updateConversationExternalId,
updateConversationStatus,
updateConversationTitle,
} from "@/lib/tauri"
} from "@/lib/api"
import { useConversationRuntime } from "@/contexts/conversation-runtime-context"
import { useConversationDetail } from "@/hooks/use-conversation-detail"
import {

View File

@@ -14,7 +14,7 @@ import {
getFileTree,
listFolderConversations,
readFilePreview,
} from "@/lib/tauri"
} from "@/lib/api"
import type {
AgentType,
ConversationStatus,

View File

@@ -22,7 +22,7 @@ import {
updateConversationTitle,
updateConversationStatus,
deleteConversation,
} from "@/lib/tauri"
} from "@/lib/api"
import type { ConversationStatus, DbConversationSummary } from "@/lib/types"
import { STATUS_ORDER, STATUS_COLORS } from "@/lib/types"
import { SidebarConversationCard } from "./sidebar-conversation-card"

View File

@@ -21,7 +21,7 @@ import { code } from "@streamdown/code"
import { math } from "@streamdown/math"
import { mermaid } from "@streamdown/mermaid"
import { Streamdown } from "streamdown"
import { readFileBase64 } from "@/lib/tauri"
import { readFileBase64 } from "@/lib/api"
import { defineMonacoThemes, useMonacoThemeSync } from "@/lib/monaco-themes"
import "@/lib/monaco-local"

View File

@@ -4,7 +4,7 @@ import { useCallback, useEffect, useRef, useState } from "react"
import { Reorder } from "motion/react"
import { Code, Eye, ExternalLink, FileText, GitCompare, X } from "lucide-react"
import { useTranslations } from "next-intl"
import { openPath } from "@tauri-apps/plugin-opener"
import { openPath } from "@/lib/platform"
import { useFolderContext } from "@/contexts/folder-context"
import { useWorkspaceContext } from "@/contexts/workspace-context"
import { useShortcutSettings } from "@/hooks/use-shortcut-settings"

View File

@@ -22,7 +22,7 @@ import {
toIntlLocale,
type IntlLocale,
} from "@/lib/i18n"
import { getSystemLanguageSettings } from "@/lib/tauri"
import { getSystemLanguageSettings } from "@/lib/api"
import { disposeTauriListener } from "@/lib/tauri-listener"
import { AppBootLoading } from "@/components/layout/app-boot-loading"
import type { AppLocale, SystemLanguageSettings } from "@/lib/types"

View File

@@ -8,8 +8,7 @@ import {
useState,
type ReactNode,
} from "react"
import { listen, type UnlistenFn } from "@tauri-apps/api/event"
import { revealItemInDir } from "@tauri-apps/plugin-opener"
import { revealItemInDir, subscribe } from "@/lib/platform"
import ignore from "ignore"
import { Check, ChevronRight } from "lucide-react"
import { useTranslations } from "next-intl"
@@ -36,8 +35,7 @@ import {
saveFileCopy,
startFileTreeWatch,
stopFileTreeWatch,
} from "@/lib/tauri"
import { disposeTauriListener } from "@/lib/tauri-listener"
} from "@/lib/api"
import {
emitAttachFileToSession,
emitAppendTextToSession,
@@ -1907,7 +1905,7 @@ export function FileTreeTab() {
const rootPath = folder?.path
if (!rootPath) return
let unlisten: UnlistenFn | null = null
let unlisten: (() => void) | null = null
const normalizedRootPath = normalizeComparePath(rootPath)
const scheduleTreeRefresh = (refreshGitStatus: boolean) => {
@@ -2046,20 +2044,17 @@ export function FileTreeTab() {
}
try {
unlisten = await listen<FileTreeChangedEvent>(
unlisten = await subscribe<FileTreeChangedEvent>(
"folder://file-tree-changed",
(event) => {
(payload) => {
if (
normalizeComparePath(event.payload.root_path) !==
normalizedRootPath
normalizeComparePath(payload.root_path) !== normalizedRootPath
) {
return
}
const changedPaths =
event.payload.changed_paths.map(normalizeComparePath)
const shouldRefreshGitStatus =
event.payload.refresh_git_status ?? true
const changedPaths = payload.changed_paths.map(normalizeComparePath)
const shouldRefreshGitStatus = payload.refresh_git_status ?? true
const nonGitChangedPaths = changedPaths.filter(
(path) => !isGitMetadataPath(path)
)
@@ -2069,13 +2064,13 @@ export function FileTreeTab() {
(path) => !filePathSetRef.current.has(path)
)
const needsTreeRefresh =
event.payload.full_reload ||
payload.full_reload ||
(!onlyGitMetadataChanges &&
(event.payload.kind !== "modify" ||
(payload.kind !== "modify" ||
nonGitChangedPaths.length === 0 ||
hasUnknownPath))
if (onlyGitMetadataChanges && !event.payload.full_reload) {
if (onlyGitMetadataChanges && !payload.full_reload) {
if (shouldRefreshGitStatus) {
scheduleStatusRefresh()
}
@@ -2085,13 +2080,13 @@ export function FileTreeTab() {
scheduleStatusRefresh()
}
if (onlyGitMetadataChanges && !event.payload.full_reload) {
if (onlyGitMetadataChanges && !payload.full_reload) {
return
}
const changedActivePath = getActiveChangedFilePath(
nonGitChangedPaths,
event.payload.full_reload
payload.full_reload
)
if (!changedActivePath) return
@@ -2145,7 +2140,7 @@ export function FileTreeTab() {
pendingTreeRefreshRef.current = false
pendingTreeRefreshNeedsStatusRef.current = false
pendingStatusRefreshRef.current = false
disposeTauriListener(unlisten, "AuxPanelFileTree.fileTreeChanged")
unlisten?.()
void stopFileTreeWatch(rootPath)
}
}, [fetchTree, folder?.path, openFilePreview, t])

View File

@@ -8,7 +8,7 @@ import {
useRef,
useState,
} from "react"
import { listen, type UnlistenFn } from "@tauri-apps/api/event"
import { subscribe } from "@/lib/platform"
import { ChevronsDownUp, ChevronsUpDown } from "lucide-react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
@@ -46,8 +46,7 @@ import {
openCommitWindow,
startFileTreeWatch,
stopFileTreeWatch,
} from "@/lib/tauri"
import { disposeTauriListener } from "@/lib/tauri-listener"
} from "@/lib/api"
import type { FileTreeChangedEvent, GitStatusEntry } from "@/lib/types"
import {
AlertDialog,
@@ -611,7 +610,7 @@ export function GitChangesTab() {
const rootPath = folder?.path
if (!rootPath || !isChangesTabActive) return
let unlisten: UnlistenFn | null = null
let unlisten: (() => void) | null = null
const normalizedRootPath = normalizeComparePath(rootPath)
const scheduleRefresh = () => {
@@ -631,16 +630,15 @@ export function GitChangesTab() {
}
try {
unlisten = await listen<FileTreeChangedEvent>(
unlisten = await subscribe<FileTreeChangedEvent>(
"folder://file-tree-changed",
(event) => {
(payload) => {
if (
normalizeComparePath(event.payload.root_path) !==
normalizedRootPath
normalizeComparePath(payload.root_path) !== normalizedRootPath
) {
return
}
if (!shouldRefreshFromEvent(event.payload)) return
if (!shouldRefreshFromEvent(payload)) return
scheduleRefresh()
}
)
@@ -656,7 +654,7 @@ export function GitChangesTab() {
clearTimeout(refreshTimerRef.current)
refreshTimerRef.current = null
}
disposeTauriListener(unlisten, "AuxPanelGitChanges.fileTreeChanged")
unlisten?.()
void stopFileTreeWatch(rootPath)
}
}, [fetchChanges, folder?.path, isChangesTabActive])

View File

@@ -75,8 +75,7 @@ import {
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import { Skeleton } from "@/components/ui/skeleton"
import { listen, type UnlistenFn } from "@tauri-apps/api/event"
import { disposeTauriListener } from "@/lib/tauri-listener"
import { subscribe } from "@/lib/platform"
import { useFolderContext } from "@/contexts/folder-context"
import { useWorkspaceContext } from "@/contexts/workspace-context"
import {
@@ -86,7 +85,7 @@ import {
gitLog,
gitNewBranch,
openPushWindow,
} from "@/lib/tauri"
} from "@/lib/api"
import type { GitBranchList, GitLogEntry, GitLogFileChange } from "@/lib/types"
import { toast } from "sonner"
import { toErrorMessage } from "@/lib/app-error"
@@ -874,11 +873,11 @@ export function GitLogTab() {
"folder://git-push-succeeded",
] as const
const unlistens: (UnlistenFn | null)[] = events.map(() => null)
const unlistens: ((() => void) | null)[] = events.map(() => null)
events.forEach((eventName, i) => {
listen<{ folder_id: number }>(eventName, (event) => {
if (event.payload.folder_id !== folder.id) return
subscribe<{ folder_id: number }>(eventName, (payload) => {
if (payload.folder_id !== folder.id) return
void refreshBranches()
void fetchLog({ inline: true })
})
@@ -891,8 +890,8 @@ export function GitLogTab() {
})
return () => {
events.forEach((eventName, i) => {
disposeTauriListener(unlistens[i], `GitLogTab.${eventName}`)
events.forEach((_eventName, i) => {
unlistens[i]?.()
})
}
}, [folder, refreshBranches, fetchLog])

View File

@@ -1,7 +1,13 @@
"use client"
import { useState, useRef, useCallback, useMemo, useEffect } from "react"
import { emit, listen, type UnlistenFn } from "@tauri-apps/api/event"
const emitEvent = async (event: string, payload?: unknown) => {
try {
const { emit } = await import("@tauri-apps/api/event")
await emit(event, payload)
} catch { /* not in Tauri */ }
}
import { openFileDialog, subscribe } from "@/lib/platform"
import {
GitBranch,
ChevronDown,
@@ -62,7 +68,6 @@ import { useTranslations } from "next-intl"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { open } from "@tauri-apps/plugin-dialog"
import {
gitInit,
gitPull,
@@ -79,11 +84,10 @@ import {
setFolderParentBranch,
openStashWindow,
openPushWindow,
} from "@/lib/tauri"
} from "@/lib/api"
import { RemoteManageDialog } from "@/components/layout/remote-manage-dialog"
import { ConflictDialog } from "@/components/layout/conflict-dialog"
import { StashDialog } from "@/components/layout/stash-dialog"
import { disposeTauriListener } from "@/lib/tauri-listener"
import { toErrorMessage } from "@/lib/app-error"
import type { GitBranchList, GitConflictInfo } from "@/lib/types"
import { toast } from "sonner"
@@ -167,15 +171,15 @@ export function BranchDropdown({
useEffect(() => {
if (!folder) return
let unlisten: UnlistenFn | null = null
let unlisten: (() => void) | null = null
listen<GitCommitSucceededEventPayload>(
subscribe<GitCommitSucceededEventPayload>(
"folder://git-commit-succeeded",
(event) => {
if (event.payload.folder_id !== folder.id) return
(payload) => {
if (payload.folder_id !== folder.id) return
toast.success(t("toasts.commitCodeCompleted"), {
description: t("toasts.committedFiles", {
count: event.payload.committed_files,
count: payload.committed_files,
}),
})
onBranchChange()
@@ -189,20 +193,20 @@ export function BranchDropdown({
})
return () => {
disposeTauriListener(unlisten, "BranchDropdown.gitCommitSucceeded")
unlisten?.()
}
}, [folder, onBranchChange, t])
useEffect(() => {
if (!folder) return
let unlisten: UnlistenFn | null = null
let unlisten: (() => void) | null = null
listen<GitPushSucceededEventPayload>(
subscribe<GitPushSucceededEventPayload>(
"folder://git-push-succeeded",
(event) => {
if (event.payload.folder_id !== folder.id) return
const { pushed_commits, upstream_set } = event.payload
(payload) => {
if (payload.folder_id !== folder.id) return
const { pushed_commits, upstream_set } = payload
let description: string
if (upstream_set) {
description =
@@ -226,7 +230,7 @@ export function BranchDropdown({
})
return () => {
disposeTauriListener(unlisten, "BranchDropdown.gitPushSucceeded")
unlisten?.()
}
}, [folder, onBranchChange, t])
@@ -245,7 +249,7 @@ export function BranchDropdown({
const successDescription = getSuccessDescription?.(result)
updateTask(taskId, { status: "completed" })
onBranchChange()
void emit("folder://git-branch-changed", {
void emitEvent("folder://git-branch-changed", {
folder_id: folder?.id,
})
if (successDescription !== false) {
@@ -326,9 +330,11 @@ export function BranchDropdown({
}
async function handleBrowseWorktreePath() {
const selected = await open({ directory: true, multiple: false })
const selected = await openFileDialog({ directory: true, multiple: false })
if (selected) {
setWorktreePath(selected)
setWorktreePath(
Array.isArray(selected) ? selected[0] : selected,
)
}
}

View File

@@ -1,6 +1,6 @@
"use client"
import { listen, type UnlistenFn } from "@tauri-apps/api/event"
import { subscribe } from "@/lib/platform"
import { useState, useEffect, useCallback, useMemo, useRef } from "react"
import { ChevronDown, Play, Plus, Square } from "lucide-react"
import { useTranslations } from "next-intl"
@@ -19,8 +19,7 @@ import {
listFolderCommands,
terminalKill,
terminalList,
} from "@/lib/tauri"
import { disposeTauriListener } from "@/lib/tauri-listener"
} from "@/lib/api"
import type { FolderCommand, TerminalEvent } from "@/lib/types"
import { CommandManageDialog } from "./command-manage-dialog"
@@ -54,7 +53,7 @@ export function CommandDropdown() {
const [runningCommandTerminals, setRunningCommandTerminals] = useState<
Record<number, string>
>({})
const exitUnlistenersRef = useRef<Map<string, UnlistenFn>>(new Map())
const exitUnlistenersRef = useRef<Map<string, () => void>>(new Map())
const runningCommandTerminalsRef = useRef<Record<number, string>>({})
const folderId = folder?.id ?? 0
@@ -67,7 +66,7 @@ export function CommandDropdown() {
const clearRunningByTerminalId = useCallback((terminalId: string) => {
const unlisten = exitUnlistenersRef.current.get(terminalId)
if (unlisten) {
disposeTauriListener(unlisten, "CommandDropdown.terminalExit")
unlisten()
exitUnlistenersRef.current.delete(terminalId)
}
@@ -86,7 +85,7 @@ export function CommandDropdown() {
const clearAllRunningStates = useCallback(() => {
for (const unlisten of exitUnlistenersRef.current.values()) {
disposeTauriListener(unlisten, "CommandDropdown.terminalExit")
unlisten()
}
exitUnlistenersRef.current.clear()
setRunningCommandTerminals({})
@@ -165,7 +164,7 @@ export function CommandDropdown() {
async (terminalId: string) => {
if (exitUnlistenersRef.current.has(terminalId)) return
try {
const unlisten = await listen<TerminalEvent>(
const unlisten = await subscribe<TerminalEvent>(
`terminal://exit/${terminalId}`,
() => {
clearRunningByTerminalId(terminalId)

View File

@@ -18,7 +18,7 @@ import {
createFolderCommand,
updateFolderCommand,
deleteFolderCommand,
} from "@/lib/tauri"
} from "@/lib/api"
interface CommandDraft {
id: number | null

View File

@@ -41,7 +41,7 @@ import {
gitStatus,
deleteFileTreeEntry,
readFilePreview,
} from "@/lib/tauri"
} from "@/lib/api"
import type { GitStatusEntry } from "@/lib/types"
import { cn } from "@/lib/utils"
import { toast } from "sonner"

View File

@@ -1,7 +1,7 @@
"use client"
import { useCallback, useEffect, useState } from "react"
import { listen, type UnlistenFn } from "@tauri-apps/api/event"
import { subscribe } from "@/lib/platform"
import { AlertTriangle, Check, FileWarning, Loader2 } from "lucide-react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
@@ -20,8 +20,7 @@ import {
gitAbortOperation,
gitContinueOperation,
openMergeWindow,
} from "@/lib/tauri"
import { disposeTauriListener } from "@/lib/tauri-listener"
} from "@/lib/api"
import type { GitConflictInfo } from "@/lib/types"
interface ConflictDialogProps {
@@ -76,15 +75,15 @@ export function ConflictDialog({
useEffect(() => {
if (!open) return
let unlistenResolved: UnlistenFn | null = null
let unlistenCompleted: UnlistenFn | null = null
let unlistenAborted: UnlistenFn | null = null
let unlistenResolved: (() => void) | null = null
let unlistenCompleted: (() => void) | null = null
let unlistenAborted: (() => void) | null = null
listen<{ folder_id: number; file: string }>(
subscribe<{ folder_id: number; file: string }>(
"folder://merge-conflict-resolved",
(event) => {
if (event.payload.folder_id !== folderId) return
setResolvedFiles((prev) => new Set([...prev, event.payload.file]))
(payload) => {
if (payload.folder_id !== folderId) return
setResolvedFiles((prev) => new Set([...prev, payload.file]))
}
)
.then((fn) => {
@@ -92,8 +91,8 @@ export function ConflictDialog({
})
.catch(() => {})
listen<{ folder_id: number }>("folder://merge-completed", (event) => {
if (event.payload.folder_id !== folderId) return
subscribe<{ folder_id: number }>("folder://merge-completed", (payload) => {
if (payload.folder_id !== folderId) return
setDone(true)
onResolved()
onClose()
@@ -105,8 +104,8 @@ export function ConflictDialog({
// Merge was aborted (user clicked abort in merge window, or window closed)
// Reset resolved state since abort reverts all changes
listen<{ folder_id: number }>("folder://merge-aborted", (event) => {
if (event.payload.folder_id !== folderId) return
subscribe<{ folder_id: number }>("folder://merge-aborted", (payload) => {
if (payload.folder_id !== folderId) return
setDone(true)
setResolvedFiles(new Set())
onClose()
@@ -117,12 +116,9 @@ export function ConflictDialog({
.catch(() => {})
return () => {
disposeTauriListener(
unlistenResolved,
"ConflictDialog.mergeConflictResolved"
)
disposeTauriListener(unlistenCompleted, "ConflictDialog.mergeCompleted")
disposeTauriListener(unlistenAborted, "ConflictDialog.mergeAborted")
unlistenResolved?.()
unlistenCompleted?.()
unlistenAborted?.()
}
}, [open, folderId, onResolved, onClose])

View File

@@ -2,7 +2,6 @@
import { useState } from "react"
import { ChevronDown, Folder, FolderOpen, GitBranch } from "lucide-react"
import { open } from "@tauri-apps/plugin-dialog"
import { useTranslations } from "next-intl"
import {
DropdownMenu,
@@ -17,7 +16,8 @@ import {
listOpenFolders,
loadFolderHistory,
openFolderWindow,
} from "@/lib/tauri"
} from "@/lib/api"
import { openFileDialog } from "@/lib/platform"
import { useFolderContext } from "@/contexts/folder-context"
import { CloneDialog } from "@/components/welcome/clone-dialog"
import type { FolderHistoryEntry } from "@/lib/types"
@@ -50,9 +50,11 @@ export function FolderNameDropdown() {
}
async function handleOpenFolder() {
const selected = await open({ directory: true, multiple: false })
const selected = await openFileDialog({ directory: true, multiple: false })
if (selected) {
await openFolderWindow(selected)
await openFolderWindow(
Array.isArray(selected) ? selected[0] : selected,
)
}
}

View File

@@ -7,7 +7,6 @@ import {
useState,
type KeyboardEvent as ReactKeyboardEvent,
} from "react"
import { open } from "@tauri-apps/plugin-dialog"
import {
Columns2,
FileCode2,
@@ -19,7 +18,8 @@ import {
Settings,
} from "lucide-react"
import { useTranslations } from "next-intl"
import { getGitBranch, openFolderWindow, openSettingsWindow } from "@/lib/tauri"
import { getGitBranch, openFolderWindow, openSettingsWindow } from "@/lib/api"
import { openFileDialog } from "@/lib/platform"
import { useFolderContext } from "@/contexts/folder-context"
import { Button } from "@/components/ui/button"
import { useSidebarContext } from "@/contexts/sidebar-context"
@@ -79,8 +79,9 @@ export function FolderTitleBar() {
const handleOpenFolder = useCallback(async () => {
try {
const selected = await open({ directory: true, multiple: false })
if (!selected) return
const result = await openFileDialog({ directory: true, multiple: false })
if (!result) return
const selected = Array.isArray(result) ? result[0] : result
await openFolderWindow(selected)
} catch (err) {
console.error("[FolderTitleBar] failed to open folder:", err)

View File

@@ -51,7 +51,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { gitLog, gitPush, gitPushInfo, gitShowFile } from "@/lib/tauri"
import { gitLog, gitPush, gitPushInfo, gitShowFile } from "@/lib/api"
import { toErrorMessage } from "@/lib/app-error"
import { languageFromPath } from "@/lib/language-detect"
import type { GitLogEntry, GitLogFileChange, GitPushInfo } from "@/lib/types"

View File

@@ -19,7 +19,7 @@ import {
gitAddRemote,
gitRemoveRemote,
gitSetRemoteUrl,
} from "@/lib/tauri"
} from "@/lib/api"
interface RemoteDraft {
originalName: string | null

View File

@@ -16,7 +16,7 @@ import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { gitStashPush } from "@/lib/tauri"
import { gitStashPush } from "@/lib/api"
import { toErrorMessage } from "@/lib/app-error"
interface StashDialogProps {

View File

@@ -13,8 +13,8 @@ import {
PopoverTrigger,
} from "@/components/ui/popover"
import { useAcpActions } from "@/contexts/acp-connections-context"
import { openUrl } from "@tauri-apps/plugin-opener"
import { openSettingsWindow } from "@/lib/tauri"
import { openUrl } from "@/lib/platform"
import { openSettingsWindow } from "@/lib/api"
import { AGENT_LABELS, type AgentType } from "@/lib/types"
const KNOWN_AGENT_TYPES = new Set<AgentType>(

View File

@@ -43,7 +43,7 @@ import {
gitStashApply,
gitStashDrop,
gitShowFile,
} from "@/lib/tauri"
} from "@/lib/api"
import { toErrorMessage } from "@/lib/app-error"
import { languageFromPath } from "@/lib/language-detect"
import type { GitStashEntry, GitStatusEntry } from "@/lib/types"

View File

@@ -1,72 +1,74 @@
"use client"
import { useEffect, useState } from "react"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { useEffect, useRef, useState } from "react"
import { isDesktop } from "@/lib/platform"
import { useTranslations } from "next-intl"
import { usePlatform } from "@/hooks/use-platform"
import { disposeTauriListener } from "@/lib/tauri-listener"
import { cn } from "@/lib/utils"
async function getTauriWindow() {
const { getCurrentWindow } = await import("@tauri-apps/api/window")
return getCurrentWindow()
}
export function WindowControls() {
const t = useTranslations("Folder.windowControls")
const { isWindows } = usePlatform()
const [isMaximized, setIsMaximized] = useState(false)
const appWindowRef = useRef<Awaited<
ReturnType<typeof getTauriWindow>
> | null>(null)
useEffect(() => {
if (!isWindows) return
if (!isWindows || !isDesktop()) return
let disposed = false
let unlistenResize: (() => void) | null = null
let resizeFrame: number | null = null
const appWindow = getCurrentWindow()
const syncMaximized = async () => {
try {
const maximized = await appWindow.isMaximized()
if (!disposed) {
setIsMaximized(maximized)
}
} catch {
if (!disposed) {
setIsMaximized(false)
getTauriWindow().then((appWindow) => {
if (disposed) return
appWindowRef.current = appWindow
const syncMaximized = async () => {
try {
const maximized = await appWindow.isMaximized()
if (!disposed) setIsMaximized(maximized)
} catch {
if (!disposed) setIsMaximized(false)
}
}
}
const scheduleSync = () => {
if (resizeFrame !== null) return
const scheduleSync = () => {
if (resizeFrame !== null) return
resizeFrame = window.requestAnimationFrame(() => {
resizeFrame = null
void syncMaximized()
})
}
resizeFrame = window.requestAnimationFrame(() => {
resizeFrame = null
void syncMaximized()
})
}
void syncMaximized()
void syncMaximized()
appWindow
.onResized(() => {
scheduleSync()
})
.then((unlisten) => {
unlistenResize = unlisten
})
.catch(() => {
unlistenResize = null
})
appWindow
.onResized(() => scheduleSync())
.then((unlisten) => {
unlistenResize = unlisten
})
.catch(() => {
unlistenResize = null
})
})
return () => {
disposed = true
if (resizeFrame !== null) {
window.cancelAnimationFrame(resizeFrame)
}
disposeTauriListener(unlistenResize, "WindowControls.resize")
unlistenResize?.()
}
}, [isWindows])
if (!isWindows) return null
const appWindow = getCurrentWindow()
if (!isWindows || !isDesktop()) return null
return (
<div className="flex h-8 items-stretch [-webkit-app-region:no-drag]">
@@ -74,7 +76,7 @@ export function WindowControls() {
type="button"
className={buttonClass}
onClick={() => {
appWindow.minimize().catch((err) => {
appWindowRef.current?.minimize().catch((err: unknown) => {
console.error("[WindowControls] failed to minimize:", err)
})
}}
@@ -87,7 +89,7 @@ export function WindowControls() {
type="button"
className={buttonClass}
onClick={() => {
appWindow.toggleMaximize().catch((err) => {
appWindowRef.current?.toggleMaximize().catch((err: unknown) => {
console.error("[WindowControls] failed to toggle maximize:", err)
})
}}
@@ -103,7 +105,7 @@ export function WindowControls() {
"hover:bg-[#e81123] hover:text-white active:bg-[#c50f1f] active:text-white"
)}
onClick={() => {
appWindow.close().catch((err) => {
appWindowRef.current?.close().catch((err: unknown) => {
console.error("[WindowControls] failed to close:", err)
})
}}

View File

@@ -1,7 +1,12 @@
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import { emit } from "@tauri-apps/api/event"
async function emitEvent(event: string, payload?: unknown) {
try {
const { emit } = await import("@tauri-apps/api/event")
await emit(event, payload)
} catch { /* not in Tauri */ }
}
import { Check, FileWarning, Loader2, X, CheckCheck } from "lucide-react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
@@ -19,7 +24,7 @@ import {
gitAbortOperation,
gitContinueOperation,
gitStartPullMerge,
} from "@/lib/tauri"
} from "@/lib/api"
import { languageFromPath } from "@/lib/language-detect"
import { toErrorMessage } from "@/lib/app-error"
import type { GitConflictFileVersions } from "@/lib/types"
@@ -121,7 +126,7 @@ export function MergeWorkspace({
setResolvedFiles((prev) => new Set([...prev, selectedFile]))
// Notify parent window
await emit("folder://merge-conflict-resolved", {
await emitEvent("folder://merge-conflict-resolved", {
folder_id: folderId,
file: selectedFile,
})
@@ -145,7 +150,7 @@ export function MergeWorkspace({
try {
await gitAbortOperation(folderPath, operation)
toast.success(t("abortSuccess"))
await emit("folder://merge-aborted", { folder_id: folderId })
await emitEvent("folder://merge-aborted", { folder_id: folderId })
onAborted()
} catch (err) {
toast.error(toErrorMessage(err))
@@ -159,7 +164,7 @@ export function MergeWorkspace({
try {
await gitContinueOperation(folderPath, operation)
toast.success(t("allResolved"))
await emit("folder://merge-completed", { folder_id: folderId })
await emitEvent("folder://merge-completed", { folder_id: folderId })
onCompleted()
} catch (err) {
toast.error(toErrorMessage(err))

View File

@@ -28,7 +28,7 @@ import {
Trash2,
Wrench,
} from "lucide-react"
import { openUrl } from "@tauri-apps/plugin-opener"
import { openUrl } from "@/lib/platform"
import { toast } from "sonner"
import { AgentIcon } from "@/components/agent-icon"
import {
@@ -64,7 +64,7 @@ import {
acpReorderAgents,
acpUninstallAgent,
acpUpdateAgentPreferences,
} from "@/lib/tauri"
} from "@/lib/api"
import type {
AcpAgentInfo,
AgentType,

View File

@@ -13,7 +13,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { saveAccountToken } from "@/lib/tauri"
import { saveAccountToken } from "@/lib/api"
import type { GitHubAccount } from "@/lib/types"
interface AddGitAccountDialogProps {

View File

@@ -2,7 +2,7 @@
import { useCallback, useState } from "react"
import { ExternalLink, Eye, EyeOff, Loader2 } from "lucide-react"
import { openUrl } from "@tauri-apps/plugin-opener"
import { openUrl } from "@/lib/platform"
import { useTranslations } from "next-intl"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
@@ -14,7 +14,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { validateGitHubToken, saveAccountToken } from "@/lib/tauri"
import { validateGitHubToken, saveAccountToken } from "@/lib/api"
import type { GitHubAccount } from "@/lib/types"
interface AddGitHubAccountDialogProps {

View File

@@ -45,7 +45,7 @@ import {
mcpScanLocal,
mcpSearchMarketplace,
mcpUpsertLocalServer,
} from "@/lib/tauri"
} from "@/lib/api"
import { cn } from "@/lib/utils"
import type {
LocalMcpServer,

View File

@@ -5,6 +5,7 @@ import {
Bot,
BookOpenText,
GitBranch,
Globe,
Keyboard,
Palette,
PlugZap,
@@ -28,6 +29,7 @@ interface SettingsNavItem {
| "shortcuts"
| "version_control"
| "system"
| "web_service"
icon: ComponentType<{ className?: string }>
}
@@ -67,6 +69,11 @@ const SETTINGS_NAV_ITEMS: SettingsNavItem[] = [
labelKey: "system",
icon: Settings,
},
{
href: "/settings/web-service",
labelKey: "web_service",
icon: Globe,
},
]
interface SettingsShellProps {

View File

@@ -55,7 +55,7 @@ import {
openFolderWindow,
acpReadAgentSkill,
acpSaveAgentSkill,
} from "@/lib/tauri"
} from "@/lib/api"
import type {
AcpAgentInfo,
AgentSkillItem,

View File

@@ -9,7 +9,8 @@ import {
RefreshCw,
Wifi,
} from "lucide-react"
import type { Update } from "@tauri-apps/plugin-updater"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Update = any
import { useLocale, useTranslations } from "next-intl"
import { toast } from "sonner"
import { useAppI18n } from "@/components/i18n-provider"
@@ -26,7 +27,7 @@ import {
getSystemProxySettings,
updateSystemLanguageSettings,
updateSystemProxySettings,
} from "@/lib/tauri"
} from "@/lib/api"
import type { AppLocale } from "@/lib/types"
import {
checkAppUpdate,

View File

@@ -35,7 +35,7 @@ import {
validateGitHubToken,
getAccountToken,
deleteAccountToken,
} from "@/lib/tauri"
} from "@/lib/api"
import type {
GitDetectResult,
GitHubAccount,

View File

@@ -0,0 +1,179 @@
"use client"
import { useCallback, useEffect, useState } from "react"
import {
startWebServer,
stopWebServer,
getWebServerStatus,
type WebServerInfo,
} from "@/lib/api"
export function WebServiceSettings() {
const [status, setStatus] = useState<WebServerInfo | null>(null)
const [port, setPort] = useState("3080")
const [loading, setLoading] = useState(false)
const [error, setError] = useState("")
const fetchStatus = useCallback(async () => {
try {
const info = await getWebServerStatus()
setStatus(info)
if (info) {
setPort(String(info.port))
}
} catch {
// Server status unavailable
}
}, [])
useEffect(() => {
fetchStatus()
}, [fetchStatus])
async function handleStart() {
setError("")
setLoading(true)
try {
const info = await startWebServer({
port: parseInt(port, 10) || 3080,
})
setStatus(info)
} catch (e: unknown) {
const msg =
e && typeof e === "object" && "message" in e
? (e as { message: string }).message
: "启动失败"
setError(msg)
} finally {
setLoading(false)
}
}
async function handleStop() {
setLoading(true)
try {
await stopWebServer()
setStatus(null)
} catch {
setError("停止失败")
} finally {
setLoading(false)
}
}
function copyToken() {
if (status?.token) {
navigator.clipboard.writeText(status.token)
}
}
function copyUrl() {
if (status?.addresses?.[1]) {
navigator.clipboard.writeText(status.addresses[1])
} else if (status?.addresses?.[0]) {
navigator.clipboard.writeText(status.addresses[0])
}
}
const isRunning = status !== null
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium">Web </h3>
<p className="text-sm text-muted-foreground">
访 Codeg
</p>
</div>
<div className="space-y-4">
{/* Port config */}
<div className="flex items-center gap-4">
<label className="w-20 text-sm font-medium"></label>
<input
type="number"
value={port}
onChange={(e) => setPort(e.target.value)}
disabled={isRunning}
min={1024}
max={65535}
className="flex h-9 w-32 rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
/>
</div>
{/* Start/Stop button */}
<div className="flex items-center gap-4">
<label className="w-20 text-sm font-medium"></label>
<div className="flex items-center gap-3">
<span
className={`inline-block h-2 w-2 rounded-full ${
isRunning ? "bg-green-500" : "bg-muted-foreground/30"
}`}
/>
<span className="text-sm">
{isRunning ? "运行中" : "已停止"}
</span>
<button
onClick={isRunning ? handleStop : handleStart}
disabled={loading}
className="inline-flex h-8 items-center rounded-md border border-input bg-background px-3 text-xs font-medium ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
>
{loading
? "处理中..."
: isRunning
? "停止"
: "启动"}
</button>
</div>
</div>
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
{/* Connection info */}
{isRunning && (
<div className="rounded-md border p-4 space-y-3">
<div className="space-y-1">
<div className="text-xs font-medium text-muted-foreground">
访
</div>
{status.addresses.map((addr) => (
<div key={addr} className="flex items-center gap-2">
<code className="text-sm">{addr}</code>
</div>
))}
<button
onClick={copyUrl}
className="text-xs text-primary hover:underline"
>
</button>
</div>
<div className="space-y-1">
<div className="text-xs font-medium text-muted-foreground">
访 Token
</div>
<div className="flex items-center gap-2">
<code className="rounded bg-muted px-2 py-0.5 text-xs">
{status.token}
</code>
<button
onClick={copyToken}
className="text-xs text-primary hover:underline"
>
</button>
</div>
</div>
<p className="text-xs text-muted-foreground">
Web 访 Token
</p>
</div>
)}
</div>
</div>
)
}

View File

@@ -1,9 +1,8 @@
"use client"
import { useEffect, useRef } from "react"
import { listen } from "@tauri-apps/api/event"
import { terminalWrite, terminalResize } from "@/lib/tauri"
import { disposeTauriListener } from "@/lib/tauri-listener"
import { subscribe } from "@/lib/platform"
import { terminalWrite, terminalResize } from "@/lib/api"
import type { TerminalEvent } from "@/lib/types"
import type { ITheme } from "@xterm/xterm"
@@ -169,14 +168,14 @@ export function TerminalView({
)
// Set up event listeners BEFORE fit so initial output is captured
const unlisten = await listen<TerminalEvent>(
const unlisten = await subscribe<TerminalEvent>(
`terminal://output/${terminalId}`,
(event) => {
term.write(event.payload.data)
(payload) => {
term.write(payload.data)
}
)
const unlistenExit = await listen<TerminalEvent>(
const unlistenExit = await subscribe<TerminalEvent>(
`terminal://exit/${terminalId}`,
() => {
term.write("\r\n\x1b[90m[Process exited]\x1b[0m\r\n")
@@ -187,8 +186,8 @@ export function TerminalView({
themeObserver.disconnect()
onDataDisposable.dispose()
onResizeDisposable.dispose()
disposeTauriListener(unlisten, "TerminalView.output")
disposeTauriListener(unlistenExit, "TerminalView.exit")
unlisten()
unlistenExit()
term.dispose()
return
}
@@ -222,8 +221,8 @@ export function TerminalView({
themeObserver.disconnect()
onDataDisposable.dispose()
onResizeDisposable.dispose()
disposeTauriListener(unlisten, "TerminalView.output")
disposeTauriListener(unlistenExit, "TerminalView.exit")
unlisten()
unlistenExit()
resizeObserver.disconnect()
term.dispose()
fitAddonRef.current = null

View File

@@ -1,10 +1,10 @@
"use client"
import { useState } from "react"
import { open } from "@tauri-apps/plugin-dialog"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import { cloneRepository, openFolderWindow } from "@/lib/tauri"
import { cloneRepository, openFolderWindow } from "@/lib/api"
import { openFileDialog } from "@/lib/platform"
import { useGitCredential } from "@/contexts/git-credential-context"
import {
Dialog,
@@ -36,9 +36,9 @@ export function CloneDialog({ open: isOpen, onOpenChange }: CloneDialogProps) {
} | null>(null)
const handleBrowse = async () => {
const selected = await open({ directory: true, multiple: false })
const selected = await openFileDialog({ directory: true, multiple: false })
if (selected) {
setTargetDir(selected)
setTargetDir(Array.isArray(selected) ? selected[0] : selected)
}
}

View File

@@ -4,8 +4,8 @@ import { useState } from "react"
import { FolderOpen, GitBranch } from "lucide-react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import { open } from "@tauri-apps/plugin-dialog"
import { openFolderWindow } from "@/lib/tauri"
import { openFolderWindow } from "@/lib/api"
import { openFileDialog } from "@/lib/platform"
import { Button } from "@/components/ui/button"
import { CloneDialog } from "./clone-dialog"
import { resolveWelcomeError } from "@/components/welcome/error-utils"
@@ -15,8 +15,9 @@ export function FolderActions() {
const [cloneOpen, setCloneOpen] = useState(false)
const handleOpen = async () => {
const selected = await open({ directory: true, multiple: false })
if (!selected) return
const result = await openFileDialog({ directory: true, multiple: false })
if (!result) return
const selected = Array.isArray(result) ? result[0] : result
try {
await openFolderWindow(selected)

View File

@@ -6,7 +6,7 @@ import { formatDistanceToNow } from "date-fns"
import { enUS, zhCN, zhTW } from "date-fns/locale"
import { useLocale, useTranslations } from "next-intl"
import { toast } from "sonner"
import { openFolderWindow, removeFolderFromHistory } from "@/lib/tauri"
import { openFolderWindow, removeFolderFromHistory } from "@/lib/api"
import type { FolderHistoryEntry } from "@/lib/types"
import { Input } from "@/components/ui/input"
import { resolveWelcomeError } from "@/components/welcome/error-utils"

View File

@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from "react"
import { Settings } from "lucide-react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import { loadFolderHistory, openSettingsWindow } from "@/lib/tauri"
import { loadFolderHistory, openSettingsWindow } from "@/lib/api"
import type { FolderHistoryEntry } from "@/lib/types"
import { FolderList } from "@/components/welcome/folder-list"
import { FolderActions } from "@/components/welcome/folder-actions"