@multiclawcli/dashboard 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +0,0 @@
1
- import * as React from "react"
2
- import * as SeparatorPrimitive from "@radix-ui/react-separator"
3
- import { cn } from "@/lib/utils"
4
-
5
- const Separator = React.forwardRef<
6
- React.ElementRef<typeof SeparatorPrimitive.Root>,
7
- React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
8
- >(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
9
- <SeparatorPrimitive.Root
10
- ref={ref}
11
- decorative={decorative}
12
- orientation={orientation}
13
- className={cn(
14
- "shrink-0 bg-border",
15
- orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
16
- className
17
- )}
18
- {...props}
19
- />
20
- ))
21
- Separator.displayName = SeparatorPrimitive.Root.displayName
22
-
23
- export { Separator }
@@ -1,33 +0,0 @@
1
- import { useEffect, useRef } from "react"
2
- import type { MultiClawEvent } from "@multiclawcli/core"
3
-
4
- export function useWebSocket(onEvent: (e: MultiClawEvent) => void): void {
5
- const ref = useRef(onEvent)
6
- ref.current = onEvent
7
- // Stop reconnecting after orchestration completes to avoid errors when server has shut down
8
- const doneRef = useRef(false)
9
-
10
- useEffect(() => {
11
- const wsUrl = `ws://${location.host}`
12
- let ws: WebSocket
13
- let retry: ReturnType<typeof setTimeout>
14
-
15
- const connect = () => {
16
- if (doneRef.current) return
17
- ws = new WebSocket(wsUrl)
18
- ws.onmessage = (msg) => {
19
- try {
20
- const event = JSON.parse(msg.data) as MultiClawEvent
21
- if (event.type === "orchestration:complete") doneRef.current = true
22
- ref.current(event)
23
- } catch {}
24
- }
25
- ws.onclose = () => {
26
- if (!doneRef.current) retry = setTimeout(connect, 1500)
27
- }
28
- }
29
- connect()
30
-
31
- return () => { clearTimeout(retry); ws?.close() }
32
- }, [])
33
- }
@@ -1,31 +0,0 @@
1
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
4
-
5
- @layer base {
6
- :root {
7
- --background: 0 0% 100%;
8
- --foreground: 222.2 84% 4.9%;
9
- --card: 0 0% 100%;
10
- --card-foreground: 222.2 84% 4.9%;
11
- --primary: 221.2 83.2% 53.3%;
12
- --primary-foreground: 210 40% 98%;
13
- --secondary: 210 40% 96.1%;
14
- --secondary-foreground: 222.2 47.4% 11.2%;
15
- --muted: 210 40% 96.1%;
16
- --muted-foreground: 215.4 16.3% 46.9%;
17
- --accent: 210 40% 96.1%;
18
- --accent-foreground: 222.2 47.4% 11.2%;
19
- --destructive: 0 84.2% 60.2%;
20
- --destructive-foreground: 210 40% 98%;
21
- --border: 214.3 31.8% 91.4%;
22
- --input: 214.3 31.8% 91.4%;
23
- --ring: 221.2 83.2% 53.3%;
24
- --radius: 0.5rem;
25
- }
26
- }
27
-
28
- @layer base {
29
- * { @apply border-border; }
30
- body { @apply bg-background text-foreground; }
31
- }
@@ -1,6 +0,0 @@
1
- import { clsx, type ClassValue } from "clsx"
2
- import { twMerge } from "tailwind-merge"
3
-
4
- export function cn(...inputs: ClassValue[]) {
5
- return twMerge(clsx(inputs))
6
- }
@@ -1,8 +0,0 @@
1
- import React from "react"
2
- import ReactDOM from "react-dom/client"
3
- import { App } from "./App"
4
- import "./index.css"
5
-
6
- ReactDOM.createRoot(document.getElementById("root")!).render(
7
- <React.StrictMode><App /></React.StrictMode>
8
- )
@@ -1,132 +0,0 @@
1
- import { create } from "zustand"
2
- import type { MultiClawEvent, StageInfo, AgentStatus } from "@multiclawcli/core"
3
-
4
- const stripAnsi = (str: string) => str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
5
-
6
- interface AgentState {
7
- id: string
8
- name: string
9
- icon?: string
10
- model?: string
11
- taskTitle?: string
12
- status: AgentStatus
13
- duration?: number
14
- logs: string
15
- }
16
-
17
- interface OrchestrationState {
18
- name: string
19
- requirement?: string
20
- running: boolean
21
- success?: boolean
22
- totalDuration?: number
23
- stages: StageInfo[]
24
- agents: Record<string, AgentState>
25
- // Agents that completed before orchestration:start (e.g. leader)
26
- preflightBuffer: Record<string, AgentState>
27
- preflightAgentIds: string[]
28
- apply: (e: MultiClawEvent) => void
29
- }
30
-
31
- export const useOrchestrationStore = create<OrchestrationState>((set) => ({
32
- name: "",
33
- requirement: undefined,
34
- running: false,
35
- stages: [],
36
- agents: {},
37
- preflightBuffer: {},
38
- preflightAgentIds: [],
39
- apply: (e) => set((state) => {
40
- switch (e.type) {
41
- case "orchestration:start": {
42
- // Move pre-flight agents (e.g. leader) into the main agents map
43
- const preflightAgentIds = Object.keys(state.preflightBuffer)
44
- const map: Record<string, AgentState> = { ...state.preflightBuffer }
45
- for (const stage of e.payload.stages) {
46
- for (const a of stage.agents) {
47
- map[a.id] = { id: a.id, name: a.name, icon: a.icon, model: a.model, taskTitle: a.taskTitle, status: "pending", logs: "" }
48
- }
49
- }
50
- return {
51
- name: e.payload.name, requirement: e.payload.requirement, running: true,
52
- stages: e.payload.stages, agents: map,
53
- preflightBuffer: {}, preflightAgentIds,
54
- }
55
- }
56
- case "agent:start": {
57
- const agentId = e.payload.agentId
58
- // If already a known pipeline agent, update in place
59
- if (agentId in state.agents) {
60
- return { agents: { ...state.agents, [agentId]: { ...state.agents[agentId], status: "running" } } }
61
- }
62
- // Otherwise buffer as pre-flight (will be merged at orchestration:start)
63
- const existing = state.preflightBuffer[agentId]
64
- return {
65
- preflightBuffer: {
66
- ...state.preflightBuffer,
67
- [agentId]: existing
68
- ? { ...existing, status: "running" }
69
- : { id: agentId, name: e.payload.agentName, icon: e.payload.icon, model: e.payload.model, taskTitle: e.payload.taskTitle, status: "running", logs: "" },
70
- },
71
- }
72
- }
73
- case "agent:output": {
74
- const { agentId, chunk } = e.payload
75
- const text = stripAnsi(chunk)
76
- if (agentId in state.agents) {
77
- const a = state.agents[agentId]
78
- return { agents: { ...state.agents, [agentId]: { ...a, logs: a.logs + text } } }
79
- }
80
- if (agentId in state.preflightBuffer) {
81
- const a = state.preflightBuffer[agentId]
82
- return { preflightBuffer: { ...state.preflightBuffer, [agentId]: { ...a, logs: a.logs + text } } }
83
- }
84
- return {}
85
- }
86
- case "agent:retrying": {
87
- const { agentId, attempt, maxAttempts } = e.payload
88
- const msg = `\n[retry] attempt ${attempt} of ${maxAttempts}...\n`
89
- if (agentId in state.agents) {
90
- const a = state.agents[agentId]
91
- return { agents: { ...state.agents, [agentId]: { ...a, status: "retrying", logs: a.logs + msg } } }
92
- }
93
- if (agentId in state.preflightBuffer) {
94
- const a = state.preflightBuffer[agentId]
95
- return { preflightBuffer: { ...state.preflightBuffer, [agentId]: { ...a, status: "retrying", logs: a.logs + msg } } }
96
- }
97
- return {}
98
- }
99
- case "agent:complete": {
100
- const { agentId, duration } = e.payload
101
- if (agentId in state.agents) {
102
- return { agents: { ...state.agents, [agentId]: { ...state.agents[agentId], status: "success", duration } } }
103
- }
104
- if (agentId in state.preflightBuffer) {
105
- return { preflightBuffer: { ...state.preflightBuffer, [agentId]: { ...state.preflightBuffer[agentId], status: "success", duration } } }
106
- }
107
- return {}
108
- }
109
- case "agent:failed": {
110
- const { agentId, duration } = e.payload
111
- if (agentId in state.agents) {
112
- return { agents: { ...state.agents, [agentId]: { ...state.agents[agentId], status: "failed", duration } } }
113
- }
114
- if (agentId in state.preflightBuffer) {
115
- return { preflightBuffer: { ...state.preflightBuffer, [agentId]: { ...state.preflightBuffer[agentId], status: "failed", duration } } }
116
- }
117
- return {}
118
- }
119
- case "agent:skipped": {
120
- const { agentId } = e.payload
121
- if (agentId in state.agents) {
122
- return { agents: { ...state.agents, [agentId]: { ...state.agents[agentId], status: "skipped" } } }
123
- }
124
- return {}
125
- }
126
- case "orchestration:complete":
127
- return { running: false, success: e.payload.success, totalDuration: e.payload.totalDuration }
128
- default:
129
- return {}
130
- }
131
- }),
132
- }))
@@ -1,56 +0,0 @@
1
- import * as http from "http"
2
- import * as path from "path"
3
- import * as fs from "fs"
4
- import { WebSocketServer } from "ws"
5
- import type { EventBus } from "@multiclawcli/core"
6
- import { WebSocketHub } from "./WebSocketHub"
7
-
8
- export interface DashboardServerOptions {
9
- port: number
10
- acceptClientEvents?: boolean
11
- }
12
-
13
- export class DashboardServer {
14
- private server: http.Server
15
- private hub: WebSocketHub
16
- private port: number
17
-
18
- constructor(options: DashboardServerOptions) {
19
- this.port = options.port
20
-
21
- const clientDist = path.resolve(new URL(import.meta.url).pathname, "../../client")
22
-
23
- this.server = http.createServer((req, res) => {
24
- let filePath = path.join(
25
- clientDist,
26
- req.url === "/" ? "index.html" : req.url!
27
- )
28
- if (!fs.existsSync(filePath)) filePath = path.join(clientDist, "index.html")
29
-
30
- const ext = path.extname(filePath)
31
- const mime: Record<string, string> = {
32
- ".html": "text/html", ".js": "text/javascript",
33
- ".css": "text/css", ".svg": "image/svg+xml",
34
- }
35
- const stream = fs.createReadStream(filePath)
36
- stream.on("error", () => { res.writeHead(404); res.end() })
37
- res.writeHead(200, { "Content-Type": mime[ext] ?? "text/plain" })
38
- stream.pipe(res)
39
- })
40
-
41
- const wss = new WebSocketServer({ server: this.server })
42
- this.hub = new WebSocketHub(wss, options.acceptClientEvents)
43
- }
44
-
45
- start(): Promise<void> {
46
- return new Promise((resolve) => this.server.listen(this.port, () => resolve()))
47
- }
48
-
49
- bindEventBus(eventBus: EventBus): void {
50
- eventBus.subscribe((e) => this.hub.broadcast(e))
51
- }
52
-
53
- stop(): void {
54
- this.server.close()
55
- }
56
- }
@@ -1,35 +0,0 @@
1
- import { WebSocketServer, WebSocket } from "ws"
2
- import type { MultiClawEvent } from "@multiclawcli/core"
3
-
4
- export class WebSocketHub {
5
- private wss: WebSocketServer
6
- private clients = new Set<WebSocket>()
7
- private history: MultiClawEvent[] = []
8
-
9
- constructor(wss: WebSocketServer, acceptClientEvents = false) {
10
- this.wss = wss
11
- this.wss.on("connection", (ws) => {
12
- this.clients.add(ws)
13
- for (const e of this.history) ws.send(JSON.stringify(e))
14
-
15
- if (acceptClientEvents) {
16
- ws.on("message", (data) => {
17
- try {
18
- const event = JSON.parse(data.toString()) as MultiClawEvent
19
- this.broadcast(event)
20
- } catch { /* ignore */ }
21
- })
22
- }
23
-
24
- ws.on("close", () => this.clients.delete(ws))
25
- })
26
- }
27
-
28
- broadcast(event: MultiClawEvent): void {
29
- this.history.push(event)
30
- const msg = JSON.stringify(event)
31
- for (const ws of this.clients) {
32
- if (ws.readyState === ws.OPEN) ws.send(msg)
33
- }
34
- }
35
- }
@@ -1,2 +0,0 @@
1
- export { DashboardServer } from "./DashboardServer"
2
- export type { DashboardServerOptions } from "./DashboardServer"
@@ -1,50 +0,0 @@
1
- import tailwindAnimate from "tailwindcss-animate"
2
-
3
- export default {
4
- content: ["./src/client/**/*.{ts,tsx,html}", "./index.html"],
5
- theme: {
6
- extend: {
7
- colors: {
8
- border: "hsl(var(--border))",
9
- input: "hsl(var(--input))",
10
- ring: "hsl(var(--ring))",
11
- background: "hsl(var(--background))",
12
- foreground: "hsl(var(--foreground))",
13
- primary: {
14
- DEFAULT: "hsl(var(--primary))",
15
- foreground: "hsl(var(--primary-foreground))",
16
- },
17
- secondary: {
18
- DEFAULT: "hsl(var(--secondary))",
19
- foreground: "hsl(var(--secondary-foreground))",
20
- },
21
- destructive: {
22
- DEFAULT: "hsl(var(--destructive))",
23
- foreground: "hsl(var(--destructive-foreground))",
24
- },
25
- muted: {
26
- DEFAULT: "hsl(var(--muted))",
27
- foreground: "hsl(var(--muted-foreground))",
28
- },
29
- accent: {
30
- DEFAULT: "hsl(var(--accent))",
31
- foreground: "hsl(var(--accent-foreground))",
32
- },
33
- card: {
34
- DEFAULT: "hsl(var(--card))",
35
- foreground: "hsl(var(--card-foreground))",
36
- },
37
- },
38
- borderRadius: {
39
- lg: "var(--radius)",
40
- md: "calc(var(--radius) - 2px)",
41
- sm: "calc(var(--radius) - 4px)",
42
- },
43
- keyframes: {
44
- "pulse-fast": { "0%, 100%": { opacity: "1" }, "50%": { opacity: ".5" } },
45
- },
46
- animation: { "pulse-fast": "pulse-fast 1s ease-in-out infinite" },
47
- },
48
- },
49
- plugins: [tailwindAnimate],
50
- }
package/tsconfig.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "lib": ["ES2021", "DOM"],
5
- "jsx": "react-jsx",
6
- "noEmit": true,
7
- "paths": {
8
- "@/*": ["./src/client/*"]
9
- }
10
- },
11
- "include": ["src/client"]
12
- }
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "dist/server",
5
- "rootDir": "src/server",
6
- "lib": ["ES2021"]
7
- },
8
- "include": ["src/server"]
9
- }
package/vite.config.ts DELETED
@@ -1,25 +0,0 @@
1
- import { defineConfig } from "vite"
2
- import react from "@vitejs/plugin-react"
3
- import * as path from "path"
4
-
5
- export default defineConfig({
6
- plugins: [react()],
7
- root: ".",
8
- define: {
9
- // ws 等 Node.js 包引用了 global,浏览器端需要映射到 globalThis
10
- global: "globalThis",
11
- },
12
- build: {
13
- outDir: "dist/client",
14
- emptyOutDir: true,
15
- },
16
- resolve: {
17
- alias: {
18
- "@multiclawcli/core": path.resolve(__dirname, "../core/src"),
19
- "@": path.resolve(__dirname, "./src/client"),
20
- },
21
- },
22
- optimizeDeps: {
23
- exclude: ["ws", "child_process", "fs", "path"],
24
- },
25
- })