Auto-wake agents on btmsg and fix unwanted auto-scroll

- Add btmsg inbox polling (10s) to AgentSession so agents wake when
  they receive messages from other agents (not just admin DMs)
- Remove automatic setActiveProject on agent activation to prevent
  focus stealing from the user
- Use untrack() in ProjectGrid scroll effect so agent re-renders
  don't trigger unwanted scrollIntoView

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
DexterFromLab 2026-03-12 14:14:13 +01:00
parent 5b7ad30573
commit c6a41018b6
3 changed files with 41 additions and 7 deletions

View file

@ -27,6 +27,7 @@
import { getProvider, getDefaultProviderId } from '../../providers/registry.svelte';
import { loadAnchorsForProject } from '../../stores/anchors.svelte';
import { getSecret } from '../../adapters/secrets-bridge';
import { getUnreadCount } from '../../adapters/btmsg-bridge';
import { getWakeEvent, consumeWakeEvent, updateManagerSession } from '../../stores/wake-scheduler.svelte';
import { SessionId, ProjectId } from '../../types/ids';
import AgentPane from '../Agent/AgentPane.svelte';
@ -136,11 +137,43 @@
stopAgent(sessionId).catch(() => {});
});
// btmsg inbox polling — auto-wake agent when it receives messages from other agents
let msgPollTimer: ReturnType<typeof setInterval> | null = null;
let lastKnownUnread = 0;
function startMsgPoll() {
if (msgPollTimer) clearInterval(msgPollTimer);
msgPollTimer = setInterval(async () => {
if (contextRefreshPrompt) return; // Don't queue if already has a pending prompt
try {
const count = await getUnreadCount(project.id as unknown as AgentId);
if (count > 0 && count > lastKnownUnread) {
lastKnownUnread = count;
const wakeMsg = project.isAgent
? `[New Message] You have ${count} unread message(s). Check your inbox with \`btmsg inbox\` and respond appropriately.`
: `[New Message] You have ${count} unread message(s). Check your inbox and respond.`;
contextRefreshPrompt = wakeMsg;
logAuditEvent(
project.id as unknown as AgentId,
'wake_event',
`Agent woken by ${count} unread btmsg message(s)`,
).catch(() => {});
} else if (count === 0) {
lastKnownUnread = 0;
}
} catch {
// btmsg not available, ignore
}
}, 10_000); // Check every 10s
}
// Start timer and clean up
startReinjectionTimer();
startMsgPoll();
onDestroy(() => {
if (reinjectionTimer) clearInterval(reinjectionTimer);
if (wakeCheckTimer) clearInterval(wakeCheckTimer);
if (msgPollTimer) clearInterval(msgPollTimer);
unsubAgentStart();
unsubAgentStop();
});

View file

@ -64,7 +64,6 @@
await setAgentStatus(agent.id, newStatus);
await pollBtmsg(); // Refresh immediately
if (newStatus === 'active') {
setActiveProject(agent.id);
emitAgentStart(agent.id);
} else {
emitAgentStop(agent.id);

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { onMount, onDestroy, untrack } from 'svelte';
import { getAllWorkItems, getActiveProjectId, setActiveProject } from '../../stores/workspace.svelte';
import ProjectBox from './ProjectBox.svelte';
@ -15,14 +15,16 @@
// Track slot elements for auto-scroll
let slotEls = $state<Record<string, HTMLElement>>({});
// Auto-scroll to active project when it changes
// Auto-scroll to active project only when activeProjectId changes
// (untrack slotEls so agent re-renders don't trigger unwanted scrolls)
$effect(() => {
const id = activeProjectId;
if (!id) return;
const el = slotEls[id];
if (!el) return;
// Use smooth scroll; block: nearest avoids jumping if already visible
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
untrack(() => {
const el = slotEls[id];
if (!el) return;
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
});
});
let observer: ResizeObserver | undefined;