Skip to content

Commit 15fedc0

Browse files
Merge pull request #36 from fanqiNO1/fix_runtime_bugs
[Fix] Fix runtime bugs
2 parents d923422 + 449144e commit 15fedc0

29 files changed

Lines changed: 1618 additions & 233 deletions

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ dependencies = [
3535
"pyreadline3>=3.5; sys_platform == 'win32'",
3636
"rich>=13.0",
3737
"prompt_toolkit>=3.0.40",
38+
"pyobjc-framework-Quartz>=12.2; sys_platform == 'darwin'",
39+
"pynput>=1.8.0"
3840
]
3941

4042
[project.optional-dependencies]

src/leapflow/cli/banner.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,20 @@
4747
def _categorize_tools(
4848
tool_defs: Sequence[Dict[str, Any]],
4949
) -> Dict[str, List[str]]:
50-
"""Group tool names by category for display."""
50+
"""Group tool names by category for display.
51+
52+
The static display map wins for known names; tools injected at runtime
53+
(semantic desktop schemas) fall back to their declared x_leapflow
54+
category instead of collapsing into "other".
55+
"""
5156
groups: Dict[str, List[str]] = {}
5257
for td in tool_defs:
5358
name = td.get("function", {}).get("name", "")
5459
if not name:
5560
continue
56-
cat = _TOOL_CATEGORIES.get(name, "other")
61+
cat = _TOOL_CATEGORIES.get(name)
62+
if not cat:
63+
cat = str((td.get("x_leapflow") or {}).get("category") or "") or "other"
5764
groups.setdefault(cat, []).append(name)
5865
return dict(sorted(groups.items()))
5966

src/leapflow/cli/commands/host.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ def _cua_driver_version() -> Optional[str]:
7979
[_CUA_DRIVER_CMD, "--version"],
8080
capture_output=True,
8181
text=True,
82+
encoding="utf-8",
83+
errors="replace",
8284
timeout=5.0,
8385
)
8486
if result.returncode == 0 and result.stdout.strip():
@@ -377,11 +379,13 @@ async def _cmd_doctor() -> int:
377379
client.start()
378380
_ok("MCP session established")
379381

380-
# Step 3: Ping test (list_apps as health probe)
382+
# Step 3: Ping test (get_screen_size as health probe — a real driver
383+
# round-trip that responds instantly; list_apps enumerates the whole
384+
# UI tree and can take 20s+ on Windows, making it a poor probe)
381385
print()
382386
print(f" {_BOLD}3. Ping test{_RESET}")
383-
_info("Sending probe (list_apps)...")
384-
result = client._session.call_tool_sync("list_apps", {}, timeout=5.0)
387+
_info("Sending probe (get_screen_size)...")
388+
result = client._session.call_tool_sync("get_screen_size", {}, timeout=10.0)
385389
if result.get("isError"):
386390
_warn("Probe returned error (non-fatal)")
387391
else:

src/leapflow/cli/commands/slash_handlers.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,18 @@
2323
def build_tool_payload(ctx: "Context") -> dict[str, Any]:
2424
"""Build a serializable tool summary for local or daemon rendering."""
2525
from leapflow.cli.banner import _categorize_tools
26-
from leapflow.tools.registry_bootstrap import TOOL_DEFINITIONS
26+
from leapflow.tools.registry_bootstrap import _capability_catalog
2727

