From 8818b1f3632f507f66a8b441fce64b5a3b746390 Mon Sep 17 00:00:00 2001 From: bjlida Date: Wed, 3 Jun 2026 13:34:41 +0800 Subject: [PATCH 1/2] feat: Cursor-style UI overhaul, in-app settings, and installer branding Unify AI chat and input in a resizable bottom Agent panel, add in-app settings overlay, wire Claude Code sections, and improve editor UX. Refresh NSIS installer artwork and drop Windows from release CI matrix. Co-authored-by: Cursor --- .github/workflows/release.yml | 22 - scripts/generate-installer-assets.py | 163 ++++++ src-tauri/icons/installer-header.bmp | Bin 25818 -> 25818 bytes src-tauri/icons/installer-sidebar.bmp | Bin 154542 -> 154542 bytes src/app/App.tsx | 171 +++++-- src/modules/ai/components/AiAgentPanel.tsx | 469 ++++++++++++++++++ src/modules/ai/components/AiInputBar.tsx | 15 +- .../ai/components/AiStatusBarControls.tsx | 2 +- src/modules/ai/components/lazy.tsx | 16 +- src/modules/ai/index.ts | 1 + src/modules/ai/store/chatStore.ts | 2 +- src/modules/editor/EditorPane.tsx | 97 +++- src/modules/editor/EditorStack.tsx | 21 +- src/modules/editor/index.ts | 2 +- src/modules/explorer/TreeRow.tsx | 2 +- src/modules/header/SearchInline.tsx | 53 +- src/modules/settings/SettingsOverlay.tsx | 92 ++++ src/modules/settings/openSettingsWindow.ts | 15 +- .../settings/openSettingsWindow.types.ts | 9 + src/modules/settings/settingsOverlayStore.ts | 19 + src/modules/settings/settingsTabs.tsx | 46 ++ src/modules/settings/store.ts | 10 + src/modules/shortcuts/shortcuts.ts | 26 +- src/modules/statusbar/StatusBar.tsx | 8 + src/modules/theme/ThemeProvider.tsx | 10 +- src/settings/SettingsApp.tsx | 65 +-- src/settings/main.tsx | 2 +- src/settings/sections/GeneralSection.tsx | 268 +++++----- 28 files changed, 1332 insertions(+), 274 deletions(-) create mode 100644 scripts/generate-installer-assets.py create mode 100644 src/modules/ai/components/AiAgentPanel.tsx create mode 100644 src/modules/settings/SettingsOverlay.tsx create mode 100644 src/modules/settings/openSettingsWindow.types.ts create mode 100644 src/modules/settings/settingsOverlayStore.ts create mode 100644 src/modules/settings/settingsTabs.tsx diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5db0fb0..b7caf54 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,9 +23,6 @@ jobs: - platform: ubuntu-22.04 args: "" rust-target: "" - - platform: windows-latest - args: "" - rust-target: "" runs-on: ${{ matrix.platform }} steps: @@ -78,22 +75,3 @@ jobs: prerelease: false args: ${{ matrix.args }} - - name: Build portable (Windows) - if: matrix.platform == 'windows-latest' - shell: pwsh - run: | - $exe = "src-tauri/target/release/gencode.exe" - if (Test-Path $exe) { - $dest = "GenCode_${{ github.ref_name }}_x64-portable.zip" - Compress-Archive -Path $exe -DestinationPath $dest - Write-Output "portable_zip=$dest" >> $env:GITHUB_ENV - Write-Output "portable=yes" >> $env:GITHUB_ENV - } - - - name: Upload portable to release - if: env.portable == 'yes' - shell: bash - run: | - gh release upload "${{ github.ref_name }}" "${{ env.portable_zip }}" --clobber - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/generate-installer-assets.py b/scripts/generate-installer-assets.py new file mode 100644 index 0000000..eebda8a --- /dev/null +++ b/scripts/generate-installer-assets.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""Generate NSIS installer header/sidebar BMP assets for GenCode.""" + +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont, ImageFilter + +ROOT = Path(__file__).resolve().parents[1] +ICONS = ROOT / "src-tauri" / "icons" +ICON_SRC = ICONS / "icon.png" +HEADER_OUT = ICONS / "installer-header.bmp" +SIDEBAR_OUT = ICONS / "installer-sidebar.bmp" + +HEADER_SIZE = (150, 57) +SIDEBAR_SIZE = (164, 314) + +BG_TOP = (12, 12, 18) +BG_BOTTOM = (8, 8, 14) +ACCENT_A = (124, 92, 255) +ACCENT_B = (56, 189, 248) +TEXT_PRIMARY = (245, 245, 250) +TEXT_MUTED = (156, 163, 175) + + +def load_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + "C:/Windows/Fonts/segoeuib.ttf" if bold else "C:/Windows/Fonts/segoeui.ttf", + "C:/Windows/Fonts/msyh.ttc", + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" + if bold + else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/System/Library/Fonts/Supplemental/Arial Bold.ttf" + if bold + else "/System/Library/Fonts/Supplemental/Arial.ttf", + ] + for path in candidates: + if Path(path).exists(): + return ImageFont.truetype(path, size=size) + return ImageFont.load_default() + + +def vertical_gradient(size: tuple[int, int]) -> Image.Image: + w, h = size + img = Image.new("RGB", size) + draw = ImageDraw.Draw(img) + for y in range(h): + t = y / max(h - 1, 1) + r = int(BG_TOP[0] * (1 - t) + BG_BOTTOM[0] * t) + g = int(BG_TOP[1] * (1 - t) + BG_BOTTOM[1] * t) + b = int(BG_TOP[2] * (1 - t) + BG_BOTTOM[2] * t) + draw.line([(0, y), (w, y)], fill=(r, g, b)) + return img + + +def lerp(a: int, b: int, t: float) -> int: + return int(a + (b - a) * t) + + +def accent_color(t: float) -> tuple[int, int, int]: + return ( + lerp(ACCENT_A[0], ACCENT_B[0], t), + lerp(ACCENT_A[1], ACCENT_B[1], t), + lerp(ACCENT_A[2], ACCENT_B[2], t), + ) + + +def rounded_mask(size: tuple[int, int], radius: int) -> Image.Image: + mask = Image.new("L", size, 0) + draw = ImageDraw.Draw(mask) + draw.rounded_rectangle((0, 0, size[0] - 1, size[1] - 1), radius=radius, fill=255) + return mask + + +def load_logo(size: int) -> Image.Image: + logo = Image.open(ICON_SRC).convert("RGBA") + logo.thumbnail((size, size), Image.Resampling.LANCZOS) + return logo + + +def save_bmp(img: Image.Image, path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + img.convert("RGB").save(path, format="BMP") + + +def draw_header() -> Image.Image: + w, h = HEADER_SIZE + canvas = vertical_gradient(HEADER_SIZE) + draw = ImageDraw.Draw(canvas) + + for x in range(w): + t = x / max(w - 1, 1) + draw.line([(x, h - 2), (x, h - 1)], fill=accent_color(t)) + + logo = load_logo(34) + canvas.paste(logo, (10, (h - logo.height) // 2), logo) + + title_font = load_font(13, bold=True) + sub_font = load_font(9) + draw.text((52, 16), "GenCode", fill=TEXT_PRIMARY, font=title_font) + draw.text((52, 33), "灵码ADE", fill=TEXT_MUTED, font=sub_font) + return canvas + + +def draw_sidebar() -> Image.Image: + w, h = SIDEBAR_SIZE + canvas = vertical_gradient(SIDEBAR_SIZE) + draw = ImageDraw.Draw(canvas) + + for y in range(h): + t = y / max(h - 1, 1) + draw.line([(0, y), (3, y)], fill=accent_color(t)) + + glow = Image.new("RGBA", (w, h), (0, 0, 0, 0)) + glow_draw = ImageDraw.Draw(glow) + glow_draw.ellipse((18, 36, 146, 164), fill=(124, 92, 255, 28)) + glow_draw.ellipse((28, 56, 136, 144), fill=(56, 189, 248, 18)) + glow = glow.filter(ImageFilter.GaussianBlur(radius=8)) + canvas.paste(glow, (0, 0), glow) + + logo = load_logo(72) + lx = (w - logo.width) // 2 + canvas.paste(logo, (lx, 58), logo) + + title_font = load_font(18, bold=True) + sub_font = load_font(11) + tag_font = load_font(9) + + title = "GenCode" + tw = draw.textlength(title, font=title_font) + draw.text(((w - tw) / 2, 148), title, fill=TEXT_PRIMARY, font=title_font) + + sub = "灵码ADE" + sw = draw.textlength(sub, font=sub_font) + draw.text(((w - sw) / 2, 174), sub, fill=accent_color(0.55), font=sub_font) + + tag = "AI-native terminal" + tg_w = draw.textlength(tag, font=tag_font) + draw.text(((w - tg_w) / 2, 204), tag, fill=TEXT_MUTED, font=tag_font) + + for x in range(24, w - 24): + t = (x - 24) / max(w - 48, 1) + draw.point((x, 232), fill=accent_color(t)) + + footer_font = load_font(8) + footer = "yamikeji.com" + fw = draw.textlength(footer, font=footer_font) + draw.text(((w - fw) / 2, h - 24), footer, fill=(90, 96, 110), font=footer_font) + return canvas + + +def main() -> None: + if not ICON_SRC.exists(): + raise SystemExit(f"Missing icon source: {ICON_SRC}") + save_bmp(draw_header(), HEADER_OUT) + save_bmp(draw_sidebar(), SIDEBAR_OUT) + print(f"Wrote {HEADER_OUT}") + print(f"Wrote {SIDEBAR_OUT}") + + +if __name__ == "__main__": + main() diff --git a/src-tauri/icons/installer-header.bmp b/src-tauri/icons/installer-header.bmp index 19317e6fa6041eb68a61cb3b732bda34274da10e..3fb8ef91081002287aa3c118df9130658f280c33 100644 GIT binary patch literal 25818 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 ZW^p>?bjazj>m$H0_=3h+w~heA{2KrqyVw8# literal 25818 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 diff --git a/src-tauri/icons/installer-sidebar.bmp b/src-tauri/icons/installer-sidebar.bmp index 4fdbfff288a2e6d488ad07743d9baa2e16618a68..fe5ec7de551b4712347b57598a6efc93f15aa85d 100644 GIT binary patch literal 154542 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 M#d8#c=D!~Jf5|REVgLXD literal 154542 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 zE6+dw(null); const sidebarWidthRef = useRef(readSidebarWidth()); + const aiPanelSizeRef = useRef(readAiPanelSize()); const sidebarWidthWriteTimerRef = useRef(0); const [sidebarView, setSidebarViewState] = useState(readSidebarView); const persistSidebarView = useCallback((view: SidebarViewId) => { @@ -317,6 +337,9 @@ export default function App() { const [home, setHome] = useState(null); const [pendingCloseTab, setPendingCloseTab] = useState(null); + const [editorCursor, setEditorCursor] = useState( + null, + ); const workspaceEnv = useWorkspaceEnvStore((s) => s.env); const setWorkspaceEnv = useWorkspaceEnvStore((s) => s.setEnv); const [launchCwd, setLaunchCwd] = useState(null); @@ -395,6 +418,7 @@ export default function App() { }, []); const [shortcutsOpen, setShortcutsOpen] = useState(false); + const [ccCommandsOpen, setCcCommandsOpen] = useState(false); const [newEditorOpen, setNewEditorOpen] = useState(false); const miniOpen = useChatStore((s) => s.mini.open); const activeSessionId = useChatStore((s) => s.activeSessionId); @@ -631,7 +655,14 @@ export default function App() { activeLeafId !== null ? (searchAddons.current.get(activeLeafId) ?? null) : null, ); setActiveEditorHandle(editorRefs.current.get(activeId) ?? null); - }, [activeId, activeLeafId]); + if (activeTab?.kind === "editor") { + const handle = editorRefs.current.get(activeId); + setEditorCursor(handle?.getCursorPosition() ?? null); + requestAnimationFrame(() => handle?.focus()); + } else { + setEditorCursor(null); + } + }, [activeId, activeLeafId, activeTab?.kind]); const handleSearchReady = useCallback( (leafId: number, addon: SearchAddon) => { @@ -692,6 +723,14 @@ export default function App() { } }, [pendingCloseTab, disposeTab]); + const saveAndClose = useCallback(async () => { + if (pendingCloseTab === null) return; + const ok = await editorRefs.current.get(pendingCloseTab)?.save(); + if (!ok) return; + disposeTab(pendingCloseTab); + setPendingCloseTab(null); + }, [pendingCloseTab, disposeTab]); + const cancelClose = useCallback(() => { setPendingCloseTab(null); }, []); @@ -1061,6 +1100,7 @@ export default function App() { "ai.toggle": togglePanelAndFocus, "ai.askSelection": askFromSelection, "shortcuts.open": () => setShortcutsOpen((v) => !v), + "claudeCode.commands": () => setCcCommandsOpen(true), "settings.open": () => void openSettingsWindow(), "sidebar.toggle": toggleSidebar, "explorer.focus": toggleExplorerFocus, @@ -1069,6 +1109,22 @@ export default function App() { "view.zoomReset": zoomReset, "editor.undo": () => editorRefs.current.get(activeId)?.undo(), "editor.redo": () => editorRefs.current.get(activeId)?.redo(), + "editor.replace": () => searchInlineRef.current?.openReplace(), + "editor.format": () => { + const t = tabs.find((x) => x.id === activeId); + if (t?.kind !== "editor") return; + const path = t.path; + const ext = path.split(".").pop()?.toLowerCase() ?? ""; + const cmd = + ext === "rs" + ? `cargo fmt -- "${path}"` + : `npx prettier --write "${path.replace(/\\/g, "/")}"`; + void invoke("shell_run_command", { + command: cmd, + cwd: explorerRoot ?? launchCwd ?? home, + workspace: currentWorkspaceEnv(), + }).then(() => editorRefs.current.get(activeId)?.reload()); + }, }), [ activeId, @@ -1096,6 +1152,12 @@ export default function App() { if (id === "editor.undo" || id === "editor.redo") { return activeTab?.kind !== "editor"; } + if ( + id === "editor.replace" || + id === "editor.format" + ) { + return activeTab?.kind !== "editor"; + } if (id === "ai.askSelection") { const target = (e.target as HTMLElement | null) ?? document.activeElement; @@ -1121,6 +1183,13 @@ export default function App() { [], ); + const handleEditorCursor = useCallback( + (id: number, pos: EditorCursorPosition | null) => { + if (id === activeId) setEditorCursor(pos); + }, + [activeId], + ); + const registerEditorHandle = useCallback( (id: number, h: EditorPaneHandle | null) => { if (h) editorRefs.current.set(id, h); @@ -1345,6 +1414,7 @@ export default function App() { registerHandle={registerEditorHandle} onDirtyChange={handleEditorDirty} onCloseTab={disposeTab} + onCursorChange={handleEditorCursor} />
-
-
- {workspaceSurface} -
- - {keysLoaded ? ( - - {hasComposer ? ( - - ) : ( - void openSettingsWindow("models")} - /> - )} - + + +
+ {workspaceSurface} +
+
+ {panelOpen && keysLoaded ? ( + <> + + { + if (size.asPercentage > 0) { + aiPanelSizeRef.current = Math.round(size.asPercentage); + try { + localStorage.setItem( + AI_PANEL_SIZE_KEY, + String(aiPanelSizeRef.current), + ); + } catch { + /* ignore */ + } + } + }} + > + {hasComposer ? ( + + ) : ( + void openSettingsWindow("models")} + /> + )} + + ) : null} -
+
@@ -1521,6 +1610,7 @@ export default function App() { + + + + + + Claude Code 命令参考 + + setCcCommandsOpen(false)} /> + + + - + 取消 - - 强制关闭 + + 不保存 + + void saveAndClose()}> + 保存 diff --git a/src/modules/ai/components/AiAgentPanel.tsx b/src/modules/ai/components/AiAgentPanel.tsx new file mode 100644 index 0000000..bc819ca --- /dev/null +++ b/src/modules/ai/components/AiAgentPanel.tsx @@ -0,0 +1,469 @@ +import { Button } from "@/components/ui/button"; +import { + Context, + ContextContent, + ContextContentBody, + ContextContentFooter, + ContextContentHeader, + ContextTrigger, +} from "@/components/ai-elements/context"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Spinner } from "@/components/ui/spinner"; +import { cn } from "@/lib/utils"; +import { useChat, type UIMessage } from "@ai-sdk/react"; +import { + Add01Icon, + AlertCircleIcon, + ArrowDown01Icon, + Cancel01Icon, + Delete02Icon, + FilterIcon, + TerminalIcon, +} from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { useMemo } from "react"; +import { estimateCost, getModel, getModelContextLimit } from "../config"; +import type { SessionMeta } from "../lib/sessions"; +import { useAgentsStore } from "../store/agentsStore"; +import { getOrCreateChat, useChatStore } from "../store/chatStore"; +import { usePreferencesStore } from "@/modules/settings/preferences"; +import { usePlanStore } from "../store/planStore"; +import { AgentSwitcher } from "./AgentSwitcher"; +import { AiChatView } from "./AiChat"; +import { AiInputBar } from "./AiInputBar"; +import { PlanDiffReview } from "./PlanDiffReview"; +import { TodoStrip } from "./TodoStrip"; + +const SUGGESTIONS = [ + { + label: "解释上一个错误", + hint: "读取终端缓冲区", + icon: AlertCircleIcon, + text: "解释终端中的上一个错误。", + }, + { + label: "生成命令", + hint: "告诉我你想做什么", + icon: TerminalIcon, + text: "帮我生成一个命令:", + }, + { + label: "总结缓冲区", + hint: "回顾近期活动", + icon: FilterIcon, + text: "总结终端中刚刚发生的事情。", + }, +]; + +type PanelHeaderProps = { + step: string | null; + isBusy: boolean; + onClose?: () => void; + messages?: UIMessage[]; +}; + +export function AiAgentPanel({ onClose }: { onClose?: () => void }) { + const sessionId = useChatStore((s) => s.activeSessionId); + const closePanel = useChatStore((s) => s.closePanel); + + const handleClose = () => { + onClose?.(); + closePanel(); + }; + + if (!sessionId) { + return ( +
+ +
+ 加载会话中… +
+
+ ); + } + + return ( +
+ + +
+ ); +} + +function PanelBody({ + sessionId, + onClose, +}: { + sessionId: string; + onClose: () => void; +}) { + const focusInput = useChatStore((s) => s.focusInput); + const step = useChatStore((s) => s.agentMeta.step); + + const chat = useMemo(() => getOrCreateChat(sessionId), [sessionId]); + const helpers = useChat({ chat }); + const isBusy = + helpers.status === "submitted" || helpers.status === "streaming"; + + return ( + <> + + + + +
+ {helpers.messages.length === 0 ? ( + focusInput(text)} /> + ) : ( +
+ +
+ )} +
+ + + + + ); +} + +function PlanModeStrip() { + const active = usePlanStore((s) => s.active); + const queueLen = usePlanStore((s) => s.queue.length); + const disable = usePlanStore((s) => s.disable); + if (!active) return null; + return ( +
+ + 规划模式 + + {queueLen > 0 ? `· ${queueLen} 个待执行` : "· 无待执行编辑"} + + + +
+ ); +} + +function PanelHeader({ step, isBusy, onClose, messages }: PanelHeaderProps) { + void useAgentsStore((s) => s.customAgents); + + return ( +
+
+ + {messages !== undefined ? ( + + ) : null} +
+
+ {isBusy ? ( + + + {step ?? "思考中…"} + + ) : null} + + + {onClose ? ( + + ) : null} +
+
+ ); +} + +function estimateTokens(messages: UIMessage[]): number { + let chars = 0; + for (const m of messages) { + for (const p of m.parts) { + if (p.type === "text") { + chars += (p as { text?: string }).text?.length ?? 0; + } else if (p.type === "reasoning") { + chars += (p as { text?: string }).text?.length ?? 0; + } else if (typeof p.type === "string" && p.type.startsWith("tool-")) { + const tp = p as unknown as { input?: unknown; output?: unknown }; + if (tp.input) chars += JSON.stringify(tp.input).length; + if (tp.output) chars += JSON.stringify(tp.output).length; + } + } + } + return Math.ceil(chars / 4); +} + +function formatTokens(n: number): string { + if (n < 1000) return String(n); + if (n < 1_000_000) return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`; + return `${(n / 1_000_000).toFixed(2)}M`; +} + +function ContextIndicator({ messages }: { messages: UIMessage[] }) { + const modelId = useChatStore((s) => s.selectedModelId); + const tokens = useChatStore((s) => s.agentMeta.tokens); + const lastInput = useChatStore((s) => s.agentMeta.lastInputTokens); + const lastCached = useChatStore((s) => s.agentMeta.lastCachedTokens); + const estimated = useMemo(() => estimateTokens(messages), [messages]); + const used = lastInput > 0 ? lastInput : estimated; + const reported = tokens.inputTokens + tokens.outputTokens; + const openaiCompatibleContextLimit = usePreferencesStore( + (s) => s.openaiCompatibleContextLimit, + ); + const max = getModelContextLimit(modelId, openaiCompatibleContextLimit); + const modelLabel = useMemo(() => { + try { + return getModel(modelId).label; + } catch { + return modelId; + } + }, [modelId]); + const cost = estimateCost(modelId, tokens); + const cacheRate = + tokens.inputTokens > 0 + ? Math.round((tokens.cachedInputTokens / tokens.inputTokens) * 100) + : 0; + + return ( + + + + + {modelLabel} + + +
+ 当前上下文 + + {formatTokens(used)} / {formatTokens(max)} + +
+ {lastCached > 0 && ( +
+ 其中缓存 + + {formatTokens(lastCached)} + +
+ )} + {reported > 0 && ( + <> +
+ 会话输入 + + {formatTokens(tokens.inputTokens)} + +
+
+ 会话输出 + + {formatTokens(tokens.outputTokens)} + +
+ {tokens.cachedInputTokens > 0 && ( +
+ 缓存命中 + {cacheRate}% +
+ )} + {cost != null && ( +
+ 会话费用 + + ${cost.toFixed(cost < 0.01 ? 4 : cost < 1 ? 3 : 2)} + +
+ )} + + )} +
+ + + {lastInput > 0 + ? "上次请求反映当前上下文大小。" + : "Token 数为近似值(字符数 / 4)。"} + + +
+
+ ); +} + +function SessionPicker() { + const sessions = useChatStore((s) => s.sessions); + const activeId = useChatStore((s) => s.activeSessionId); + const switchSession = useChatStore((s) => s.switchSession); + const newSession = useChatStore((s) => s.newSession); + const deleteSession = useChatStore((s) => s.deleteSession); + + const active = sessions.find((s) => s.id === activeId) ?? null; + if (!active) return null; + + const sorted = [...sessions].sort((a, b) => b.updatedAt - a.updatedAt); + + return ( + + + + + + newSession()} + className="gap-2 text-xs" + > + + 新建会话 + + {sorted.length > 0 ? : null} + {sorted.map((s) => ( + switchSession(s.id)} + onDelete={() => deleteSession(s.id)} + /> + ))} + + + ); +} + +function SessionRow({ + session, + active, + onSelect, + onDelete, +}: { + session: SessionMeta; + active: boolean; + onSelect: () => void; + onDelete: () => void; +}) { + return ( + { + const target = e.target as HTMLElement | null; + if (target?.closest("[data-session-delete]")) { + e.preventDefault(); + return; + } + onSelect(); + }} + className={cn( + "group flex items-center justify-between gap-2 text-xs", + active && "bg-accent/40", + )} + > + + {session.title || "新对话"} + + + + ); +} + +function EmptyState({ onPick }: { onPick: (text: string) => void }) { + return ( +
+ 灵码ADE +
+

新代理

+

+ 灵码ADE 可以看到当前终端的工作目录、最近命令和输出。 +

+
+
+ {SUGGESTIONS.map((s) => ( + + ))} +
+
+ ); +} diff --git a/src/modules/ai/components/AiInputBar.tsx b/src/modules/ai/components/AiInputBar.tsx index 4556ef3..9562287 100644 --- a/src/modules/ai/components/AiInputBar.tsx +++ b/src/modules/ai/components/AiInputBar.tsx @@ -72,7 +72,7 @@ function detectFileTrigger( return null; } -export function AiInputBar() { +export function AiInputBar({ embedded = false }: { embedded?: boolean }) { const c = useComposer(); const snippets = useSnippetsStore((s) => s.snippets); const workspaceRoot = useChatStore((s) => s.live.getWorkspaceRoot()); @@ -129,9 +129,7 @@ export function AiInputBar() { c.name.includes(q) || c.label.includes(q) || c.description.includes(q), - ) - .slice(0, 20) - .map((command) => ({ kind: "command", command })); + ).map((command) => ({ kind: "command", command })); const snipItems: PickerItem[] = snippets .filter( (s) => @@ -236,7 +234,14 @@ export function AiInputBar() { : null; return ( -
+
diff --git a/src/modules/ai/components/lazy.tsx b/src/modules/ai/components/lazy.tsx index bc88bd9..2e53e52 100644 --- a/src/modules/ai/components/lazy.tsx +++ b/src/modules/ai/components/lazy.tsx @@ -10,6 +10,10 @@ const AiMiniWindowInner = lazy(() => import("./AiMiniWindow").then((m) => ({ default: m.AiMiniWindow })), ); +const AiAgentPanelInner = lazy(() => + import("./AiAgentPanel").then((m) => ({ default: m.AiAgentPanel })), +); + const AiInputBarModule = () => import("./AiInputBar"); const AiInputBarInner = lazy(() => @@ -40,10 +44,18 @@ export function AiMiniWindow() { ); } -export function AiInputBar() { +export function AiAgentPanel() { + return ( + + + + ); +} + +export function AiInputBar(props: { embedded?: boolean }) { return ( - + ); } diff --git a/src/modules/ai/index.ts b/src/modules/ai/index.ts index fe11cdc..5ec5f10 100644 --- a/src/modules/ai/index.ts +++ b/src/modules/ai/index.ts @@ -1,5 +1,6 @@ export { AgentRunBridge, + AiAgentPanel, AiInputBar, AiInputBarConnect, AiMiniWindow, diff --git a/src/modules/ai/store/chatStore.ts b/src/modules/ai/store/chatStore.ts index 4018b49..dedfaf8 100644 --- a/src/modules/ai/store/chatStore.ts +++ b/src/modules/ai/store/chatStore.ts @@ -352,7 +352,7 @@ export const useChatStore = create((set, get) => ({ }, mini: { open: false }, - openMini: () => set({ mini: { open: true } }), + openMini: () => set({ mini: { open: true }, panelOpen: true }), closeMini: () => set({ mini: { open: false } }), toggleMini: () => set((s) => ({ mini: { open: !s.mini.open } })), diff --git a/src/modules/editor/EditorPane.tsx b/src/modules/editor/EditorPane.tsx index 8399a5f..32607af 100644 --- a/src/modules/editor/EditorPane.tsx +++ b/src/modules/editor/EditorPane.tsx @@ -2,10 +2,12 @@ import { redo, undo } from "@codemirror/commands"; import { findNext, findPrevious, + replaceAll, + replaceNext, SearchQuery, setSearchQuery, } from "@codemirror/search"; -import { keymap } from "@codemirror/view"; +import { keymap, EditorView } from "@codemirror/view"; import { usePreferencesStore } from "@/modules/settings/preferences"; import CodeMirror, { type ReactCodeMirrorRef } from "@uiw/react-codemirror"; import { EDITOR_THEME_EXT } from "./lib/themes"; @@ -22,6 +24,7 @@ import { buildSharedExtensions, languageCompartment, vimCompartment, + wrapCompartment, } from "./lib/extensions"; import { initVimGlobals, vimHandlersExtension } from "./lib/vim"; @@ -32,6 +35,8 @@ import { inlineCompletion } from "./lib/autocomplete/inlineExtension"; import { getKey } from "@/modules/ai/lib/keyring"; import { onKeysChanged } from "@/modules/settings/store"; +export type EditorCursorPosition = { line: number; col: number }; + export type EditorPaneHandle = { setQuery: (q: string) => void; findNext: () => void; @@ -41,8 +46,14 @@ export type EditorPaneHandle = { getSelection: () => string | null; getSelectionRect: () => { left: number; top: number; width: number } | null; getPath: () => string; + getCursorPosition: () => EditorCursorPosition | null; + setReplaceQuery: (replace: string) => void; + replaceNext: () => void; + replaceAll: () => void; /** Re-read the file from disk. Skips silently if the buffer is dirty. */ reload: () => boolean; + /** Save buffer to disk. Returns false if nothing to save or save failed. */ + save: () => Promise; /** Apply CodeMirror's undo/redo commands. */ undo: () => void; redo: () => void; @@ -53,6 +64,7 @@ type Props = { onDirtyChange?: (dirty: boolean) => void; onSaved?: () => void; onClose?: () => void; + onCursorChange?: (pos: EditorCursorPosition | null) => void; }; function formatBytes(n: number): string { @@ -62,13 +74,14 @@ function formatBytes(n: number): string { } export const EditorPane = forwardRef( - function EditorPane({ path, onDirtyChange, onSaved, onClose }, ref) { + function EditorPane({ path, onDirtyChange, onSaved, onClose, onCursorChange }, ref) { const { doc, onChange, save, reload } = useDocument({ path, onDirtyChange }); const reloadRef = useRef(reload); reloadRef.current = reload; const cmRef = useRef(null); const editorThemeId = usePreferencesStore((s) => s.editorTheme); const vimMode = usePreferencesStore((s) => s.vimMode); + const editorWordWrap = usePreferencesStore((s) => s.editorWordWrap); const languageRef = useRef(null); const apiKeyRef = useRef(null); @@ -114,13 +127,22 @@ export const EditorPane = forwardRef( const pathRef = useRef(path); pathRef.current = path; + const onCursorChangeRef = useRef(onCursorChange); + onCursorChangeRef.current = onCursorChange; + + const searchQueryRef = useRef(""); + const replaceQueryRef = useRef(""); + const extensions = useMemo( () => [ - // basicSetup is added before user extensions by @uiw/react-codemirror, - // so we must elevate vim's precedence to win the keymap. vimCompartment.of( usePreferencesStore.getState().vimMode ? Prec.highest(vim()) : [], ), + wrapCompartment.of( + usePreferencesStore.getState().editorWordWrap + ? EditorView.lineWrapping + : [], + ), vimHandlersExtension(() => ({ save: () => { void (async () => { @@ -175,6 +197,16 @@ export const EditorPane = forwardRef( }, }, ]), + EditorView.updateListener.of((update) => { + if (!update.selectionSet && !update.docChanged) return; + const view = update.view; + const pos = view.state.selection.main.head; + const line = view.state.doc.lineAt(pos); + onCursorChangeRef.current?.({ + line: line.number, + col: pos - line.from + 1, + }); + }), ], [], ); @@ -189,6 +221,16 @@ export const EditorPane = forwardRef( }); }, [vimMode]); + useEffect(() => { + const view = cmRef.current?.view; + if (!view) return; + view.dispatch({ + effects: wrapCompartment.reconfigure( + editorWordWrap ? EditorView.lineWrapping : [], + ), + }); + }, [editorWordWrap]); + useEffect(() => { let cancelled = false; const ext = path.split(".").pop()?.toLowerCase() ?? null; @@ -222,13 +264,40 @@ export const EditorPane = forwardRef( setQuery: (q: string) => { const view = cmRef.current?.view; if (!view) return; + searchQueryRef.current = q; view.dispatch({ effects: setSearchQuery.of( - new SearchQuery({ search: q, caseSensitive: false }), + new SearchQuery({ + search: q, + replace: replaceQueryRef.current, + caseSensitive: false, + }), ), }); if (q) findNext(view); }, + setReplaceQuery: (replace: string) => { + replaceQueryRef.current = replace; + const view = cmRef.current?.view; + if (!view) return; + view.dispatch({ + effects: setSearchQuery.of( + new SearchQuery({ + search: searchQueryRef.current, + replace, + caseSensitive: false, + }), + ), + }); + }, + replaceNext: () => { + const view = cmRef.current?.view; + if (view) replaceNext(view); + }, + replaceAll: () => { + const view = cmRef.current?.view; + if (view) replaceAll(view); + }, findNext: () => { const view = cmRef.current?.view; if (view) findNext(view); @@ -274,7 +343,23 @@ export const EditorPane = forwardRef( return { left, top, width: Math.max(1, right - left) }; }, getPath: () => path, + getCursorPosition: () => { + const view = cmRef.current?.view; + if (!view) return null; + const pos = view.state.selection.main.head; + const line = view.state.doc.lineAt(pos); + return { line: line.number, col: pos - line.from + 1 }; + }, reload: () => reloadRef.current(), + save: async () => { + try { + await saveRef.current(); + onSavedRef.current?.(); + return true; + } catch { + return false; + } + }, undo: () => { const view = cmRef.current?.view; if (view) undo(view); @@ -290,7 +375,7 @@ export const EditorPane = forwardRef( if (doc.status === "loading") { return (
- Loading… + 加载中…
); } diff --git a/src/modules/editor/EditorStack.tsx b/src/modules/editor/EditorStack.tsx index c31c86c..98798aa 100644 --- a/src/modules/editor/EditorStack.tsx +++ b/src/modules/editor/EditorStack.tsx @@ -1,7 +1,7 @@ import { cn } from "@/lib/utils"; import type { EditorTab, Tab } from "@/modules/tabs"; import { useEffect, useRef } from "react"; -import { EditorPane, type EditorPaneHandle } from "./EditorPane"; +import { EditorPane, type EditorCursorPosition, type EditorPaneHandle } from "./EditorPane"; type Props = { tabs: Tab[]; @@ -9,6 +9,7 @@ type Props = { onDirtyChange: (id: number, dirty: boolean) => void; registerHandle: (id: number, handle: EditorPaneHandle | null) => void; onCloseTab: (id: number) => void; + onCursorChange?: (id: number, pos: EditorCursorPosition | null) => void; }; export function EditorStack({ @@ -17,6 +18,7 @@ export function EditorStack({ onDirtyChange, registerHandle, onCloseTab, + onCursorChange, }: Props) { const editors = tabs.filter((t): t is EditorTab => t.kind === "editor"); @@ -68,6 +70,19 @@ export function EditorStack({ return cb; }; + const cursorCallbacks = useRef( + new Map void>(), + ); + + const getCursorCallback = (id: number) => { + let cb = cursorCallbacks.current.get(id); + if (!cb) { + cb = (pos) => onCursorChange?.(id, pos); + cursorCallbacks.current.set(id, cb); + } + return cb; + }; + // Drop callback entries for closed tabs to avoid unbounded growth. useEffect(() => { const live = new Set(editors.map((t) => t.id)); @@ -80,6 +95,9 @@ export function EditorStack({ for (const id of closeCallbacks.current.keys()) { if (!live.has(id)) closeCallbacks.current.delete(id); } + for (const id of cursorCallbacks.current.keys()) { + if (!live.has(id)) cursorCallbacks.current.delete(id); + } }, [editors]); if (editors.length === 0) return null; @@ -102,6 +120,7 @@ export function EditorStack({ path={t.path} onDirtyChange={getDirtyCallback(t.id)} onClose={getCloseCallback(t.id)} + onCursorChange={getCursorCallback(t.id)} />
diff --git a/src/modules/editor/index.ts b/src/modules/editor/index.ts index 6e9d9aa..38ad964 100644 --- a/src/modules/editor/index.ts +++ b/src/modules/editor/index.ts @@ -1,4 +1,4 @@ -export type { EditorPaneHandle } from "./EditorPane"; +export type { EditorPaneHandle, EditorCursorPosition } from "./EditorPane"; export { EditorStack } from "./EditorStackLazy"; export { AiDiffStack } from "./AiDiffStackLazy"; export { GitDiffStack } from "./GitDiffStackLazy"; diff --git a/src/modules/explorer/TreeRow.tsx b/src/modules/explorer/TreeRow.tsx index 6f3e615..28ca2ba 100644 --- a/src/modules/explorer/TreeRow.tsx +++ b/src/modules/explorer/TreeRow.tsx @@ -69,7 +69,7 @@ function EntryRowImpl(props: EntryRowProps) { if (tree.renaming) return; onSelectPath(path); if (isDir) tree.toggle(path); - else onOpenFile(path); + else onOpenFile(path, true); }; return ( diff --git a/src/modules/header/SearchInline.tsx b/src/modules/header/SearchInline.tsx index 06218ba..b46121b 100644 --- a/src/modules/header/SearchInline.tsx +++ b/src/modules/header/SearchInline.tsx @@ -35,7 +35,7 @@ export type SearchTarget = } | null; -export type SearchInlineHandle = { focus: () => void }; +export type SearchInlineHandle = { focus: () => void; openReplace: () => void }; type Props = { target: SearchTarget; @@ -46,6 +46,8 @@ type Props = { export const SearchInline = forwardRef( function SearchInline({ target, compact }, ref) { const [q, setQ] = useState(""); + const [replaceQ, setReplaceQ] = useState(""); + const [showReplace, setShowReplace] = useState(false); // In compact mode the field is hidden behind an icon until activated. // In normal mode the field is always present. const [openInCompact, setOpenInCompact] = useState(false); @@ -88,7 +90,12 @@ export const SearchInline = forwardRef( if (inputRef.current) pendingFocusRef.current = false; }, [compact]); - useImperativeHandle(ref, () => ({ focus }), [focus]); + const openReplace = useCallback(() => { + setShowReplace(true); + focus(); + }, [focus]); + + useImperativeHandle(ref, () => ({ focus, openReplace }), [focus, openReplace]); const clearTarget = useCallback(() => { if (!target) return; @@ -115,6 +122,9 @@ export const SearchInline = forwardRef( } else { target.addon.clearDecorations(); } + } else if (target.kind === "editor") { + target.handle.setQuery(next); + if (showReplace) target.handle.setReplaceQuery(replaceQ); } else { target.handle.setQuery(next); } @@ -137,9 +147,9 @@ export const SearchInline = forwardRef( {expanded ? ( @@ -149,8 +159,9 @@ export const SearchInline = forwardRef( animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.12 }} - className="absolute inset-0" + className="flex flex-col gap-1" > +
( applyIncremental(next); }} onBlur={() => { - if (compact && !q) setOpenInCompact(false); + if (compact && !q && !showReplace) setOpenInCompact(false); }} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); - findDirection(!e.shiftKey); + if (e.altKey && target?.kind === "editor") { + target.handle.replaceAll(); + } else if (showReplace && target?.kind === "editor") { + target.handle.replaceNext(); + } else { + findDirection(!e.shiftKey); + } } else if (e.key === "Escape") { e.preventDefault(); clearTarget(); setQ(""); + setReplaceQ(""); + setShowReplace(false); if (compact) { setOpenInCompact(false); } @@ -203,6 +222,26 @@ export const SearchInline = forwardRef( /> )} +
+ {showReplace && target?.kind === "editor" ? ( + { + const next = e.target.value; + setReplaceQ(next); + target.handle.setReplaceQuery(next); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + if (e.altKey) target.handle.replaceAll(); + else target.handle.replaceNext(); + } + }} + /> + ) : null}
) : ( s.open); + const tab = useSettingsOverlayStore((s) => s.tab); + const setTab = useSettingsOverlayStore((s) => s.setTab); + const closeSettings = useSettingsOverlayStore((s) => s.closeSettings); + const init = usePreferencesStore((s) => s.init); + + useEffect(() => { + if (open) void init(); + }, [open, init]); + + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") { + const target = e.target as HTMLElement | null; + const tag = target?.tagName; + if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return; + closeSettings(); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [open, closeSettings]); + + const ActiveSection = SETTINGS_TABS.find((t) => t.id === tab)?.component; + + return ( + + {open ? ( + + + +
+
+ {ActiveSection ? : null} +
+
+
+ ) : null} +
+ ); +} diff --git a/src/modules/settings/openSettingsWindow.ts b/src/modules/settings/openSettingsWindow.ts index f0395c2..9a3ff44 100644 --- a/src/modules/settings/openSettingsWindow.ts +++ b/src/modules/settings/openSettingsWindow.ts @@ -1,15 +1,8 @@ -import { invoke } from "@tauri-apps/api/core"; +import { useSettingsOverlayStore } from "./settingsOverlayStore"; +import type { SettingsTab } from "./openSettingsWindow.types"; -export type SettingsTab = - | "general" - | "themes" - | "shortcuts" - | "models" - | "agents" - | "skills" - | "about" - | "claude-code"; +export type { SettingsTab } from "./openSettingsWindow.types"; export async function openSettingsWindow(tab?: SettingsTab): Promise { - await invoke("open_settings_window", { tab: tab ?? null }); + useSettingsOverlayStore.getState().openSettings(tab); } diff --git a/src/modules/settings/openSettingsWindow.types.ts b/src/modules/settings/openSettingsWindow.types.ts new file mode 100644 index 0000000..109c10c --- /dev/null +++ b/src/modules/settings/openSettingsWindow.types.ts @@ -0,0 +1,9 @@ +export type SettingsTab = + | "general" + | "themes" + | "shortcuts" + | "models" + | "agents" + | "skills" + | "about" + | "claude-code"; diff --git a/src/modules/settings/settingsOverlayStore.ts b/src/modules/settings/settingsOverlayStore.ts new file mode 100644 index 0000000..5638b52 --- /dev/null +++ b/src/modules/settings/settingsOverlayStore.ts @@ -0,0 +1,19 @@ +import { create } from "zustand"; +import type { SettingsTab } from "./openSettingsWindow"; + +type State = { + open: boolean; + tab: SettingsTab; + openSettings: (tab?: SettingsTab) => void; + closeSettings: () => void; + setTab: (tab: SettingsTab) => void; +}; + +export const useSettingsOverlayStore = create((set) => ({ + open: false, + tab: "general", + openSettings: (tab) => + set({ open: true, tab: tab ?? "general" }), + closeSettings: () => set({ open: false }), + setTab: (tab) => set({ tab }), +})); diff --git a/src/modules/settings/settingsTabs.tsx b/src/modules/settings/settingsTabs.tsx new file mode 100644 index 0000000..984c246 --- /dev/null +++ b/src/modules/settings/settingsTabs.tsx @@ -0,0 +1,46 @@ +import type { SettingsTab } from "./openSettingsWindow"; +import { AboutSection } from "@/settings/sections/AboutSection"; +import { AgentsSection } from "@/settings/sections/AgentsSection"; +import { ClaudeCodeSection } from "@/settings/sections/ClaudeCodeSection"; +import { GeneralSection } from "@/settings/sections/GeneralSection"; +import { ModelsSection } from "@/settings/sections/ModelsSection"; +import { ShortcutsSection } from "@/settings/sections/ShortcutsSection"; +import { SkillsSection } from "@/settings/sections/SkillsSection"; +import { ThemesSection } from "@/settings/sections/ThemesSection"; +import { + AiScanIcon, + BookOpen01Icon, + ClaudeIcon, + InformationCircleIcon, + KeyboardIcon, + PaintBoardIcon, + Settings01Icon, + UserMultiple02Icon, +} from "@hugeicons/core-free-icons"; +import type { JSX } from "react"; + +export const SETTINGS_TABS: { + id: SettingsTab; + label: string; + icon: typeof Settings01Icon; + component: () => JSX.Element; +}[] = [ + { id: "general", label: "通用", icon: Settings01Icon, component: GeneralSection }, + { id: "themes", label: "主题", icon: PaintBoardIcon, component: ThemesSection }, + { id: "shortcuts", label: "快捷键", icon: KeyboardIcon, component: ShortcutsSection }, + { id: "models", label: "模型", icon: AiScanIcon, component: ModelsSection }, + { id: "agents", label: "代理", icon: UserMultiple02Icon, component: AgentsSection }, + { id: "claude-code", label: "Claude Code", icon: ClaudeIcon, component: ClaudeCodeSection }, + { id: "skills", label: "Skills", icon: BookOpen01Icon, component: SkillsSection }, + { id: "about", label: "关于", icon: InformationCircleIcon, component: AboutSection }, +]; + +export const VALID_SETTINGS_TABS: SettingsTab[] = SETTINGS_TABS.map((t) => t.id); + +export function normalizeSettingsTab(tab: string | null | undefined): SettingsTab { + if (tab === "ai" || tab === "connections") return "models"; + if (tab && (VALID_SETTINGS_TABS as string[]).includes(tab)) { + return tab as SettingsTab; + } + return "general"; +} diff --git a/src/modules/settings/store.ts b/src/modules/settings/store.ts index 912482e..2b038b8 100644 --- a/src/modules/settings/store.ts +++ b/src/modules/settings/store.ts @@ -89,6 +89,7 @@ export type Preferences = { favoriteModelIds: string[]; recentModelIds: string[]; vimMode: boolean; + editorWordWrap: boolean; showHidden: boolean; terminalWebglEnabled: boolean; terminalFontFamily: string; @@ -140,6 +141,7 @@ const KEY_PERM_PROJECT_SCAN = "permProjectScan"; const KEY_FAVORITE_MODELS = "favoriteModelIds"; const KEY_RECENT_MODELS = "recentModelIds"; const KEY_VIM_MODE = "vimMode"; +const KEY_EDITOR_WORD_WRAP = "editorWordWrap"; const KEY_SHOW_HIDDEN = "showHidden"; const LEGACY_KEY_SHOW_HIDDEN_DIRS = "showHiddenDirectories"; const KEY_TERMINAL_WEBGL_ENABLED = "terminalWebglEnabled"; @@ -206,6 +208,7 @@ export const DEFAULT_PREFERENCES: Preferences = { favoriteModelIds: [], recentModelIds: [], vimMode: false, + editorWordWrap: false, showHidden: false, terminalWebglEnabled: true, terminalFontFamily: "JetBrains Mono", @@ -330,6 +333,8 @@ export async function loadPreferences(): Promise { get(KEY_RECENT_MODELS) ?? DEFAULT_PREFERENCES.recentModelIds ).filter(isKnownModelId), vimMode: get(KEY_VIM_MODE) ?? DEFAULT_PREFERENCES.vimMode, + editorWordWrap: + get(KEY_EDITOR_WORD_WRAP) ?? DEFAULT_PREFERENCES.editorWordWrap, showHidden: get(KEY_SHOW_HIDDEN) ?? get(LEGACY_KEY_SHOW_HIDDEN_DIRS) ?? @@ -543,6 +548,10 @@ export async function setVimMode(value: boolean): Promise { await writePref(KEY_VIM_MODE, value); } +export async function setEditorWordWrap(value: boolean): Promise { + await writePref(KEY_EDITOR_WORD_WRAP, value); +} + export async function setShowHidden(value: boolean): Promise { await writePref(KEY_SHOW_HIDDEN, value); } @@ -649,6 +658,7 @@ export async function onPreferencesChange( [KEY_FAVORITE_MODELS]: "favoriteModelIds", [KEY_RECENT_MODELS]: "recentModelIds", [KEY_VIM_MODE]: "vimMode", + [KEY_EDITOR_WORD_WRAP]: "editorWordWrap", [KEY_SHOW_HIDDEN]: "showHidden", [KEY_TERMINAL_WEBGL_ENABLED]: "terminalWebglEnabled", [KEY_TERMINAL_FONT_FAMILY]: "terminalFontFamily", diff --git a/src/modules/shortcuts/shortcuts.ts b/src/modules/shortcuts/shortcuts.ts index f200f83..20923ba 100644 --- a/src/modules/shortcuts/shortcuts.ts +++ b/src/modules/shortcuts/shortcuts.ts @@ -30,7 +30,10 @@ export type ShortcutId = | "settings.open" | "sidebar.toggle" | "editor.undo" - | "editor.redo"; + | "editor.redo" + | "editor.replace" + | "editor.format" + | "claudeCode.commands"; export type ShortcutGroup = | "General" @@ -39,7 +42,8 @@ export type ShortcutGroup = | "Search" | "AI" | "View" - | "Editor"; + | "Editor" + | "Claude Code"; export type KeyBinding = { key: string; @@ -172,6 +176,12 @@ export const SHORTCUTS: Shortcut[] = [ group: "AI", defaultBindings: [{ [MOD_PROP]: true, key: "l" }], }, + { + id: "claudeCode.commands", + label: "Claude Code command reference", + group: "Claude Code", + defaultBindings: [{ [MOD_PROP]: true, shift: true, key: "?" }], + }, { id: "sidebar.toggle", label: "Toggle file explorer", @@ -227,6 +237,18 @@ export const SHORTCUTS: Shortcut[] = [ group: "Editor", defaultBindings: [{ [MOD_PROP]: true, key: "y" }], }, + { + id: "editor.replace", + label: "Find and replace", + group: "Editor", + defaultBindings: [{ [MOD_PROP]: true, key: "h" }], + }, + { + id: "editor.format", + label: "Format document", + group: "Editor", + defaultBindings: [{ [MOD_PROP]: true, shift: true, alt: true, key: "f" }], + }, ]; export const SHORTCUT_GROUPS: ShortcutGroup[] = [ diff --git a/src/modules/statusbar/StatusBar.tsx b/src/modules/statusbar/StatusBar.tsx index 3574566..2e1bc1f 100644 --- a/src/modules/statusbar/StatusBar.tsx +++ b/src/modules/statusbar/StatusBar.tsx @@ -10,6 +10,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { ClaudeCodeStatus } from "@/modules/claude-code/components/ClaudeCodeStatus"; +import type { EditorCursorPosition } from "@/modules/editor/EditorPane"; import { IncognitoIcon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { CwdBreadcrumb } from "./CwdBreadcrumb"; @@ -19,6 +20,7 @@ import type { WorkspaceEnv } from "@/modules/workspace"; type Props = { cwd: string | null; filePath?: string | null; + editorCursor?: EditorCursorPosition | null; home: string | null; onCd: (path: string) => void; onWorkspaceChange: (env: WorkspaceEnv) => void; @@ -31,6 +33,7 @@ type Props = { export function StatusBar({ cwd, filePath, + editorCursor, home, onCd, onWorkspaceChange, @@ -61,6 +64,11 @@ export function StatusBar({ ) : null}
+ {editorCursor ? ( + + 行 {editorCursor.line},列 {editorCursor.col} + + ) : null} {panelOpen && hasComposer ? ( diff --git a/src/modules/theme/ThemeProvider.tsx b/src/modules/theme/ThemeProvider.tsx index 633eedb..b69b842 100644 --- a/src/modules/theme/ThemeProvider.tsx +++ b/src/modules/theme/ThemeProvider.tsx @@ -33,6 +33,8 @@ export type ThemeModePref = ThemePref; type ThemeProviderProps = { children: React.ReactNode; defaultMode?: ThemePref; + /** When false, skip the global wallpaper portal (e.g. settings overlay). */ + surfaceLayer?: boolean; }; type ThemeProviderState = { @@ -72,7 +74,11 @@ function resolveTheme(id: string, custom: Theme[]): Theme { return custom.find((t) => t.id === id) ?? getBuiltinTheme(id) ?? getDefaultTheme(); } -export function ThemeProvider({ children, defaultMode = "system" }: ThemeProviderProps) { +export function ThemeProvider({ + children, + defaultMode = "system", + surfaceLayer = true, +}: ThemeProviderProps) { const [mode, setModeState] = useState(() => readFastMode(defaultMode)); const [themeId, setThemeIdState] = useState(() => readFastThemeId()); const [customThemes, setCustomThemes] = useState([]); @@ -183,7 +189,7 @@ export function ThemeProvider({ children, defaultMode = "system" }: ThemeProvide return ( - + {surfaceLayer ? : null} {children} ); diff --git a/src/settings/SettingsApp.tsx b/src/settings/SettingsApp.tsx index 8beb6ed..036ae38 100644 --- a/src/settings/SettingsApp.tsx +++ b/src/settings/SettingsApp.tsx @@ -4,56 +4,23 @@ import { IS_MAC, USE_CUSTOM_WINDOW_CONTROLS } from "@/lib/platform"; import type { SettingsTab } from "@/modules/settings/openSettingsWindow"; import { usePreferencesStore } from "@/modules/settings/preferences"; import { - AiScanIcon, - InformationCircleIcon, - PaintBoardIcon, - Settings01Icon, - UserMultiple02Icon, - KeyboardIcon, -} from "@hugeicons/core-free-icons"; + normalizeSettingsTab, + SETTINGS_TABS, +} from "@/modules/settings/settingsTabs"; import { HugeiconsIcon } from "@hugeicons/react"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; -import { JSX, useEffect, useState } from "react"; -import { AboutSection } from "./sections/AboutSection"; -import { AgentsSection } from "./sections/AgentsSection"; -import { GeneralSection } from "./sections/GeneralSection"; -import { ModelsSection } from "./sections/ModelsSection"; -import { ShortcutsSection } from "./sections/ShortcutsSection"; -import { ThemesSection } from "./sections/ThemesSection"; - -const TABS: { id: SettingsTab; label: string; icon: typeof Settings01Icon, component: () => JSX.Element }[] = - [ - { id: "general", label: "通用", icon: Settings01Icon, component: GeneralSection }, - { id: "themes", label: "主题", icon: PaintBoardIcon, component: ThemesSection }, - { id: "shortcuts", label: "快捷键", icon: KeyboardIcon, component: ShortcutsSection }, - { id: "models", label: "模型", icon: AiScanIcon, component: ModelsSection }, - { id: "agents", label: "技能", icon: UserMultiple02Icon, component: AgentsSection }, - { id: "about", label: "关于", icon: InformationCircleIcon, component: AboutSection }, - ]; - -const VALID_TABS: SettingsTab[] = [ - "general", - "themes", - "shortcuts", - "models", - "agents", - "about", -]; +import { useEffect, useState } from "react"; function readInitialTab(): SettingsTab { if (typeof window === "undefined") return "general"; const url = new URL(window.location.href); - const t = url.searchParams.get("tab"); - // Back-compat: legacy "ai" / "connections" → "models". - if (t === "ai" || t === "connections") return "models"; - if (t && (VALID_TABS as string[]).includes(t)) return t as SettingsTab; - return "general"; + return normalizeSettingsTab(url.searchParams.get("tab")); } export function SettingsApp() { const [active, setActive] = useState(readInitialTab); const init = usePreferencesStore((s) => s.init); - const ActiveSection = TABS.find(t => t.id === active)?.component; + const ActiveSection = SETTINGS_TABS.find((t) => t.id === active)?.component; useEffect(() => { void init(); @@ -61,16 +28,10 @@ export function SettingsApp() { useEffect(() => { const apply = (detail: string) => { - if (detail === "ai" || detail === "connections") { - setActive("models"); - return; - } - if ((VALID_TABS as string[]).includes(detail)) { - setActive(detail as SettingsTab); - } + setActive(normalizeSettingsTab(detail)); }; const unlistenPromise = getCurrentWebviewWindow().listen( - "terax:settings-tab", + "gencode:settings-tab", (e) => apply(e.payload), ); return () => { @@ -79,10 +40,10 @@ export function SettingsApp() { }, []); return ( -
+
- {TABS.map((t) => ( + {SETTINGS_TABS.map((t) => ( }
-
+
{ActiveSection && }
); -} +} \ No newline at end of file diff --git a/src/settings/main.tsx b/src/settings/main.tsx index 9cc6f1a..f03cf21 100644 --- a/src/settings/main.tsx +++ b/src/settings/main.tsx @@ -17,7 +17,7 @@ if (USE_CUSTOM_WINDOW_CONTROLS) { ReactDOM.createRoot( document.getElementById("settings-root") as HTMLElement, ).render( - + , ); diff --git a/src/settings/sections/GeneralSection.tsx b/src/settings/sections/GeneralSection.tsx index 0a9f9a7..43a7445 100644 --- a/src/settings/sections/GeneralSection.tsx +++ b/src/settings/sections/GeneralSection.tsx @@ -29,6 +29,7 @@ import { setTerminalScrollback, setTerminalWebglEnabled, setVimMode, + setEditorWordWrap, setZoomLevel, } from "@/modules/settings/store"; import { useTheme } from "@/modules/theme"; @@ -58,12 +59,35 @@ const ZOOM_MIN = 0.5; const ZOOM_MAX = 2.0; const ZOOM_STEP = 0.05; +function SettingsCard({ + title, + children, + className, +}: { + title: string; + children: React.ReactNode; + className?: string; +}) { + return ( +
+

{title}

+ {children} +
+ ); +} + export function GeneralSection() { const { mode, setMode } = useTheme(); const autostart = usePreferencesStore((s) => s.autostart); const restoreWindowState = usePreferencesStore((s) => s.restoreWindowState); const vimMode = usePreferencesStore((s) => s.vimMode); + const editorWordWrap = usePreferencesStore((s) => s.editorWordWrap); const showHidden = usePreferencesStore((s) => s.showHidden); const terminalWebglEnabled = usePreferencesStore( (s) => s.terminalWebglEnabled, @@ -109,81 +133,120 @@ export function GeneralSection() { description="外观模式、编辑器与启动项。" /> -
- -
- {APPEARANCE.map((o) => ( - - ))} -
-

- 主题、背景与更多自定义选项请前往{" "} - 主题 标签页。 -

-
+
+ +
+ {APPEARANCE.map((o) => ( + + ))} +
+

+ 主题、背景与更多自定义选项请前往{" "} + 主题 页。 +

+
-
- -
-
- - UI 缩放级别 - - - {Math.round(zoomLevel * 100)}% - + +
+
+ + UI 缩放级别 + + + {Math.round(zoomLevel * 100)}% + +
+ void setZoomLevel(v[0] ?? 1)} + />
- void setZoomLevel(v[0] ?? 1)} - /> -
-
+ -
- - - void setVimMode(v)} - /> - -
+ + + void setVimMode(v)} + /> + + + void setEditorWordWrap(v)} + /> + + -
- - - void setShowHidden(v)} - /> - + + + void setShowHidden(v)} + /> + + + + + + void setAgentNotifications(v)} + /> + + + + + + void onToggleAutostart(v)} + /> + + + void setRestoreWindowState(v)} + /> + +
-
- + @@ -195,17 +258,13 @@ export function GeneralSection() { className="cursor-help text-[11px] text-muted-foreground/70 leading-none" aria-label="WebGL 渲染器更多信息" > - ⓘ + i - - xterm WebGL 渲染器将字形缓存在 GPU 纹理图集中。在某些 - macOS 环境下(尤其搭配 Nerd Font 使用时),图集可能损坏, - 导致终端文字无法辨认。关闭此选项可回退到 DOM 渲染器, - 性能略有下降但文字渲染正常。 + + xterm WebGL 渲染器将字形缓存在 GPU 纹理图集中。在某些 macOS + 环境下,图集可能损坏导致文字无法辨认。关闭可回退到 DOM + 渲染器。 @@ -220,7 +279,7 @@ export function GeneralSection() { -
- -
- - - void setAgentNotifications(v)} - /> - -
- -
- -
- - void onToggleAutostart(v)} - /> - - - void setRestoreWindowState(v)} - /> - -
-
+
); } - -function Label({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} From 23ecd76f6b85ab588719474e46e267e6f1afd64f Mon Sep 17 00:00:00 2001 From: bjlida Date: Wed, 3 Jun 2026 13:37:40 +0800 Subject: [PATCH 2/2] fix(ci): restore Windows release build with NSIS-only bundling GitHub Actions WiX/MSI step was failing on windows-latest; build NSIS installer and portable zip instead so Windows assets publish on release tags. Co-authored-by: Cursor --- .github/workflows/release.yml | 24 ++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b7caf54..d10e634 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,10 @@ jobs: - platform: ubuntu-22.04 args: "" rust-target: "" + - platform: windows-latest + # WiX/MSI light.exe fails on GitHub runners; NSIS matches custom installer UI. + args: --bundles nsis + rust-target: "" runs-on: ${{ matrix.platform }} steps: @@ -75,3 +79,23 @@ jobs: prerelease: false args: ${{ matrix.args }} + - name: Build portable (Windows) + if: matrix.platform == 'windows-latest' + shell: pwsh + run: | + $exe = "src-tauri/target/release/gencode.exe" + if (Test-Path $exe) { + $dest = "GenCode_${{ github.ref_name }}_x64-portable.zip" + Compress-Archive -Path $exe -DestinationPath $dest + Write-Output "portable_zip=$dest" >> $env:GITHUB_ENV + Write-Output "portable=yes" >> $env:GITHUB_ENV + } + + - name: Upload portable to release + if: env.portable == 'yes' + shell: bash + run: | + gh release upload "${{ github.ref_name }}" "${{ env.portable_zip }}" --clobber + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/README.md b/README.md index 7647430..d20ffad 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ cp .env.example .env |---|---|---| | macOS | Apple Silicon (aarch64) + Intel (x86_64) | `.dmg` | | Linux | x86_64 | `.deb`、`.AppImage`、`.rpm` | -| Windows | x86_64 | `.msi`、`.exe` (NSIS) | +| Windows | x86_64 | `.exe` (NSIS 安装包) + portable zip | 发布为草稿 (draft),确认无误后在 GitHub 手动发布。