Refactor ActionGraph and related components for improved type safety and code clarity. Removed unnecessary peer dependency in package-lock.json. Updated session and action node components to use explicit types and fixed minor styling issues. Enhanced local collection options in clawdbot integration and demo-db. Adjusted live query handling in demo and monitor routes for better data management.

This commit is contained in:
luccast
2026-01-25 17:59:15 -05:00
parent 58973da692
commit 4d82b71a7a
15 changed files with 98 additions and 69 deletions
-1
View File
@@ -2154,7 +2154,6 @@
"integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
+23 -22
View File
@@ -24,9 +24,10 @@ interface ActionGraphProps {
onSessionSelect: (key: string | null) => void
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nodeTypes: NodeTypes = {
session: SessionNode,
action: ActionNode,
session: SessionNode as any,
action: ActionNode as any,
}
export function ActionGraph({
@@ -55,7 +56,7 @@ export function ActionGraph({
id: `session-${session.key}`,
type: 'session',
position: { x: 0, y: 0 },
data: session,
data: session as unknown as Record<string, unknown>,
})
}
@@ -65,7 +66,7 @@ export function ActionGraph({
id: `action-${action.id}`,
type: 'action',
position: { x: 0, y: 0 },
data: action,
data: action as unknown as Record<string, unknown>,
})
}
@@ -87,8 +88,8 @@ export function ActionGraph({
// Connect session to first action of each run
for (const [runId, runActs] of runActions) {
const sorted = [...runActs].sort((a, b) => a.seq - b.seq)
if (sorted.length > 0) {
const first = sorted[0]
const first = sorted[0]
if (first) {
const sessionId = `session-${first.sessionKey}`
edges.push({
id: `e-session-${runId}`,
@@ -98,20 +99,20 @@ export function ActionGraph({
markerEnd: { type: MarkerType.ArrowClosed },
style: { stroke: '#6b7280' },
})
}
// Connect actions in sequence
for (let i = 1; i < sorted.length; i++) {
const prev = sorted[i - 1]
const curr = sorted[i]
edges.push({
id: `e-${prev.id}-${curr.id}`,
source: `action-${prev.id}`,
target: `action-${curr.id}`,
animated: curr.type === 'delta',
markerEnd: { type: MarkerType.ArrowClosed },
style: { stroke: '#6b7280' },
})
// Connect actions in sequence
for (let i = 1; i < sorted.length; i++) {
const prev = sorted[i - 1]!
const curr = sorted[i]!
edges.push({
id: `e-${prev.id}-${curr.id}`,
source: `action-${prev.id}`,
target: `action-${curr.id}`,
animated: curr.type === 'delta',
markerEnd: { type: MarkerType.ArrowClosed },
style: { stroke: '#6b7280' },
})
}
}
}
@@ -142,7 +143,7 @@ export function ActionGraph({
const onNodeClick = useCallback(
(_: React.MouseEvent, node: Node) => {
if (node.type === 'session') {
const sessionKey = (node.data as MonitorSession).key
const sessionKey = (node.data as unknown as MonitorSession).key
onSessionSelect(selectedSession === sessionKey ? null : sessionKey)
}
},
@@ -165,9 +166,9 @@ export function ActionGraph({
proOptions={{ hideAttribution: true }}
>
<Background color="#374151" gap={20} />
<Controls className="!bg-gray-800 !border-gray-700 !rounded-lg" />
<Controls className="bg-gray-800! !border-gray-700! rounded-lg!" />
<MiniMap
className="!bg-gray-800 !border-gray-700 !rounded-lg"
className="bg-gray-800! border-gray-700! rounded-lg!"
nodeColor={(node) => {
if (node.type === 'session') return '#06b6d4'
return '#6b7280'
+19 -8
View File
@@ -1,12 +1,23 @@
import { memo, useState } from 'react'
import { Handle, Position, type NodeProps } from '@xyflow/react'
import { Handle, Position } from '@xyflow/react'
import { motion } from 'framer-motion'
import { Loader2, CheckCircle, XCircle, Wrench, MessageSquare } from 'lucide-react'
import type { MonitorAction } from '~/integrations/clawdbot'
type ActionNodeData = MonitorAction
interface ActionNodeProps {
data: MonitorAction
selected?: boolean
}
const typeConfig = {
const typeConfig: Record<
MonitorAction['type'],
{
icon: typeof Loader2
color: string
iconColor: string
animate: boolean
}
> = {
delta: {
icon: Loader2,
color: 'border-blue-500 bg-blue-500/10',
@@ -48,7 +59,7 @@ const typeConfig = {
export const ActionNode = memo(function ActionNode({
data,
selected,
}: NodeProps<ActionNodeData>) {
}: ActionNodeProps) {
const [expanded, setExpanded] = useState(false)
const config = typeConfig[data.type]
const Icon = config.icon
@@ -71,7 +82,7 @@ export const ActionNode = memo(function ActionNode({
${selected ? 'ring-2 ring-white/50' : ''}
`}
>
<Handle type="target" position={Position.Top} className="!bg-gray-500 !w-2 !h-2" />
<Handle type="target" position={Position.Top} className="bg-gray-500! w-2! h-2!" />
<div className="flex items-center gap-2 mb-1">
<Icon
@@ -98,13 +109,13 @@ export const ActionNode = memo(function ActionNode({
</div>
)}
{expanded && data.toolArgs && (
{expanded && data.toolArgs != null && (
<pre className="mt-2 text-[10px] text-gray-400 bg-black/30 p-1 rounded overflow-auto max-h-32">
{JSON.stringify(data.toolArgs, null, 2)}
{JSON.stringify(data.toolArgs, null, 2) as string}
</pre>
)}
<Handle type="source" position={Position.Bottom} className="!bg-gray-500 !w-2 !h-2" />
<Handle type="source" position={Position.Bottom} className="bg-gray-500! w-2! h-2!" />
</motion.div>
)
})
+1 -1
View File
@@ -1,6 +1,6 @@
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Search, Filter } from 'lucide-react'
import { Search } from 'lucide-react'
import { StatusIndicator } from './StatusIndicator'
import type { MonitorSession } from '~/integrations/clawdbot'
+9 -6
View File
@@ -1,11 +1,14 @@
import { memo } from 'react'
import { Handle, Position, type NodeProps } from '@xyflow/react'
import { Handle, Position } from '@xyflow/react'
import { motion } from 'framer-motion'
import { MessageSquare, Users, User } from 'lucide-react'
import { Users, User } from 'lucide-react'
import { StatusIndicator } from './StatusIndicator'
import type { MonitorSession } from '~/integrations/clawdbot'
type SessionNodeData = MonitorSession
interface SessionNodeProps {
data: MonitorSession
selected?: boolean
}
const platformIcons: Record<string, string> = {
whatsapp: '💬',
@@ -17,7 +20,7 @@ const platformIcons: Record<string, string> = {
export const SessionNode = memo(function SessionNode({
data,
selected,
}: NodeProps<SessionNodeData>) {
}: SessionNodeProps) {
const platformIcon = platformIcons[data.platform] ?? '📱'
return (
@@ -31,7 +34,7 @@ export const SessionNode = memo(function SessionNode({
${data.status === 'thinking' ? 'border-yellow-500' : ''}
`}
>
<Handle type="target" position={Position.Top} className="!bg-gray-500" />
<Handle type="target" position={Position.Top} className="bg-gray-500!" />
<div className="flex items-center gap-2 mb-2">
<span className="text-lg">{platformIcon}</span>
@@ -52,7 +55,7 @@ export const SessionNode = memo(function SessionNode({
{data.agentId}
</div>
<Handle type="source" position={Position.Bottom} className="!bg-gray-500" />
<Handle type="source" position={Position.Bottom} className="bg-gray-500!" />
</motion.div>
)
})
+1 -1
View File
@@ -67,7 +67,7 @@ export class ClawdbotClient {
private handleMessage(
msg: GatewayFrame | HelloOk,
connectResolve?: (v: HelloOk) => void,
connectReject?: (e: Error) => void
_connectReject?: (e: Error) => void
) {
if ('type' in msg) {
switch (msg.type) {
+19 -13
View File
@@ -1,21 +1,27 @@
import { createCollection } from '@tanstack/db'
import { createCollection, localOnlyCollectionOptions } from '@tanstack/db'
import type { MonitorSession, MonitorAction } from './protocol'
export const sessionsCollection = createCollection<MonitorSession>({
id: 'clawdbot-sessions',
primaryKey: 'key',
})
export const sessionsCollection = createCollection(
localOnlyCollectionOptions<MonitorSession>({
id: 'clawdbot-sessions',
getKey: (item) => item.key,
})
)
export const actionsCollection = createCollection<MonitorAction>({
id: 'clawdbot-actions',
primaryKey: 'id',
})
export const actionsCollection = createCollection(
localOnlyCollectionOptions<MonitorAction>({
id: 'clawdbot-actions',
getKey: (item) => item.id,
})
)
// Helper to update or insert session
export function upsertSession(session: MonitorSession) {
const existing = sessionsCollection.state.get(session.key)
if (existing) {
sessionsCollection.update(session.key, session)
sessionsCollection.update(session.key, (draft) => {
Object.assign(draft, session)
})
} else {
sessionsCollection.insert(session)
}
@@ -36,9 +42,9 @@ export function updateSessionStatus(
) {
const session = sessionsCollection.state.get(key)
if (session) {
sessionsCollection.update(key, {
status,
lastActivityAt: Date.now(),
sessionsCollection.update(key, (draft) => {
draft.status = status
draft.lastActivityAt = Date.now()
})
}
}
+2 -1
View File
@@ -12,7 +12,8 @@ export const dbDevtoolsPlugin = {
}
function DbInspector() {
const todos = useLiveQuery(todosCollection)
const todosQuery = useLiveQuery(todosCollection)
const todos = todosQuery.data ?? []
return (
<div>
<div className="text-xs text-gray-400 mb-1">todos ({todos.length})</div>
-2
View File
@@ -4,8 +4,6 @@ import superjson from 'superjson'
import { z } from 'zod'
import {
getClawdbotClient,
isChatEvent,
isAgentEvent,
parseEventFrame,
sessionInfoToMonitor,
type MonitorSession,
+7 -5
View File
@@ -1,4 +1,4 @@
import { createCollection } from '@tanstack/db'
import { createCollection, localOnlyCollectionOptions } from '@tanstack/db'
export interface Todo {
id: string
@@ -7,7 +7,9 @@ export interface Todo {
createdAt: number
}
export const todosCollection = createCollection<Todo>({
id: 'todos',
primaryKey: 'id',
})
export const todosCollection = createCollection(
localOnlyCollectionOptions<Todo>({
id: 'todos',
getKey: (item) => item.id,
})
)
-1
View File
@@ -12,7 +12,6 @@ async function handler({ request }: { request: Request }) {
}
export const Route = createFileRoute('/api/trpc/$')({
// @ts-expect-error server property not in route types yet
server: {
handlers: {
GET: handler,
+5 -2
View File
@@ -9,7 +9,8 @@ export const Route = createFileRoute('/demo/db')({
function DbDemo() {
const [newTodo, setNewTodo] = useState('')
const todos = useLiveQuery(todosCollection)
const todosQuery = useLiveQuery(todosCollection)
const todos = todosQuery.data ?? []
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
@@ -40,7 +41,9 @@ function DbDemo() {
})
tx.mutate(() => {
todosCollection.update(todo.id, { completed: !todo.completed })
todosCollection.update(todo.id, (draft) => {
draft.completed = !draft.completed
})
})
await tx.commit()
+1 -1
View File
@@ -7,7 +7,7 @@ export const Route = createFileRoute('/')({
function Home() {
return (
<div className="min-h-[calc(100vh-72px)] bg-gradient-to-br from-gray-900 to-gray-800 flex items-center justify-center">
<div className="min-h-[calc(100vh-72px)] bg-linear-to-br from-gray-900 to-gray-800 flex items-center justify-center">
<div className="text-center px-4">
<Activity size={64} className="mx-auto mb-6 text-cyan-400" />
<h1 className="text-5xl font-bold text-white mb-4">Crabwalk</h1>
+5 -5
View File
@@ -7,10 +7,7 @@ import {
sessionsCollection,
actionsCollection,
upsertSession,
addAction,
clearCollections,
type MonitorSession,
type MonitorAction,
} from '~/integrations/clawdbot'
import {
ActionGraph,
@@ -31,8 +28,11 @@ function MonitorPage() {
const [selectedSession, setSelectedSession] = useState<string | null>(null)
// Live queries from TanStack DB collections
const sessions = useLiveQuery(sessionsCollection)
const actions = useLiveQuery(actionsCollection)
const sessionsQuery = useLiveQuery(sessionsCollection)
const actionsQuery = useLiveQuery(actionsCollection)
const sessions = sessionsQuery.data ?? []
const actions = actionsQuery.data ?? []
// Check connection status on mount
useEffect(() => {
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.css?url' {
const url: string
export default url
}