28-
tool_groups = _categorize_tools(TOOL_DEFINITIONS)
28+
# Live catalog: static registry plus semantic desktop tools while
29+
# perception is online (falls back to the static list otherwise).
30+
tool_groups = _categorize_tools(_capability_catalog())
2931
groups = {category: sorted(names) for category, names in tool_groups.items()}
3032
mcp_count = 0
3133
if hasattr(ctx.rpc, "connected") and ctx.rpc.connected:
3234
mcp_count = len(getattr(ctx, "platform_tools", []))
3335
return {
3436
"ok": True,
37+
"view": "tools",
3538
"groups": groups,
3639
"total": sum(len(names) for names in groups.values()),
3740
"mcp_count": mcp_count,
@@ -1886,6 +1889,9 @@ def render_command_payload(console: "LeapConsole", payload: dict[str, Any]) -> N
18861889
if view == "status":
18871890
_render_status_view(console, payload)
18881891
return
1892+
if view == "tools":
1893+
render_tool_payload(console, payload)
1894+
return
18891895
if view == "model":
18901896
render_model_payload(console, payload)
18911897
return

src/leapflow/cli/context.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import asyncio
66
import logging
77
import os
8-
import re
98
import sys
109
import time
1110
from concurrent.futures import ThreadPoolExecutor
@@ -263,13 +262,6 @@ def _promote(frag: MemoryFragment) -> None:
263262
return _promote
264263

265264

266-
def sanitize_skill_name(title: str) -> str:
267-
"""Convert a skill title to a registry-safe name."""
268-
name = re.sub(r"[^\w\s-]", "", title.lower())
269-
name = re.sub(r"[\s]+", "-", name.strip())
270-
return name or "unnamed-skill"
271-
272-
273265
def _make_stored_skill_fn(stored: "StoredSkill", llm: Any):
274266
"""Create an LLM-backed execution function from a StoredSkill."""
275267
steps_text = "\n".join(f" {i+1}. {step}" for i, step in enumerate(stored.steps))
@@ -310,14 +302,17 @@ def _register_stored_skill_fallbacks(
310302
llm: Any,
311303
) -> int:
312304
"""Register StoredSkills that lack a parameterized or doc counterpart."""
305+
from leapflow.learning.document import title_to_kebab
313306
from leapflow.skills.registry import Skill, SkillMetadata
314307

315308
registered_names = set(registry.names()) if hasattr(registry, 'names') else {s.name for s in registry.list_all()}
316309
stored = skill_lib.load_all_active()
317310
count = 0
318311

319312
for s in stored:
320-
name = sanitize_skill_name(s.title)
313+
# Same naming function as the SKILL.md write paths, otherwise the
314+
# dedup below misses doc-backed skills and registers a duplicate.
315+
name = title_to_kebab(s.title)
321316
if name in registered_names:
322317
continue
323318
if not s.trigger_phrases:
@@ -1512,6 +1507,12 @@ async def _summarize_via_llm(prompt: str) -> str:
15121507
logger.debug("Shell approval gate: action orchestrator mode")
15131508
except Exception:
15141509
logger.debug("Shell approval gate setup skipped", exc_info=True)
1510+
try:
1511+
from leapflow.tools.registry_bootstrap import set_desktop_gate
1512+
set_desktop_gate(self._approval_orchestrator)
1513+
logger.debug("Desktop approval gate: action orchestrator mode")
1514+
except Exception:
1515+
logger.debug("Desktop approval gate setup skipped", exc_info=True)
15151516

15161517
self._critical_tool_bridge = tool_bridge
15171518

src/leapflow/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ class Settings:
484484
# ── Cua Driver ──
485485
use_cua_driver: bool = True
486486
cua_driver_cmd: str = "cua-driver"
487+
desktop_tools_enabled: bool = True
487488

488489
# ── Workflow Copilot ──
489490
copilot_enabled: bool = True
@@ -989,6 +990,7 @@ def _tuple_env(key: str, default: tuple) -> tuple:
989990
# Cua Driver
990991
use_cua_driver = _bool("LEAPFLOW_USE_CUA_DRIVER", "true")
991992
cua_driver_cmd = os.getenv("LEAPFLOW_CUA_DRIVER_CMD", "cua-driver").strip()
993+
desktop_tools_enabled = _bool("LEAPFLOW_DESKTOP_TOOLS_ENABLED", "true")
992994

993995
# Workflow Copilot
994996
copilot_enabled = _bool("LEAPFLOW_COPILOT_ENABLED", "true")
@@ -1312,6 +1314,7 @@ def _tuple_env(key: str, default: tuple) -> tuple:
13121314
# Cua Driver
13131315
use_cua_driver=use_cua_driver,
13141316
cua_driver_cmd=cua_driver_cmd,
1317+
desktop_tools_enabled=desktop_tools_enabled,
13151318
# Workflow Copilot
13161319
copilot_enabled=copilot_enabled,
13171320
copilot_min_idle_ms=copilot_min_idle_ms,

src/leapflow/copilot/context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def _warmup_pipeline(self, event: "SystemEvent", state: "ContextState") -> None:
181181
loop = asyncio.get_running_loop()
182182
ctx_snapshot = self._encoder.snapshot()
183183
loop.create_task(on_observed(
184-
action_id=f"{event.event_type}:{event.source}",
184+
# action_id=f"{event.event_type}:{event.source}",
185185
context=ctx_snapshot,
186186
))
187187
except RuntimeError:

src/leapflow/daemon/approval_coordinator.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ def install_gate(self, ctx: Any, service: Any) -> None:
3131
from leapflow.security.orchestrator import ApprovalOrchestrator
3232
from leapflow.tools.config_tools import set_config_approval_gate
3333
from leapflow.tools.gateway_tool import set_gateway_approval_gate
34-
from leapflow.tools.registry_bootstrap import set_file_read_gate, set_file_write_gate
34+
from leapflow.tools.registry_bootstrap import (
35+
set_desktop_gate,
36+
set_file_read_gate,
37+
set_file_write_gate,
38+
)
3539
from leapflow.tools.shell_tools import set_approval_gate
3640
from leapflow.tools.web_fetch import set_web_approval_gate
3741

@@ -51,6 +55,9 @@ def install_gate(self, ctx: Any, service: Any) -> None:
5155
set_config_approval_gate(orchestrator)
5256
# Same for outbound fetches that resolve to internal addresses.
5357
set_web_approval_gate(orchestrator)
58+
# Mutating semantic desktop tools (click, type_text, ...) share the
59+
# same approval path.
60+
set_desktop_gate(orchestrator)
5461

5562
class _FileReadGate:
5663
def __init__(self) -> None:

0 commit comments

Comments
 (0)