@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.
- package/README.md +66 -2
- package/dist/tui.d.ts +299 -0
- package/dist/tui.js +1548 -0
- package/package.json +18 -27
- package/components.json +0 -16
- package/index.html +0 -12
- package/postcss.config.js +0 -3
- package/src/client/App.tsx +0 -96
- package/src/client/components/AgentCard.tsx +0 -60
- package/src/client/components/LogPanel.tsx +0 -43
- package/src/client/components/PipelineBar.tsx +0 -234
- package/src/client/components/ui/badge.tsx +0 -28
- package/src/client/components/ui/button.tsx +0 -43
- package/src/client/components/ui/card.tsx +0 -22
- package/src/client/components/ui/scroll-area.tsx +0 -43
- package/src/client/components/ui/separator.tsx +0 -23
- package/src/client/hooks/useWebSocket.ts +0 -33
- package/src/client/index.css +0 -31
- package/src/client/lib/utils.ts +0 -6
- package/src/client/main.tsx +0 -8
- package/src/client/store/useOrchestrationStore.ts +0 -132
- package/src/server/DashboardServer.ts +0 -56
- package/src/server/WebSocketHub.ts +0 -35
- package/src/server/index.ts +0 -2
- package/tailwind.config.js +0 -50
- package/tsconfig.json +0 -12
- package/tsconfig.server.json +0 -9
- package/vite.config.ts +0 -25
|
@@ -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
|
-
}
|
package/src/client/index.css
DELETED
|
@@ -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
|
-
}
|
package/src/client/lib/utils.ts
DELETED
package/src/client/main.tsx
DELETED
|
@@ -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
|
-
}
|
package/src/server/index.ts
DELETED
package/tailwind.config.js
DELETED
|
@@ -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
package/tsconfig.server.json
DELETED
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
|
-
})
|