Merge remote-tracking branch 'origin/main' into screens
# Conflicts: # .claude/settings.local.json # CLAUDE.md # bolt_pet/controller.py
This commit is contained in:
+77
-31
@@ -20,9 +20,9 @@ from typing import Optional
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
from . import (
|
||||
config, history as history_mod, monitors as monitors_mod, notifications,
|
||||
pet_actions, quiet, screen_context, screen_text, server_client,
|
||||
speech_text, updater,
|
||||
config, file_delivery, file_ops, history as history_mod,
|
||||
monitors as monitors_mod, notifications, pet_actions, quiet,
|
||||
screen_context, screen_text, server_client, speech_text, updater,
|
||||
)
|
||||
from .audio import barge_in, mic, stt, tts, wake_word
|
||||
from .state import PetState, PetStateMachine
|
||||
@@ -259,6 +259,7 @@ class PetController(QObject):
|
||||
self._state.transition(PetState.IDLE)
|
||||
return
|
||||
|
||||
self._check_deliveries()
|
||||
self._speak(reply)
|
||||
self._state.transition(PetState.IDLE)
|
||||
|
||||
@@ -289,42 +290,57 @@ class PetController(QObject):
|
||||
|
||||
def _handle_command(self, command: str) -> str:
|
||||
"""Server-relayed command. `petctl ...` drives the pet's body and
|
||||
never reaches a shell; everything else is a real command, exactly as
|
||||
before (see the security notes in the README)."""
|
||||
`filectl ...` does local file read/write/edit — neither ever reaches
|
||||
a shell; everything else is a real command, exactly as before (see
|
||||
the security notes in the README)."""
|
||||
try:
|
||||
action = pet_actions.parse(command)
|
||||
except pet_actions.ActionError as exc:
|
||||
self.log.emit(f"petctl: {exc}")
|
||||
return f"[pet] {exc}"
|
||||
if action is None:
|
||||
return server_client.run_local_command(command)
|
||||
self.log.emit(f"Pet action: {action}")
|
||||
if action is not None:
|
||||
self.log.emit(f"Pet action: {action}")
|
||||
|
||||
# Queries answer from here rather than from pet_actions.describe():
|
||||
# their output *is* the useful part, and it's what the server reads
|
||||
# back off the tool-result relay.
|
||||
kind = action["action"]
|
||||
if kind == "monitors":
|
||||
return monitors_mod.describe(self._monitors, self._pet_monitor)
|
||||
if kind == "read":
|
||||
return self._read_screen(action["target"])
|
||||
if kind == "jump":
|
||||
# Queries answer from here rather than from pet_actions.describe():
|
||||
# their output *is* the useful part, and it's what the server reads
|
||||
# back off the tool-result relay.
|
||||
kind = action["action"]
|
||||
if kind == "monitors":
|
||||
return monitors_mod.describe(self._monitors, self._pet_monitor)
|
||||
if kind == "read":
|
||||
return self._read_screen(action["target"])
|
||||
if kind == "jump":
|
||||
try:
|
||||
target = monitors_mod.resolve(
|
||||
self._monitors, action["target"], self._pet_monitor
|
||||
)
|
||||
except ValueError as exc:
|
||||
self.log.emit(f"petctl jump: {exc}")
|
||||
return f"[pet] {exc}"
|
||||
# Hand the window a resolved index, so it can't re-resolve the
|
||||
# spec against a different screen ordering.
|
||||
self.action.emit({"action": "jump", "monitor": target.index})
|
||||
return f"[pet] jumped to monitor {target.label}"
|
||||
|
||||
if kind == "nap":
|
||||
self.set_napping(bool(action["enabled"]))
|
||||
self.action.emit(action)
|
||||
return pet_actions.describe(action)
|
||||
|
||||
try:
|
||||
file_action = file_ops.parse(command)
|
||||
except file_ops.FileOpError as exc:
|
||||
self.log.emit(f"filectl: {exc}")
|
||||
return f"[filectl] {exc}"
|
||||
if file_action is not None:
|
||||
self.log.emit(file_ops.describe(file_action))
|
||||
try:
|
||||
target = monitors_mod.resolve(
|
||||
self._monitors, action["target"], self._pet_monitor
|
||||
)
|
||||
except ValueError as exc:
|
||||
self.log.emit(f"petctl jump: {exc}")
|
||||
return f"[pet] {exc}"
|
||||
# Hand the window a resolved index, so it can't re-resolve the
|
||||
# spec against a different screen ordering.
|
||||
self.action.emit({"action": "jump", "monitor": target.index})
|
||||
return f"[pet] jumped to monitor {target.label}"
|
||||
return file_ops.execute(file_action)
|
||||
except file_ops.FileOpError as exc:
|
||||
self.log.emit(f"filectl: {exc}")
|
||||
return f"[filectl] {exc}"
|
||||
|
||||
if kind == "nap":
|
||||
self.set_napping(bool(action["enabled"]))
|
||||
self.action.emit(action)
|
||||
return pet_actions.describe(action)
|
||||
return server_client.run_local_command(command)
|
||||
|
||||
def _read_screen(self, target: str) -> str:
|
||||
"""`petctl read` — OCR a screen and hand the text back to the server."""
|
||||
@@ -487,10 +503,39 @@ class PetController(QObject):
|
||||
except server_client.ServerError as exc:
|
||||
self.log.emit(f"Couldn't forward notification: {exc}")
|
||||
return
|
||||
self._check_deliveries()
|
||||
if reply.strip():
|
||||
self._speak(reply)
|
||||
self._state.transition(PetState.IDLE)
|
||||
|
||||
# ── file delivery ────────────────────────────────────────────────────
|
||||
|
||||
def _check_deliveries(self) -> None:
|
||||
"""Download anything the server has queued via deliver_files —
|
||||
called right after a conversation/notification turn (the common
|
||||
case: "send me that file") and once per heartbeat for anything
|
||||
queued out-of-band. Best-effort: a failure here is logged, not
|
||||
raised, so it can't sour a turn that already got its spoken reply."""
|
||||
if not config.RECEIVE_FILES:
|
||||
return
|
||||
try:
|
||||
queued = server_client.list_outbox_files()
|
||||
except server_client.ServerError as exc:
|
||||
self.log.emit(f"Couldn't check for delivered files: {exc}")
|
||||
return
|
||||
for entry in queued:
|
||||
file_id = entry.get("id")
|
||||
name = entry.get("name") or file_id
|
||||
if not file_id:
|
||||
continue
|
||||
try:
|
||||
data = server_client.download_outbox_file(file_id)
|
||||
except server_client.ServerError as exc:
|
||||
self.log.emit(f"Couldn't download {name}: {exc}")
|
||||
continue
|
||||
path = file_delivery.save(config.DELIVERED_FILES_DIR, name, data)
|
||||
self.log.emit(f"Received file: {path}")
|
||||
|
||||
# ── auto-update ──────────────────────────────────────────────────────
|
||||
|
||||
def _maybe_update(self) -> None:
|
||||
@@ -546,6 +591,7 @@ class PetController(QObject):
|
||||
return
|
||||
if self._napping:
|
||||
return # quiet hours: still answers when spoken to, just doesn't start
|
||||
self._check_deliveries()
|
||||
self._drain_notifications()
|
||||
if self._state.state != PetState.IDLE:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user