feat(desktop): 命令面板 ⌘K —— 键盘优先工作站入口

Pillar ① 交互骨架升级第一步:全局命令面板。

- components/CommandPalette.tsx:⌘K/Ctrl+K 唤起,搜索 + 多词子串过滤 + 分组(页面/动作)
  + 键盘上下选择 + Enter 执行 + Esc 关闭 + 选中项滚动入视野,纯前端两模式通用。
- App:全局 keydown 监听切换面板;命令清单(8 个页面跳转 + 生成报告/入库知识/新建编排动作)。
- TopBar:加「搜索与命令 ⌘K」触发 pill。

验证(Preview):⌘K/点 pill 唤起 → 输入“报告”过滤出 2 条 → 点「前往·报告生成」自动跳转
报告页并关闭面板。tsc + vite build 通过;重建 .app 重启原生窗口。

注:OS 级全局唤起(⌥Space 后台常驻 + 系统托盘)受 Wails v2 主线程模型限制(与
golang.design/x/hotkey 抢主线程冲突),留作 Wails v3 / cgo 助手的后续,不在本次。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-13 14:32:36 +08:00
parent 72e008bfe8
commit 84efa0a11c
3 changed files with 156 additions and 4 deletions
+35 -2
View File
@@ -1,4 +1,5 @@
import { useCallback, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { LayoutDashboard, Workflow, Database, FileText, Activity, Bookmark, Boxes, Settings } from "lucide-react";
import { TopBar } from "./shell/TopBar";
import { LeftNav, type ViewKey } from "./shell/LeftNav";
@@ -10,6 +11,7 @@ import { ReportView } from "./views/ReportView";
import { RunsView } from "./views/RunsView";
import { Home } from "./views/Home";
import { Placeholder } from "./views/Placeholder";
import { CommandPalette, type Command } from "./components/CommandPalette";
import { submitTask, streamTokens, streamExec, type Identity } from "./lib/api";
import type { TaskDsl } from "./lib/dsl";
import { emptyRun, type RunState } from "./lib/run";
@@ -28,10 +30,40 @@ export default function App() {
const [view, setView] = useState<ViewKey>("home");
const [identity, setIdentity] = useState<Identity>({ userId: "wt", sessionId: "sess-ui" });
const [run, setRun] = useState<RunState>(emptyRun);
const [cmdOpen, setCmdOpen] = useState(false);
const closeRef = useRef<(() => void) | null>(null);
const execCloseRef = useRef<(() => void) | null>(null);
// 全局 ⌘K / Ctrl+K 唤起命令面板(键盘优先工作站入口)。
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
setCmdOpen((o) => !o);
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
const commands = useMemo<Command[]>(() => {
const go = (key: ViewKey) => () => setView(key);
return [
{ id: "home", label: "前往 · 工作台", icon: LayoutDashboard, group: "页面", keywords: "home dashboard 概览", run: go("home") },
{ id: "studio", label: "前往 · 编排", icon: Workflow, group: "页面", keywords: "studio agent graph 编排 图", run: go("studio") },
{ id: "kb", label: "前往 · 知识库", icon: Database, group: "页面", keywords: "kb rag 检索 图谱 入库", run: go("kb") },
{ id: "report", label: "前往 · 报告生成", icon: FileText, group: "页面", keywords: "report 报告 word docx", run: go("report") },
{ id: "runs", label: "前往 · 运行观测", icon: Activity, group: "页面", keywords: "runs trace 轨迹 观测", run: go("runs") },
{ id: "memory", label: "前往 · 记忆", icon: Bookmark, group: "页面", keywords: "memory 画像 偏好", run: go("memory") },
{ id: "market", label: "前往 · 市场 Packs", icon: Boxes, group: "页面", keywords: "market pack 垂直", run: go("market") },
{ id: "admin", label: "前往 · 管理", icon: Settings, group: "页面", keywords: "admin 租户 模型 设置", run: go("admin") },
{ id: "act-report", label: "生成报告", icon: FileText, group: "动作", keywords: "新建 report 撰写", run: go("report") },
{ id: "act-ingest", label: "入库知识", icon: Database, group: "动作", keywords: "上传 ingest 文件", run: go("kb") },
{ id: "act-studio", label: "新建 Agent 编排", icon: Workflow, group: "动作", keywords: "new flow", run: go("studio") },
];
}, []);
const onRun = useCallback(
async (dsl: TaskDsl) => {
closeRef.current?.();
@@ -85,7 +117,7 @@ export default function App() {
className="pointer-events-none absolute inset-x-0 top-0 h-64 opacity-60"
style={{ background: "radial-gradient(60% 100% at 50% 0%, rgba(124,92,246,0.10), transparent 70%)" }}
/>
<TopBar identity={identity} setIdentity={setIdentity} />
<TopBar identity={identity} setIdentity={setIdentity} onCommand={() => setCmdOpen(true)} />
<div className="relative flex min-h-0 flex-1">
<LeftNav active={view} onSelect={setView} />
<main className="min-w-0 flex-1 overflow-hidden">
@@ -107,6 +139,7 @@ export default function App() {
</main>
</div>
<BottomDrawer run={run} />
<CommandPalette open={cmdOpen} onClose={() => setCmdOpen(false)} commands={commands} />
</div>
</ToastProvider>
);