Wire play/stop buttons and DM send to agent lifecycle

- Play button in GroupAgentsPanel now starts agent session via emitAgentStart
- Stop button now stops running agent session via emitAgentStop
- Sending a DM to a stopped agent auto-wakes it (sets active + emitAgentStart)
- Fix autoPrompt in AgentPane to work for fresh sessions (not just done/error)
- Fix btmsg: admin (tier 0) bypasses stale heartbeat check so messages deliver

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
DexterFromLab 2026-03-12 12:33:08 +01:00
parent 42907f22a4
commit 2c710fa0db
6 changed files with 114 additions and 34 deletions

View file

@ -151,14 +151,15 @@
// NOTE: Do NOT stop agents in onDestroy — it fires on layout changes/remounts,
// not just explicit close. Stop-on-close is handled by workspace teardown.
// Auto-prompt: pick up externally triggered prompts (e.g. periodic context refresh)
// Auto-prompt: pick up externally triggered prompts (e.g. periodic context refresh, play button)
$effect(() => {
if (!autoPrompt || isRunning) return;
// Only trigger if session exists and is idle (done/error)
if (!session || (session.status !== 'done' && session.status !== 'error')) return;
// Allow: no session yet (first start) or session completed (done/error)
if (session && session.status !== 'done' && session.status !== 'error') return;
const prompt = autoPrompt;
const resume = !!session; // new query if first start, resume if existing session
onautopromptconsumed?.();
startQuery(prompt, true); // resume session with context refresh
startQuery(prompt, resume);
});
let promptRef = $state<HTMLTextAreaElement | undefined>();

View file

@ -12,6 +12,8 @@
type AgentMessageRecord,
} from '../../adapters/groups-bridge';
import { registerSessionProject } from '../../agent-dispatcher';
import { onAgentStart, onAgentStop } from '../../stores/workspace.svelte';
import { stopAgent } from '../../adapters/agent-bridge';
import { trackProject, updateProjectSession } from '../../stores/health.svelte';
import {
createAgentSession,
@ -97,11 +99,30 @@
lastPromptTime = Date.now();
}
// Listen for play-button start events from GroupAgentsPanel
const unsubAgentStart = onAgentStart((projectId) => {
if (projectId !== project.id) return;
// Only auto-start if not already running and no pending prompt
if (contextRefreshPrompt) return;
const startPrompt = project.isAgent
? 'Start your work. Check your inbox with `btmsg inbox` and review the task board with `bttask board`. Take action on any pending items.'
: 'Review the instructions above and begin your work.';
contextRefreshPrompt = startPrompt;
});
// Listen for stop-button events from GroupAgentsPanel
const unsubAgentStop = onAgentStop((projectId) => {
if (projectId !== project.id) return;
stopAgent(sessionId).catch(() => {});
});
// Start timer and clean up
startReinjectionTimer();
onDestroy(() => {
if (reinjectionTimer) clearInterval(reinjectionTimer);
if (wakeCheckTimer) clearInterval(wakeCheckTimer);
unsubAgentStart();
unsubAgentStop();
});
// Wake scheduler integration — poll for wake events (Manager agents only)

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { getActiveGroup } from '../../stores/workspace.svelte';
import { getActiveGroup, emitAgentStart } from '../../stores/workspace.svelte';
import {
type BtmsgAgent,
type BtmsgMessage,
@ -17,6 +17,7 @@
getChannelMessages,
sendChannelMessage,
createChannel,
setAgentStatus,
} from '../../adapters/btmsg-bridge';
const ADMIN_ID = 'admin';
@ -127,6 +128,13 @@
try {
if (currentView.type === 'dm') {
await sendMessage(ADMIN_ID, currentView.agentId, text);
// Auto-wake agent if stopped
const recipient = agents.find(a => a.id === currentView.agentId);
if (recipient && recipient.status !== 'active') {
await setAgentStatus(currentView.agentId, 'active');
emitAgentStart(currentView.agentId);
await pollBtmsg();
}
} else if (currentView.type === 'channel') {
await sendChannelMessage(currentView.channelId, ADMIN_ID, text);
} else {
@ -140,6 +148,11 @@
}
}
/** Refresh agent list (reused by poll and wake logic) */
async function pollBtmsg() {
await loadData();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { getActiveGroup, getEnabledProjects, setActiveProject } from '../../stores/workspace.svelte';
import { getActiveGroup, getEnabledProjects, setActiveProject, emitAgentStart, emitAgentStop } from '../../stores/workspace.svelte';
import type { GroupAgentConfig, GroupAgentStatus, ProjectConfig } from '../../types/groups';
import { getGroupAgents, setAgentStatus, type BtmsgAgent } from '../../adapters/btmsg-bridge';
import type { AgentId } from '../../types/ids';
@ -63,6 +63,12 @@
try {
await setAgentStatus(agent.id, newStatus);
await pollBtmsg(); // Refresh immediately
if (newStatus === 'active') {
setActiveProject(agent.id);
emitAgentStart(agent.id);
} else {
emitAgentStop(agent.id);
}
} catch (e) {
console.warn('Failed to set agent status:', e);
}

View file

@ -82,6 +82,42 @@ export function emitTerminalToggle(projectId: string): void {
}
}
// --- Agent start event (play button in GroupAgentsPanel) ---
type AgentStartCallback = (projectId: string) => void;
let agentStartCallbacks: AgentStartCallback[] = [];
export function onAgentStart(cb: AgentStartCallback): () => void {
agentStartCallbacks.push(cb);
return () => {
agentStartCallbacks = agentStartCallbacks.filter(c => c !== cb);
};
}
export function emitAgentStart(projectId: string): void {
for (const cb of agentStartCallbacks) {
cb(projectId);
}
}
// --- Agent stop event (stop button in GroupAgentsPanel) ---
type AgentStopCallback = (projectId: string) => void;
let agentStopCallbacks: AgentStopCallback[] = [];
export function onAgentStop(cb: AgentStopCallback): () => void {
agentStopCallbacks.push(cb);
return () => {
agentStopCallbacks = agentStopCallbacks.filter(c => c !== cb);
};
}
export function emitAgentStop(projectId: string): void {
for (const cb of agentStopCallbacks) {
cb(projectId);
}
}
// --- Getters ---
export function getGroupsConfig(): GroupsFile | null {