feat: implement Monitor tool for streaming shell output (#649)
* feat: implement Monitor tool for streaming shell output Add the Monitor tool that executes shell commands in the background and streams stdout line-by-line as notifications to the model. This enables real-time monitoring of logs, builds, and long-running processes. Implementation: - MonitorTool (src/tools/MonitorTool/) — spawns LocalShellTask with kind='monitor', returns immediately with task ID - MonitorMcpTask (src/tasks/MonitorMcpTask/) — task lifecycle management and agent cleanup via killMonitorMcpTasksForAgent() - MonitorPermissionRequest — permission dialog component The codebase already had all integration points wired (tools.ts, tasks.ts, PermissionRequest.tsx, LocalShellTask kind='monitor', BashTool prompt). This PR provides the missing implementations. * fix: command-specific permission rule + architecture docs - MonitorPermissionRequest: "don't ask again" now creates a command-prefix rule (like BashTool) instead of a blanket tool-name-only rule that would auto-allow all Monitor commands - MonitorMcpTask: clarify architecture comments explaining why monitor_mcp type exists as a registry stub while actual tasks are local_bash with kind='monitor' * fix: address Copilot review feedback - Fix permission rule field: expression → ruleContent (Copilot #1) - Handle empty command prefix: skip rule creation (Copilot #2) - Remove unused useTheme() import (Copilot #3) - Save permission rules under 'Bash' toolName so bashToolHasPermission can match them — Monitor delegates to Bash permission system (Copilot #4) - Remove unused logError import from MonitorMcpTask (Copilot #6) - Copilot #5 (getAppState throws): same pattern as BashTool:915, not a bug
This commit is contained in:
committed by
GitHub
parent
24d485f42f
commit
b818dd5958
102
src/tasks/MonitorMcpTask/MonitorMcpTask.ts
Normal file
102
src/tasks/MonitorMcpTask/MonitorMcpTask.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
// MonitorMcpTask — task registry entry for the 'monitor_mcp' type.
|
||||
//
|
||||
// Architecture: MonitorTool spawns shell processes as LocalShellTask
|
||||
// (type: 'local_bash', kind: 'monitor'). The 'monitor_mcp' type exists
|
||||
// in TaskType for forward-compatibility with MCP-based monitoring (not
|
||||
// yet implemented). This module satisfies the import from tasks.ts and
|
||||
// provides killMonitorMcpTasksForAgent for agent-scoped cleanup of
|
||||
// monitor-kind shell tasks.
|
||||
|
||||
import type { AppState } from '../../state/AppState.js'
|
||||
import type { SetAppState, Task, TaskStateBase } from '../../Task.js'
|
||||
import type { AgentId } from '../../types/ids.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import { dequeueAllMatching } from '../../utils/messageQueueManager.js'
|
||||
import { evictTaskOutput } from '../../utils/task/diskOutput.js'
|
||||
import { updateTaskState } from '../../utils/task/framework.js'
|
||||
import { isLocalShellTask } from '../LocalShellTask/guards.js'
|
||||
import { killTask } from '../LocalShellTask/killShellTasks.js'
|
||||
|
||||
export type MonitorMcpTaskState = TaskStateBase & {
|
||||
type: 'monitor_mcp'
|
||||
agentId?: AgentId
|
||||
}
|
||||
|
||||
function isMonitorMcpTask(task: unknown): task is MonitorMcpTaskState {
|
||||
return (
|
||||
typeof task === 'object' &&
|
||||
task !== null &&
|
||||
'type' in task &&
|
||||
task.type === 'monitor_mcp'
|
||||
)
|
||||
}
|
||||
|
||||
export const MonitorMcpTask: Task = {
|
||||
name: 'MonitorMcpTask',
|
||||
type: 'monitor_mcp',
|
||||
async kill(taskId, setAppState) {
|
||||
updateTaskState<MonitorMcpTaskState>(taskId, setAppState, task => {
|
||||
if (task.status !== 'running') {
|
||||
return task
|
||||
}
|
||||
|
||||
return {
|
||||
...task,
|
||||
status: 'killed',
|
||||
notified: true,
|
||||
endTime: Date.now(),
|
||||
}
|
||||
})
|
||||
void evictTaskOutput(taskId)
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill all monitor tasks owned by a given agent.
|
||||
*
|
||||
* MonitorTool spawns tasks as local_bash with kind='monitor'. When an agent
|
||||
* exits, killShellTasksForAgent already handles those. This function provides
|
||||
* additional cleanup for any monitor_mcp-typed tasks and also kills any
|
||||
* local_bash tasks with kind='monitor' that might have been missed (belt and
|
||||
* suspenders). Finally, it purges queued notifications for the dead agent.
|
||||
*/
|
||||
export function killMonitorMcpTasksForAgent(
|
||||
agentId: AgentId,
|
||||
getAppState: () => AppState,
|
||||
setAppState: SetAppState,
|
||||
): void {
|
||||
const tasks = getAppState().tasks ?? {}
|
||||
|
||||
for (const [taskId, task] of Object.entries(tasks)) {
|
||||
// Kill monitor_mcp tasks for this agent
|
||||
if (
|
||||
isMonitorMcpTask(task) &&
|
||||
task.agentId === agentId &&
|
||||
task.status === 'running'
|
||||
) {
|
||||
logForDebugging(
|
||||
`killMonitorMcpTasksForAgent: killing monitor_mcp task ${taskId} (agent ${agentId} exiting)`,
|
||||
)
|
||||
void MonitorMcpTask.kill(taskId, setAppState)
|
||||
}
|
||||
|
||||
// Also kill local_bash tasks with kind='monitor' for this agent
|
||||
// (killShellTasksForAgent already does this, but being explicit
|
||||
// guards against ordering issues)
|
||||
if (
|
||||
isLocalShellTask(task) &&
|
||||
task.kind === 'monitor' &&
|
||||
task.agentId === agentId &&
|
||||
task.status === 'running'
|
||||
) {
|
||||
logForDebugging(
|
||||
`killMonitorMcpTasksForAgent: killing monitor shell task ${taskId} (agent ${agentId} exiting)`,
|
||||
)
|
||||
killTask(taskId, setAppState)
|
||||
}
|
||||
}
|
||||
|
||||
// Purge any queued notifications addressed to this agent — its query loop
|
||||
// has exited and won't drain them.
|
||||
dequeueAllMatching(cmd => cmd.agentId === agentId)
|
||||
}
|
||||
Reference in New Issue
Block a user