feat: agent overlay, stop-goal support, and state persistence across app kill
- Add draggable agent overlay pill (status dot + step text + stop button) that shows over other apps while connected. Fix ComposeView rendering in service context by providing a SavedStateRegistryOwner. - Add stop_goal protocol message so the overlay/client can abort a running agent session; server aborts via AbortController. - Persist screen-capture consent to SharedPreferences so it survives process death; restore on ConnectionService connect and Settings resume. - Query AccessibilityManager for real service state instead of relying on in-process MutableStateFlow that resets on restart. - Add overlay permission checklist item and SYSTEM_ALERT_WINDOW manifest entry. - Filter DroidClaw's own overlay nodes from the accessibility tree so the agent never interacts with them.
This commit is contained in:
@@ -22,7 +22,7 @@ async function hashApiKey(key: string): Promise<string> {
|
||||
}
|
||||
|
||||
/** Track running agent sessions to prevent duplicates per device */
|
||||
const activeSessions = new Map<string, string>();
|
||||
const activeSessions = new Map<string, { goal: string; abort: AbortController }>();
|
||||
|
||||
/**
|
||||
* Send a JSON message to a device WebSocket (safe — catches send errors).
|
||||
@@ -252,7 +252,8 @@ export async function handleDeviceMessage(
|
||||
}
|
||||
|
||||
console.log(`[Pipeline] Starting goal for device ${deviceId}: ${goal}`);
|
||||
activeSessions.set(deviceId, goal);
|
||||
const abortController = new AbortController();
|
||||
activeSessions.set(deviceId, { goal, abort: abortController });
|
||||
|
||||
sendToDevice(ws, { type: "goal_started", sessionId: deviceId, goal });
|
||||
|
||||
@@ -262,6 +263,7 @@ export async function handleDeviceMessage(
|
||||
userId,
|
||||
goal,
|
||||
llmConfig: userLlmConfig,
|
||||
signal: abortController.signal,
|
||||
onStep(step) {
|
||||
sendToDevice(ws, {
|
||||
type: "step",
|
||||
@@ -294,6 +296,23 @@ export async function handleDeviceMessage(
|
||||
break;
|
||||
}
|
||||
|
||||
case "stop_goal": {
|
||||
const deviceId = ws.data.deviceId!;
|
||||
const active = activeSessions.get(deviceId);
|
||||
if (active) {
|
||||
console.log(`[Pipeline] Stop requested for device ${deviceId}`);
|
||||
active.abort.abort();
|
||||
activeSessions.delete(deviceId);
|
||||
sendToDevice(ws, {
|
||||
type: "goal_completed",
|
||||
sessionId: deviceId,
|
||||
success: false,
|
||||
stepsUsed: 0,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "apps": {
|
||||
const persistentDeviceId = ws.data.persistentDeviceId;
|
||||
if (persistentDeviceId) {
|
||||
@@ -360,7 +379,11 @@ export function handleDeviceClose(
|
||||
const { deviceId, userId, persistentDeviceId } = ws.data;
|
||||
if (!deviceId) return;
|
||||
|
||||
activeSessions.delete(deviceId);
|
||||
const active = activeSessions.get(deviceId);
|
||||
if (active) {
|
||||
active.abort.abort();
|
||||
activeSessions.delete(deviceId);
|
||||
}
|
||||
sessions.removeDevice(deviceId);
|
||||
|
||||
// Update device status in DB
|
||||
|
||||
Reference in New Issue
Block a user