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:
Somasundaram Mahesh
2026-02-18 18:49:13 +05:30
parent 88af77ddc7
commit 011e2be291
11 changed files with 389 additions and 9 deletions

View File

@@ -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