feat(minecraft): execute tools from dashboard

This commit is contained in:
Rin
2026-02-18 11:12:03 +08:00
committed by Neko Ayaka
parent af968b5bc4
commit 07097d1f78
9 changed files with 883 additions and 207 deletions
@@ -20,7 +20,6 @@ export class MineflayerPerceptionCollector {
}> = []
private lastSelfHealth: number | null = null
private lastStatsAt = 0
private stats: Record<string, number> = {}
private sneakingState: Map<string, boolean> = new Map()
@@ -36,214 +35,12 @@ export class MineflayerPerceptionCollector {
public init(bot: MineflayerWithAgents): void {
this.bot = bot
this.lastSelfHealth = bot.bot.health
this.lastStatsAt = Date.now()
this.stats = {}
this.deps.logger.withFields({ maxDistance: this.deps.maxDistance }).log('MineflayerPerceptionCollector: init')
this.onBot('entityMoved', (entity: any) => {
const now = Date.now()
const dist = this.distanceTo(entity)
if (dist === null || dist > this.deps.maxDistance)
return
// Ignore self
if (entity.username === this.bot?.bot.username)
return
const entityId = this.entityId(entity)
const event: SightedEntityMovedEvent = {
modality: 'sighted',
kind: 'entity_moved',
entityType: entity?.type === 'player' ? 'player' : 'mob',
entityId,
displayName: entity?.username,
distance: dist,
hasLineOfSight: true,
timestamp: now,
source: 'minecraft',
pos: entity?.position,
}
this.deps.emitRaw(event)
this.bumpStat('sighted.entity_moved')
this.maybeLogStats()
})
this.onBot('entitySwingArm', (entity: any) => {
const now = Date.now()
const dist = this.distanceTo(entity)
if (dist === null || dist > this.deps.maxDistance)
return
// Ignore self
if (entity.username === this.bot?.bot.username)
return
const event: SightedArmSwingEvent = {
modality: 'sighted',
kind: 'arm_swing',
entityType: 'player',
entityId: this.entityId(entity),
displayName: entity?.username,
distance: dist,
hasLineOfSight: true,
timestamp: now,
source: 'minecraft',
pos: entity?.position,
}
this.deps.emitRaw(event)
this.bumpStat('sighted.arm_swing')
this.maybeLogStats()
})
this.onBot('entityUpdate', (entity: any) => {
if (!entity || entity.type !== 'player')
return
// Ignore self
if (entity.username === this.bot?.bot.username)
return
const entityId = this.entityId(entity)
const flags = entity?.metadata?.[0]
// Bit 1 (0x02) is sneaking
const isSneaking = typeof flags === 'number' ? !!(flags & 0x02) : false
// Check if state actually changed
const lastState = this.sneakingState.get(entityId)
if (lastState === isSneaking) {
return
}
this.sneakingState.set(entityId, isSneaking)
const now = Date.now()
const dist = this.distanceTo(entity)
if (dist === null || dist > this.deps.maxDistance)
return
const event: SightedSneakToggleEvent = {
modality: 'sighted',
kind: 'sneak_toggle',
entityType: 'player',
entityId,
displayName: entity?.username,
distance: dist,
hasLineOfSight: true,
sneaking: isSneaking,
timestamp: now,
source: 'minecraft',
pos: entity?.position,
}
this.deps.logger.withFields({ entity: entity.username, sneaking: isSneaking }).log('MineflayerPerceptionCollector: sneak_toggle')
this.deps.emitRaw(event)
this.bumpStat('sighted.sneak_toggle')
this.maybeLogStats()
})
this.onBot('soundEffectHeard', (soundId: string, pos: Vec3) => {
const now = Date.now()
if (!pos)
return
const dist = this.distanceToPos(pos)
if (dist === null || dist > this.deps.maxDistance)
return
const event: HeardSoundEvent = {
modality: 'heard',
kind: 'sound',
soundId,
distance: dist,
timestamp: now,
source: 'minecraft',
pos,
}
this.deps.emitRaw(event)
})
// Felt: damage taken (self health decreased)
this.onBot('health', () => {
if (!this.bot)
return
const now = Date.now()
const current = this.bot.bot.health
const prev = this.lastSelfHealth
this.lastSelfHealth = current
if (typeof prev !== 'number')
return
if (current >= prev)
return
const event: FeltDamageTakenEvent = {
modality: 'felt',
kind: 'damage_taken',
amount: prev - current,
timestamp: now,
source: 'minecraft',
}
this.deps.emitRaw(event)
this.bumpStat('felt.damage_taken')
this.maybeLogStats()
})
// Felt: item collected (best-effort; depends on mineflayer version/events)
this.onBot('playerCollect', (collector: any, collected: any) => {
if (!this.bot)
return
if (!collector)
return
if (collector.username !== this.bot.bot.username)
return
const now = Date.now()
const itemName = String(collected?.name ?? collected?.displayName ?? collected?.type ?? 'unknown')
const event: FeltItemCollectedEvent = {
modality: 'felt',
kind: 'item_collected',
itemName,
timestamp: now,
source: 'minecraft',
}
this.deps.emitRaw(event)
this.bumpStat('felt.item_collected')
this.maybeLogStats()
})
this.onBot('entityCollect', (collector: any, collected: any) => {
if (!this.bot)
return
if (!collector)
return
if (collector.username !== this.bot.bot.username)
return
const now = Date.now()
const itemName = String(collected?.name ?? collected?.displayName ?? collected?.type ?? 'unknown')
const event: FeltItemCollectedEvent = {
modality: 'felt',
kind: 'item_collected',
itemName,
timestamp: now,
source: 'minecraft',
}
this.deps.emitRaw(event)
this.bumpStat('felt.item_collected')
this.maybeLogStats()
})
this.registerEventHandlers()
}
public destroy(): void {
@@ -268,6 +65,211 @@ export class MineflayerPerceptionCollector {
this.bot = null
}
// ========================================
// Event Handler Registration
// ========================================
private registerEventHandlers(): void {
this.onBot('entityMoved', entity => this.handleEntityMoved(entity))
this.onBot('entitySwingArm', entity => this.handleEntitySwingArm(entity))
this.onBot('entityUpdate', entity => this.handleEntityUpdate(entity))
this.onBot('soundEffectHeard', (soundId, pos) => this.handleSoundHeard(soundId, pos))
this.onBot('health', () => this.handleHealthChange())
this.onBot('playerCollect', (collector, collected) => this.handleItemCollected(collector, collected))
this.onBot('entityCollect', (collector, collected) => this.handleItemCollected(collector, collected))
}
// ========================================
// Sighted Event Handlers
// ========================================
private handleEntityMoved(entity: any): void {
if (!this.isValidEntityInRange(entity))
return
const event: SightedEntityMovedEvent = {
modality: 'sighted',
kind: 'entity_moved',
entityType: entity?.type === 'player' ? 'player' : 'mob',
entityId: this.entityId(entity),
displayName: entity?.username,
distance: this.distanceTo(entity)!,
hasLineOfSight: true,
timestamp: Date.now(),
source: 'minecraft',
pos: entity?.position,
}
this.emitEvent(event, 'sighted.entity_moved')
}
private handleEntitySwingArm(entity: any): void {
if (!this.isValidEntityInRange(entity))
return
const event: SightedArmSwingEvent = {
modality: 'sighted',
kind: 'arm_swing',
entityType: 'player',
entityId: this.entityId(entity),
displayName: entity?.username,
distance: this.distanceTo(entity)!,
hasLineOfSight: true,
timestamp: Date.now(),
source: 'minecraft',
pos: entity?.position,
}
this.emitEvent(event, 'sighted.arm_swing')
}
private handleEntityUpdate(entity: any): void {
if (!entity || entity.type !== 'player')
return
if (this.isSelfEntity(entity))
return
const entityId = this.entityId(entity)
const isSneaking = this.extractSneakingState(entity)
if (!this.hasSneakingStateChanged(entityId, isSneaking))
return
this.sneakingState.set(entityId, isSneaking)
const dist = this.distanceTo(entity)
if (dist === null || dist > this.deps.maxDistance)
return
const event: SightedSneakToggleEvent = {
modality: 'sighted',
kind: 'sneak_toggle',
entityType: 'player',
entityId,
displayName: entity?.username,
distance: dist,
hasLineOfSight: true,
sneaking: isSneaking,
timestamp: Date.now(),
source: 'minecraft',
pos: entity?.position,
}
this.emitEvent(event, 'sighted.sneak_toggle')
}
// ========================================
// Heard Event Handlers
// ========================================
private handleSoundHeard(soundId: string, pos: Vec3): void {
if (!pos)
return
const dist = this.distanceToPos(pos)
if (dist === null || dist > this.deps.maxDistance)
return
const event: HeardSoundEvent = {
modality: 'heard',
kind: 'sound',
soundId,
distance: dist,
timestamp: Date.now(),
source: 'minecraft',
pos,
}
this.deps.emitRaw(event)
}
// ========================================
// Felt Event Handlers
// ========================================
private handleHealthChange(): void {
if (!this.bot)
return
const current = this.bot.bot.health
const prev = this.lastSelfHealth
this.lastSelfHealth = current
if (typeof prev !== 'number' || current >= prev)
return
const event: FeltDamageTakenEvent = {
modality: 'felt',
kind: 'damage_taken',
amount: prev - current,
timestamp: Date.now(),
source: 'minecraft',
}
this.emitEvent(event, 'felt.damage_taken')
}
private handleItemCollected(collector: any, collected: any): void {
if (!this.bot || !collector)
return
if (collector.username !== this.bot.bot.username)
return
const itemName = String(collected?.name ?? collected?.displayName ?? collected?.type ?? 'unknown')
const event: FeltItemCollectedEvent = {
modality: 'felt',
kind: 'item_collected',
itemName,
timestamp: Date.now(),
source: 'minecraft',
}
this.emitEvent(event, 'felt.item_collected')
}
// ========================================
// Validation Helpers
// ========================================
private isValidEntityInRange(entity: any): boolean {
const dist = this.distanceTo(entity)
if (dist === null || dist > this.deps.maxDistance)
return false
if (this.isSelfEntity(entity))
return false
return true
}
private isSelfEntity(entity: any): boolean {
return entity.username === this.bot?.bot.username
}
private extractSneakingState(entity: any): boolean {
const flags = entity?.metadata?.[0]
// Bit 1 (0x02) is sneaking
return typeof flags === 'number' ? !!(flags & 0x02) : false
}
private hasSneakingStateChanged(entityId: string, isSneaking: boolean): boolean {
const lastState = this.sneakingState.get(entityId)
return lastState !== isSneaking
}
// ========================================
// Utilities
// ========================================
private emitEvent(event: RawPerceptionEvent, statKey: string): void {
this.deps.emitRaw(event)
this.bumpStat(statKey)
this.maybeLogStats()
}
private bumpStat(key: string): void {
this.stats[key] = (this.stats[key] ?? 0) + 1
}
@@ -8,7 +8,7 @@ trigger:
entityType: player
accumulator:
threshold: 5
threshold: 14
window: 2s
signal:
@@ -200,6 +200,18 @@ export class DebugService {
case 'reflex':
this.emitReflexState(payload as Omit<ReflexStateEvent, 'timestamp'>)
break
case 'debug:tools_list':
this.server.broadcast({
type: 'debug:tools_list',
payload: payload as { tools: any[] },
})
break
case 'debug:tool_result':
this.server.broadcast({
type: 'debug:tool_result',
payload: payload as any,
})
break
default:
// For unknown types, emit as log
this.log('DEBUG', `Unknown event type: ${type}`, { payload })
@@ -0,0 +1,189 @@
import type { ZodObject, ZodType } from 'zod'
import type { Mineflayer } from '../libs/mineflayer'
import type { ToolDefinition, ToolParameter } from './types'
import { actionsList } from '../agents/action/tools'
import { DebugService } from './debug-service'
export class ToolExecutor {
private mineflayer: Mineflayer
private debugService: DebugService
constructor(mineflayer: Mineflayer) {
this.mineflayer = mineflayer
this.debugService = DebugService.getInstance()
console.log('[ToolExecutor] Initializing ToolExecutor')
this.setupHandlers()
}
private setupHandlers(): void {
console.log('[ToolExecutor] Key registered for request_tools')
// Handle tool list request
this.debugService.onCommand('request_tools', () => {
console.log('[ToolExecutor] Received request_tools command')
this.sendToolsList()
})
// Handle tool execution
this.debugService.onCommand('execute_tool', (cmd) => {
if (cmd.type === 'execute_tool') {
console.log(`[ToolExecutor] Executing tool: ${cmd.payload.toolName}`)
this.executeTool(cmd.payload.toolName, cmd.payload.params)
}
})
}
private sendToolsList(): void {
try {
const tools = this.extractToolDefinitions()
console.log(`[ToolExecutor] Sending ${tools.length} tools`)
this.debugService.emit('debug:tools_list', { tools })
}
catch (err) {
console.error('[ToolExecutor] Error sending tool list:', err)
}
}
private async executeTool(toolName: string, params: Record<string, unknown>): Promise<void> {
try {
// Check if action is blocked
// TODO: Add check for running agent if needed
const action = actionsList.find(a => a.name === toolName)
if (!action) {
throw new Error(`Tool '${toolName}' not found`)
}
// Validate params
const validated = action.schema.parse(params)
// Execute
// The perform function in existing tools often returns a function that returns a Promise (or value)
// perform: (mineflayer) => async (args) => result
const performer = action.perform(this.mineflayer)
const args: any[] = []
const shape = (action.schema as any).shape
for (const key in shape) {
if (Object.prototype.hasOwnProperty.call(validated, key)) {
args.push((validated as any)[key])
}
}
const result = await performer(...args)
this.debugService.emit('debug:tool_result', {
toolName,
params,
result: typeof result === 'string' ? result : JSON.stringify(result),
timestamp: Date.now(),
})
}
catch (err: unknown) {
this.debugService.emit('debug:tool_result', {
toolName,
params,
error: err instanceof Error ? err.message : String(err),
timestamp: Date.now(),
})
}
}
private extractToolDefinitions(): ToolDefinition[] {
return actionsList.map(action => ({
name: action.name,
description: action.description,
params: this.extractParamsFromSchema(action.schema),
}))
}
private extractParamsFromSchema(schema: ZodObject<any>): ToolParameter[] {
if (!schema || !schema.shape)
return []
const shape = schema.shape
const params: ToolParameter[] = []
for (const [name, zodType] of Object.entries(shape)) {
const def = this.getZodDef(zodType as ZodType<any>)
params.push({
name,
type: def.typeName,
description: def.description,
min: def.min,
max: def.max,
default: def.defaultValue,
})
}
return params
}
// Helper to extract metadata from Zod types
private getZodDef(zodType: ZodType<any>): { typeName: 'string' | 'number' | 'boolean', description?: string, min?: number, max?: number, defaultValue?: any } {
let typeName: 'string' | 'number' | 'boolean' = 'string'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let curr: any = zodType
const description = curr.description
let min: number | undefined
let max: number | undefined
let defaultValue: any
// Helper to get type identifier
const getTypeId = (t: any) => t.constructor.name || t._def?.typeName
// Unwrap effects/optional/nullable/default to get inner type
let infiniteLoopGuard = 0
while (infiniteLoopGuard++ < 10) {
const typeId = getTypeId(curr)
if (typeId === 'ZodOptional' || typeId === 'ZodNullable') {
curr = curr._def.innerType
}
else if (typeId === 'ZodEffects') {
curr = curr._def.schema
}
else if (typeId === 'ZodDefault') {
defaultValue = curr._def.defaultValue()
curr = curr._def.innerType
}
else {
break
}
}
const typeId = getTypeId(curr)
// Debug logging for type checking
// console.log(`[ToolExecutor] Type check: ${description} -> ${typeId}`)
if (typeId === 'ZodString') {
typeName = 'string'
}
else if (typeId === 'ZodNumber') {
typeName = 'number'
// Try to extract min/max from checks
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (curr._def.checks) {
for (const check of (curr as any)._def.checks) {
if (check.kind === 'min')
min = check.value
if (check.kind === 'max')
max = check.value
}
}
}
else if (typeId === 'ZodBoolean') {
typeName = 'boolean'
}
return { typeName, description, min, max, defaultValue }
}
}
export function setupToolExecutor(mineflayer: Mineflayer): ToolExecutor {
return new ToolExecutor(mineflayer)
}
+55 -1
View File
@@ -101,6 +101,41 @@ export interface TraceBatchEvent {
}
// Union type for all server events
// ============================================================
// Tool types
// ============================================================
export interface ToolParameter {
name: string
type: 'string' | 'number' | 'boolean'
description?: string
required?: boolean
min?: number
max?: number
default?: unknown
}
export interface ToolDefinition {
name: string
description: string
params: ToolParameter[]
}
export interface ToolExecutionResultEvent {
toolName: string
params: Record<string, unknown>
result?: string
error?: string
timestamp: number
}
// ============================================================
// Server Events Extension
// ============================================================
// ... (previous events)
export type ServerEvent
= | { type: 'log', payload: LogEvent }
| { type: 'llm', payload: LLMTraceEvent }
@@ -112,6 +147,8 @@ export type ServerEvent
| { type: 'trace_batch', payload: TraceBatchEvent }
| { type: 'history', payload: ServerEvent[] }
| { type: 'pong', payload: { timestamp: number } }
| { type: 'debug:tools_list', payload: { tools: ToolDefinition[] } }
| { type: 'debug:tool_result', payload: ToolExecutionResultEvent }
// ============================================================
// Client -> Server commands
@@ -146,13 +183,30 @@ export interface RequestHistoryCommand {
type: 'request_history'
}
// Union type for all client commands
// ============================================================
// Client Commands Extension
// ============================================================
export interface ExecuteToolCommand {
type: 'execute_tool'
payload: {
toolName: string
params: Record<string, unknown>
}
}
export interface RequestToolsCommand {
type: 'request_tools'
}
export type ClientCommand
= | ClearLogsCommand
| SetFilterCommand
| InjectEventCommand
| PingCommand
| RequestHistoryCommand
| ExecuteToolCommand
| RequestToolsCommand
// ============================================================
// Wire format
+234 -1
View File
@@ -679,9 +679,240 @@ class SaliencyPanel {
}
// =============================================================================
// Timeline Panel (Event Tracing)
// Tools Panel
// =============================================================================
class ToolsPanel {
constructor(client) {
this.client = client
this.tools = []
this.filter = ''
this.executingTools = new Set()
this.elements = {
grid: document.getElementById('tools-grid'),
search: document.getElementById('tools-search'),
}
}
init() {
this.client.on('debug:tools_list', data => this.updateTools(data))
this.client.on('debug:tool_result', data => this.handleResult(data))
this.client.on('connected', () => this.requestTools())
this.elements.search?.addEventListener('input', (e) => {
this.filter = e.target.value.toLowerCase()
this.render()
})
}
requestTools() {
console.log('[ToolsPanel] Requesting tools...')
// Check if we already have tools to avoid re-rendering on reconnect if not needed
// But re-requesting ensures we are in sync with server capabilities
this.client.send({ type: 'request_tools' })
}
updateTools(data) {
if (data && data.tools) {
this.tools = data.tools
this.render()
}
}
render() {
if (!this.tools || this.tools.length === 0) {
this.elements.grid.innerHTML = '<div class="empty-state">Loading tools...</div>'
return
}
const filtered = this.tools.filter((tool) => {
if (!this.filter)
return true
return tool.name.toLowerCase().includes(this.filter)
|| (tool.description && tool.description.toLowerCase().includes(this.filter))
})
if (filtered.length === 0) {
this.elements.grid.innerHTML = '<div class="empty-state">No tools match filter</div>'
return
}
// Don't nuke usage of existing DOM elements if possible to preserve form state?
// For simplicity, re-render is fine for this debug tool.
this.elements.grid.innerHTML = filtered.map(tool => this.renderCard(tool)).join('')
// Attach event listeners
filtered.forEach((tool) => {
const card = document.getElementById(`tool-card-${tool.name}`)
const executeBtn = card?.querySelector('.btn-execute')
executeBtn?.addEventListener('click', () => this.executeTool(tool))
})
}
renderCard(tool) {
const isExecuting = this.executingTools.has(tool.name)
const cardState = isExecuting ? 'executing' : ''
const paramCount = tool.params.length
return `
<div id="tool-card-${tool.name}" class="tool-card ${cardState}">
<div class="tool-card-header">
<span class="tool-name">${escapeHtml(tool.name)}</span>
${paramCount > 0 ? `<span class="tool-badge">${paramCount} param${paramCount > 1 ? 's' : ''}</span>` : ''}
</div>
<div class="tool-description">${escapeHtml(tool.description || '')}</div>
${this.renderParams(tool)}
<div class="tool-actions">
<button class="btn-execute" ${isExecuting ? 'disabled' : ''}>
${isExecuting ? 'Executing...' : 'Execute'}
</button>
</div>
<div id="result-${tool.name}" class="tool-result hidden"></div>
</div>
`
}
renderParams(tool) {
if (tool.params.length === 0)
return ''
return `
<div class="tool-params">
${tool.params.map(param => `
<div class="param-group">
<label class="param-label">${escapeHtml(param.name)} (${param.type})</label>
<input
type="${param.type === 'number' ? 'number' : 'text'}"
class="param-input"
data-param="${param.name}"
${param.min !== undefined ? `min="${param.min}"` : ''}
${param.max !== undefined ? `max="${param.max}"` : ''}
${param.default !== undefined ? `value="${param.default}"` : ''}
placeholder="${escapeHtml(param.description || '')}"
/>
</div>
`).join('')}
</div>
`
}
executeTool(tool) {
const card = document.getElementById(`tool-card-${tool.name}`)
if (!card)
return
// Collect parameter values
const params = {}
const inputs = card.querySelectorAll('.param-input')
for (const input of inputs) {
const paramName = input.dataset.param
let value = input.value
// Convert to appropriate type based on definition
const paramDef = tool.params.find(p => p.name === paramName)
if (paramDef) {
if (paramDef.type === 'number') {
if (value === '') {
// Handle empty number input if needed?
}
else {
value = Number.parseFloat(value)
if (isNaN(value)) {
this.showResult(tool.name, { error: `Invalid number for ${paramName}` }, true)
return
}
}
}
}
// Simple type conversion could be improved but sufficient for now
params[paramName] = value
}
// Mark as executing
this.executingTools.add(tool.name)
this.updateCardState(tool.name, 'executing')
this.hideResult(tool.name)
// Send command to server
this.client.send({
type: 'execute_tool',
payload: {
toolName: tool.name,
params,
},
})
}
handleResult(data) {
const { toolName, result, error } = data
this.executingTools.delete(toolName)
this.updateCardState(toolName, error ? 'error' : 'success')
this.showResult(toolName, data, !!error)
if (!error) {
setTimeout(() => {
// Reset state visual but keep result visible for a bit?
// Or remove success styling
const card = document.getElementById(`tool-card-${toolName}`)
if (card) {
card.classList.remove('success')
// Don't auto-hide result immediately, maybe user wants to read it
}
}, 3000)
}
}
updateCardState(toolName, state) {
const card = document.getElementById(`tool-card-${toolName}`)
if (!card)
return
card.classList.remove('executing', 'success', 'error')
if (state)
card.classList.add(state)
const btn = card.querySelector('.btn-execute')
if (btn) {
if (state === 'executing') {
btn.disabled = true
btn.textContent = 'Executing...'
}
else {
btn.disabled = false
btn.textContent = 'Execute'
}
}
}
showResult(toolName, data, isError) {
const resultEl = document.getElementById(`result-${toolName}`)
if (!resultEl)
return
resultEl.classList.remove('hidden')
const label = isError ? 'Error' : 'Result'
const content = isError ? data.error : data.result
resultEl.innerHTML = `
<span class="result-label">${label}</span>
<div class="result-content">${escapeHtml(content || 'No result')}</div>
`
}
hideResult(toolName) {
const resultEl = document.getElementById(`result-${toolName}`)
if (resultEl) {
resultEl.classList.add('hidden')
}
}
}
class TimelinePanel {
constructor(client) {
this.client = client
@@ -991,6 +1222,7 @@ class DebugApp {
this.llmPanel = new LLMPanel(this.client)
this.saliencyPanel = new SaliencyPanel(this.client)
this.timelinePanel = new TimelinePanel(this.client)
this.toolsPanel = new ToolsPanel(this.client)
this.panels = {
queue: this.queuePanel,
@@ -1000,6 +1232,7 @@ class DebugApp {
llm: this.llmPanel,
saliency: this.saliencyPanel,
timeline: this.timelinePanel,
tools: this.toolsPanel,
}
this.paused = false
}
@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Debug Dashboard</title>
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="tools.css">
</head>
<body>
@@ -108,6 +109,20 @@
<pre id="blackboard-json" class="json-display">{}</pre>
</div>
</section>
<!-- Tools Panel -->
<section id="tools-section" class="panel">
<div class="panel-header">
<h2>Tools</h2>
<div class="panel-controls">
<input type="search" id="tools-search" placeholder="Filter tools..." />
<button class="icon-btn maximize-btn" title="Toggle maximize"></button>
</div>
</div>
<div class="panel-content">
<div id="tools-grid" class="tools-grid"></div>
</div>
</section>
</div>
<!-- Splitter V1 (Left vs Logs/Timeline/etc) -->
+167
View File
@@ -0,0 +1,167 @@
/* Tools Panel */
.tools-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
max-height: 100%;
overflow-y: auto;
padding-right: 4px;
}
.tool-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 6px;
padding: 12px;
transition: all 0.2s;
display: flex;
flex-direction: column;
}
.tool-card:hover {
border-color: var(--accent);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.tool-card.executing {
border-color: var(--accent);
background: rgba(102, 126, 234, 0.05);
}
.tool-card.success {
border-color: var(--success);
background: rgba(46, 160, 67, 0.05);
}
.tool-card.error {
border-color: var(--error);
background: rgba(248, 81, 73, 0.05);
}
.tool-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.tool-name {
font-weight: 600;
font-size: 14px;
color: var(--text-primary);
font-family: monospace;
}
.tool-badge {
font-size: 10px;
padding: 2px 6px;
background: var(--bg-primary);
border-radius: 3px;
color: var(--text-muted);
}
.tool-description {
font-size: 11px;
color: var(--text-secondary);
margin-bottom: 12px;
line-height: 1.4;
flex-grow: 1;
}
.tool-params {
margin-bottom: 10px;
}
.param-group {
margin-bottom: 8px;
}
.param-label {
display: block;
margin-bottom: 3px;
font-size: 11px;
font-weight: 500;
color: var(--text-secondary);
}
.param-input {
width: 100%;
padding: 5px 7px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text-primary);
font-size: 11px;
font-family: monospace;
}
.param-input:focus {
outline: none;
border-color: var(--accent);
}
.param-description {
margin-top: 2px;
font-size: 10px;
color: var(--text-muted);
font-style: italic;
}
.tool-actions {
display: flex;
gap: 6px;
margin-top: auto;
}
.btn-execute {
flex: 1;
padding: 7px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 4px;
color: white;
font-weight: 600;
font-size: 11px;
cursor: pointer;
transition: opacity 0.2s;
}
.btn-execute:hover:not(:disabled) {
opacity: 0.9;
}
.btn-execute:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.tool-result {
margin-top: 8px;
padding: 8px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 4px;
font-family: monospace;
font-size: 10px;
line-height: 1.4;
max-height: 100px;
overflow-y: auto;
}
.tool-result.hidden {
display: none;
}
.result-label {
font-weight: 600;
margin-bottom: 4px;
display: block;
text-transform: uppercase;
font-size: 9px;
letter-spacing: 0.5px;
}
.result-content {
white-space: pre-wrap;
word-break: break-word;
}
+4
View File
@@ -48,6 +48,10 @@ async function main() {
const agent = await createNeuriAgent(bot)
await bot.loadPlugin(CognitiveEngine({ agent, airiClient }))
// Setup Tool Executor for Debug Dashboard
const { setupToolExecutor } = await import('./debug/tool-executor')
setupToolExecutor(bot)
process.on('SIGINT', () => {
bot.stop()
exit(0)