diff --git a/.claude/skills/roast-skill b/.claude/skills/roast-skill new file mode 160000 index 00000000..f2302d23 --- /dev/null +++ b/.claude/skills/roast-skill @@ -0,0 +1 @@ +Subproject commit f2302d2371fbaf16adb46363a89f9f549157693d diff --git a/.cursor/hooks.json b/.cursor/hooks.json new file mode 100644 index 00000000..ccbb5227 --- /dev/null +++ b/.cursor/hooks.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "hooks": { + "afterAgentResponse": [ + { + "command": ".cursor/hooks/say-complete.sh" + } + ] + } +} diff --git a/.cursor/hooks/say-complete.sh b/.cursor/hooks/say-complete.sh new file mode 100755 index 00000000..e613a279 --- /dev/null +++ b/.cursor/hooks/say-complete.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Read and discard hook payload from stdin. +cat >/dev/null + +if command -v say >/dev/null 2>&1; then + say "已完成" >/dev/null 2>&1 & +fi + +printf '{}\n' diff --git a/apps/demo/src/app/app-error-boundary.tsx b/apps/demo/src/app/app-error-boundary.tsx new file mode 100644 index 00000000..3221187c --- /dev/null +++ b/apps/demo/src/app/app-error-boundary.tsx @@ -0,0 +1,49 @@ +import { Component, type ErrorInfo, type ReactNode } from "react"; + +type Props = { children: ReactNode }; + +type State = { error: Error | null }; + +export class AppErrorBoundary extends Component { + state: State = { error: null }; + + static getDerivedStateFromError(error: Error): State { + return { error }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + console.error("Demo app error:", error, info.componentStack); + } + + render() { + if (this.state.error) { + return ( +
+

页面加载出错

+

+ 请打开开发者工具(Console)查看完整堆栈,或把下方错误信息发给开发者。 +

+
+            {this.state.error.message}
+          
+
+ ); + } + return this.props.children; + } +} diff --git a/apps/slark/src/renderer/src/components/onboarding/ConnectRuntimeStep.tsx b/apps/slark/src/renderer/src/components/onboarding/ConnectRuntimeStep.tsx deleted file mode 100644 index 56e9c88a..00000000 --- a/apps/slark/src/renderer/src/components/onboarding/ConnectRuntimeStep.tsx +++ /dev/null @@ -1,403 +0,0 @@ -import { Button, cn } from "@nexu-design/ui-web"; -import { - AlertTriangle, - ArrowRight, - Check, - ExternalLink, - Loader2, - RefreshCw, - Search, - X, -} from "lucide-react"; -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; - -import { useRuntimesStore } from "@/stores/runtimes"; -import type { Runtime } from "@/types"; - -const INSTALL_GUIDES: { - type: string; - name: string; - desc: string; - install: string; - docsUrl: string; - docsLabel: string; -}[] = [ - { - type: "claude-code", - name: "Claude Code", - desc: "Anthropic's coding agent", - install: "npm install -g @anthropic-ai/claude-code", - docsUrl: "https://docs.claude.com/en/docs/claude-code/overview", - docsLabel: "docs.claude.com", - }, - { - type: "cursor", - name: "Cursor", - desc: "AI-first code editor", - install: "Download the desktop app", - docsUrl: "https://docs.cursor.com/get-started/installation", - docsLabel: "docs.cursor.com", - }, - { - type: "opencode", - name: "OpenCode", - desc: "Open-source coding agent", - install: "npm install -g opencode-ai", - docsUrl: "https://opencode.ai/docs", - docsLabel: "opencode.ai/docs", - }, - { - type: "codex", - name: "Codex", - desc: "OpenAI coding agent", - install: "npm install -g @openai/codex", - docsUrl: "https://github.com/openai/codex#installation", - docsLabel: "github.com/openai/codex", - }, - { - type: "gemini-cli", - name: "Gemini CLI", - desc: "Google's AI coding agent", - install: "npm install -g @google/gemini-cli", - docsUrl: "https://github.com/google-gemini/gemini-cli#quickstart", - docsLabel: "github.com/google-gemini/gemini-cli", - }, - { - type: "hermes", - name: "Hermes", - desc: "Nous Research agent", - install: "See repo for install steps", - docsUrl: "https://github.com/nousresearch/hermes-agent", - docsLabel: "github.com/nousresearch/hermes-agent", - }, - { - type: "openclaw", - name: "OpenClaw", - desc: "Multi-agent orchestrator", - install: "npm install -g openclaw", - docsUrl: "https://github.com/openclaw/openclaw#install", - docsLabel: "github.com/openclaw", - }, - { - type: "pi", - name: "Pi", - desc: "Conversational AI assistant", - install: "brew install pi-cli", - docsUrl: "https://pi.ai/docs/cli", - docsLabel: "pi.ai/docs", - }, -]; - -interface DetectedRuntime { - type: string; - name: string; - desc: string; - detected: boolean; - version?: string; - path?: string; - error?: string; -} - -const RUNTIME_BRANDS: Record = { - "claude-code": { label: ">_", color: "#B45309" }, - cursor: { label: "▸", color: "#171717" }, - opencode: { label: "", color: "#059669" }, - codex: { label: "◎", color: "#0369A1" }, - "gemini-cli": { label: "✦", color: "#4285F4" }, - hermes: { label: "H", color: "#EA580C" }, - openclaw: { label: "⊞", color: "#7C3AED" }, - pi: { label: "π", color: "#DB2777" }, -}; - -const SCAN_DURATION = 3500; -const STAGGER_DELAY = 400; - -export function ConnectRuntimeStep(): React.ReactElement { - const navigate = useNavigate(); - const setGlobalRuntimes = useRuntimesStore((s) => s.setRuntimes); - const devSimulateNone = useRuntimesStore((s) => s.devSimulateNone); - const [phase, setPhase] = useState<"scanning" | "done">("scanning"); - const [scanProgress, setScanProgress] = useState(0); - const [runtimes, setRuntimes] = useState([ - { type: "claude-code", name: "Claude Code", desc: "Anthropic's coding agent", detected: false }, - { type: "cursor", name: "Cursor", desc: "AI-first code editor", detected: false }, - { type: "opencode", name: "OpenCode", desc: "Open-source coding agent", detected: false }, - { type: "codex", name: "Codex", desc: "OpenAI coding agent", detected: false }, - { type: "gemini-cli", name: "Gemini CLI", desc: "Google's AI coding agent", detected: false }, - { type: "hermes", name: "Hermes", desc: "Local LLM runtime", detected: false }, - { type: "openclaw", name: "OpenClaw", desc: "Multi-agent orchestrator", detected: false }, - { type: "pi", name: "Pi", desc: "Conversational AI assistant", detected: false }, - ]); - const [selected, setSelected] = useState>(new Set()); - - useEffect(() => { - const start = Date.now(); - const progressInterval = setInterval(() => { - const elapsed = Date.now() - start; - setScanProgress(Math.min(elapsed / SCAN_DURATION, 1)); - }, 50); - - const mockResults: { index: number; version: string; path: string; error?: string }[] = - devSimulateNone - ? [] - : [ - { index: 0, version: "1.0.12", path: "/usr/local/bin/claude" }, - { index: 2, version: "0.5.3", path: "/usr/local/bin/opencode" }, - { index: 4, version: "0.1.0", path: "/usr/local/bin/gemini" }, - { - index: 6, - version: "0.2.1", - path: "/usr/local/bin/openclaw", - error: "Requires update (min v0.3.0)", - }, - { index: 7, version: "1.3.0", path: "/usr/local/bin/pi", error: "Auth token expired" }, - ]; - - let revealedCount = 0; - let finished = false; - const finishScan = (): void => { - if (finished) return; - finished = true; - setPhase("done"); - clearInterval(progressInterval); - setScanProgress(1); - }; - - const timers = mockResults.map(({ index, version, path, error }, i) => - setTimeout( - () => { - setRuntimes((prev) => - prev.map((rt, j) => - j === index ? { ...rt, detected: true, version, path, error } : rt, - ), - ); - revealedCount++; - if (revealedCount === mockResults.length) { - setTimeout(finishScan, 400); - } - }, - STAGGER_DELAY * (i + 2), - ), - ); - - const doneTimer = setTimeout(finishScan, SCAN_DURATION); - - return () => { - clearInterval(progressInterval); - timers.forEach(clearTimeout); - clearTimeout(doneTimer); - }; - }, [devSimulateNone]); - - useEffect(() => { - if (phase === "done") { - const workingTypes = runtimes.filter((r) => r.detected && !r.error).map((r) => r.type); - setSelected(new Set(workingTypes)); - } - }, [phase, runtimes]); - - const toggleSelect = (type: string): void => { - const rt = runtimes.find((r) => r.type === type); - if (!rt?.detected || rt.error) return; - setSelected((prev) => { - const next = new Set(prev); - if (next.has(type)) next.delete(type); - else next.add(type); - return next; - }); - }; - - const errorCount = runtimes.filter((r) => r.detected && r.error).length; - const detectedCount = runtimes.filter((r) => r.detected).length; - const showTutorial = phase === "done" && detectedCount === 0; - - return ( -
-
-

- {phase === "scanning" - ? "Detecting Runtimes..." - : showTutorial - ? "No Runtimes Found" - : "Runtimes Detected"} -

-

- {phase === "scanning" - ? "Scanning your system for installed AI runtimes" - : showTutorial - ? "Install a runtime below, then rescan." - : `Found ${detectedCount} runtime${detectedCount !== 1 ? "s" : ""} on your system${errorCount > 0 ? ` · ${errorCount} need attention` : ""}`} -

- {!showTutorial && ( -

- Runtimes power your Agents — each agent connects to a runtime to execute tasks. -
- Once set up, you can @mention agents in chat to assign work, just like messaging a - teammate. -

- )} -
- - {!showTutorial && ( -
- {phase === "scanning" && ( - <> -
- - - Checking PATH and common install locations... - -
-
-
-
- - )} -
- )} - - {showTutorial && ( -
- {INSTALL_GUIDES.map((g) => { - const brand = RUNTIME_BRANDS[g.type]; - return ( - -
-
- {brand?.label ?? "?"} -
- -
-
{g.name}
-
- {g.docsLabel} -
-
- ); - })} -
- )} - - {!showTutorial && ( -
- {runtimes.map(({ type, name, desc, detected, version, path, error }) => { - const isSelected = selected.has(type); - const isScanning = phase === "scanning" && !detected; - const isError = detected && !!error; - const isWorking = detected && !error; - const brand = RUNTIME_BRANDS[type]; - return ( - - ); - })} -
- )} - -
- - {showTutorial ? ( - - ) : ( - - )} -
-
- ); -} diff --git a/apps/slark/src/renderer/src/components/onboarding/CreateAgentStep.tsx b/apps/slark/src/renderer/src/components/onboarding/CreateAgentStep.tsx deleted file mode 100644 index c7f8aa20..00000000 --- a/apps/slark/src/renderer/src/components/onboarding/CreateAgentStep.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { ArrowLeft, Rocket, Plus, Search } from "lucide-react"; -import { - Button, - Dialog, - DialogBody, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - FormField, - FormFieldControl, - Input, - InteractiveRow, - InteractiveRowContent, - InteractiveRowLeading, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - Textarea, - cn, -} from "@nexu-design/ui-web"; -import { useWorkspaceStore } from "@/stores/workspace"; -import { useAgentsStore } from "@/stores/agents"; -import { useRuntimesStore } from "@/stores/runtimes"; -import { mockAgentTemplates, mockRuntimes } from "@/mock/data"; -import type { AgentTemplate } from "@/types"; - -type Phase = "templates" | "settings"; - -export function CreateAgentStep(): React.ReactElement { - const navigate = useNavigate(); - const completeOnboarding = useWorkspaceStore((s) => s.completeOnboarding); - const setPendingWelcomeAgent = useWorkspaceStore((s) => s.setPendingWelcomeAgent); - const addAgent = useAgentsStore((s) => s.addAgent); - const runtimes = useRuntimesStore((s) => s.runtimes); - const setGlobalRuntimes = useRuntimesStore((s) => s.setRuntimes); - - const [phase, setPhase] = useState("templates"); - const [selectedTemplate, setSelectedTemplate] = useState(null); - - const [agentName, setAgentName] = useState(""); - const [description, setDescription] = useState(""); - const [runtimeId, setRuntimeId] = useState(null); - const [showDiscardDialog, setShowDiscardDialog] = useState(false); - - const handleSelectTemplate = (tpl: AgentTemplate): void => { - setSelectedTemplate(tpl); - setAgentName(tpl.name); - setDescription(tpl.description); - const firstConnected = runtimes.find((r) => r.status === "connected"); - if (firstConnected) setRuntimeId(firstConnected.id); - setPhase("settings"); - }; - - const handleSkip = (): void => { - completeOnboarding(); - navigate("/chat/ch-welcome"); - }; - - const handleBlankAgent = (): void => { - setSelectedTemplate(null); - setAgentName(""); - setDescription(""); - const firstConnected = runtimes.find((r) => r.status === "connected"); - if (firstConnected) setRuntimeId(firstConnected.id); - setPhase("settings"); - }; - - const handleBackToTemplates = (): void => { - const nameChanged = selectedTemplate ? agentName !== selectedTemplate.name : agentName !== ""; - const descChanged = selectedTemplate - ? description !== selectedTemplate.description - : description !== ""; - if (nameChanged || descChanged) { - setShowDiscardDialog(true); - return; - } - setPhase("templates"); - }; - - const handleDetectRuntimes = (): void => { - const connected = mockRuntimes.filter((r) => r.status === "connected"); - setGlobalRuntimes(connected); - if (connected.length > 0) setRuntimeId(connected[0].id); - }; - - const handleCreate = (): void => { - if (!agentName.trim()) return; - const agentId = `a-${Date.now()}`; - addAgent({ - id: agentId, - name: agentName.trim(), - avatar: - selectedTemplate?.avatar ?? - `https://api.dicebear.com/9.x/bottts/svg?seed=${agentId}&backgroundColor=6366f1`, - description: description.trim(), - systemPrompt: - selectedTemplate?.defaultPrompt ?? `You are ${agentName.trim()}, a helpful AI assistant.`, - status: "online", - skills: [], - runtimeId, - templateId: selectedTemplate?.id ?? null, - createdBy: "u-1", - createdAt: Date.now(), - }); - setPendingWelcomeAgent(agentId); - completeOnboarding(); - navigate("/chat/ch-welcome"); - }; - - const connectedRuntimes = runtimes.filter((r) => r.status === "connected"); - - if (phase === "templates") { - return ( -
-
-

Create your first Agent

-

Choose a template to get started

-
-
- {mockAgentTemplates.map((tpl) => ( - handleSelectTemplate(tpl)} - tone="subtle" - className="items-start rounded-xl border border-border px-3 py-3" - > - - - - -
{tpl.name}
-
- {tpl.description} -
-
-
- ))} -
- - -
- ); - } - - return ( -
-
-

Customize your Agent

-

Set a name, description, and connect a runtime

-
- -
- {selectedTemplate && ( -
- -
-
Based on template
-
{selectedTemplate.name}
-
-
- )} - - - - setAgentName(e.target.value)} - placeholder="e.g. CodeBot" - autoFocus - /> - - - - - -