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:
parent
5b7ad30573
commit
c6a41018b6
3 changed files with 41 additions and 7 deletions
|
|
@ -27,6 +27,7 @@
|
||||||
import { getProvider, getDefaultProviderId } from '../../providers/registry.svelte';
|
import { getProvider, getDefaultProviderId } from '../../providers/registry.svelte';
|
||||||
import { loadAnchorsForProject } from '../../stores/anchors.svelte';
|
import { loadAnchorsForProject } from '../../stores/anchors.svelte';
|
||||||
import { getSecret } from '../../adapters/secrets-bridge';
|
import { getSecret } from '../../adapters/secrets-bridge';
|
||||||
|
import { getUnreadCount } from '../../adapters/btmsg-bridge';
|
||||||
import { getWakeEvent, consumeWakeEvent, updateManagerSession } from '../../stores/wake-scheduler.svelte';
|
import { getWakeEvent, consumeWakeEvent, updateManagerSession } from '../../stores/wake-scheduler.svelte';
|
||||||
import { SessionId, ProjectId } from '../../types/ids';
|
import { SessionId, ProjectId } from '../../types/ids';
|
||||||
import AgentPane from '../Agent/AgentPane.svelte';
|
import AgentPane from '../Agent/AgentPane.svelte';
|
||||||
|
|
@ -136,11 +137,43 @@
|
||||||
stopAgent(sessionId).catch(() => {});
|
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
|
// Start timer and clean up
|
||||||
startReinjectionTimer();
|
startReinjectionTimer();
|
||||||
|
startMsgPoll();
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (reinjectionTimer) clearInterval(reinjectionTimer);
|
if (reinjectionTimer) clearInterval(reinjectionTimer);
|
||||||
if (wakeCheckTimer) clearInterval(wakeCheckTimer);
|
if (wakeCheckTimer) clearInterval(wakeCheckTimer);
|
||||||
|
if (msgPollTimer) clearInterval(msgPollTimer);
|
||||||
unsubAgentStart();
|
unsubAgentStart();
|
||||||
unsubAgentStop();
|
unsubAgentStop();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,6 @@
|
||||||
await setAgentStatus(agent.id, newStatus);
|
await setAgentStatus(agent.id, newStatus);
|
||||||
await pollBtmsg(); // Refresh immediately
|
await pollBtmsg(); // Refresh immediately
|
||||||
if (newStatus === 'active') {
|
if (newStatus === 'active') {
|
||||||
setActiveProject(agent.id);
|
|
||||||
emitAgentStart(agent.id);
|
emitAgentStart(agent.id);
|
||||||
} else {
|
} else {
|
||||||
emitAgentStop(agent.id);
|
emitAgentStop(agent.id);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy, untrack } from 'svelte';
|
||||||
import { getAllWorkItems, getActiveProjectId, setActiveProject } from '../../stores/workspace.svelte';
|
import { getAllWorkItems, getActiveProjectId, setActiveProject } from '../../stores/workspace.svelte';
|
||||||
import ProjectBox from './ProjectBox.svelte';
|
import ProjectBox from './ProjectBox.svelte';
|
||||||
|
|
||||||
|
|
@ -15,15 +15,17 @@
|
||||||
// Track slot elements for auto-scroll
|
// Track slot elements for auto-scroll
|
||||||
let slotEls = $state<Record<string, HTMLElement>>({});
|
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(() => {
|
$effect(() => {
|
||||||
const id = activeProjectId;
|
const id = activeProjectId;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
untrack(() => {
|
||||||
const el = slotEls[id];
|
const el = slotEls[id];
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
// Use smooth scroll; block: nearest avoids jumping if already visible
|
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
|
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
let observer: ResizeObserver | undefined;
|
let observer: ResizeObserver | undefined;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue