Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0527a38
refactor memory and storage
wangxingjun778 Jul 8, 2026
467de81
fix error log for token missing
wangxingjun778 Jul 9, 2026
86bfa20
feat(gateway): add platform integration gateway with TUI support
wangxingjun778 Jul 9, 2026
caf58db
fix(gateway): wire complete conduction chain between gateway and Leap…
wangxingjun778 Jul 9, 2026
d904fda
feat(gateway): bidirectional messaging + unified tool timeout
wangxingjun778 Jul 9, 2026
9719452
feat(security): unified approval gate with session memory
wangxingjun778 Jul 9, 2026
b85b55a
Improve TUI theming and exit resume summary
wangxingjun778 Jul 9, 2026
996bb7d
Fix CLI startup and improve TUI layout
wangxingjun778 Jul 9, 2026
c0ae831
fix mutli instance leap terminal
wangxingjun778 Jul 9, 2026
fd4e540
Implement leapd thin client runtime
wangxingjun778 Jul 9, 2026
bedd3ad
fix(cli): adapt TUI theme to terminal backgrounds
wangxingjun778 Jul 9, 2026
4aa338a
feat(cli): queue TUI commands without locking input
wangxingjun778 Jul 9, 2026
646bf1a
fix(cli): degrade first run and hot reload LLM config
wangxingjun778 Jul 9, 2026
1d2cb34
fix(cli): hot reload config in daemon sessions
wangxingjun778 Jul 9, 2026
e31af94
fix(cli): unify LLM context budget display
wangxingjun778 Jul 9, 2026
b01af51
docs(readme): prefer direct leap TUI command
wangxingjun778 Jul 9, 2026
402ed35
fix(cli): report TUI context usage
wangxingjun778 Jul 9, 2026
a03460e
fix(tui): compact large pasted input
wangxingjun778 Jul 9, 2026
33f8e0a
fix(tui): context token usage always showing 0/256K
wangxingjun778 Jul 9, 2026
c78b276
update app.py
wangxingjun778 Jul 9, 2026
a011e71
feat(daemon): improve leapd lifecycle UX
wangxingjun778 Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ LEAPFLOW_LLM_API_KEY=
LEAPFLOW_LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
LEAPFLOW_LLM_MODEL=qwen3.7-plus
LEAPFLOW_LLM_MAX_RETRIES=3
# Runtime context budget in tokens. Set to the usable limit for your provider/model.
LEAPFLOW_LLM_CONTEXT_LENGTH=256000

# ═══════════════════════════════════════════════════════════════════════
# Bridge / Host — communication between Python Brain and platform Host
Expand Down
245 changes: 233 additions & 12 deletions README.md

Large diffs are not rendered by default.

17 changes: 11 additions & 6 deletions src/leapflow/_env_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
LEAPFLOW_LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
LEAPFLOW_LLM_MODEL=qwen3.7-plus
LEAPFLOW_LLM_MAX_RETRIES=3
# Runtime context budget in tokens. Set to the usable limit for your provider/model.
LEAPFLOW_LLM_CONTEXT_LENGTH=256000

# ═══════════════════════════════════════════════════════════════════════
# Platform — CuaDriver communication
Expand All @@ -32,7 +34,8 @@
# ═══════════════════════════════════════════════════════════════════════
# Storage — persistent state (memory, trajectories, skill library)
# ═══════════════════════════════════════════════════════════════════════
LEAPFLOW_DUCKDB_PATH=~/.leapflow/memory.duckdb
# LEAPFLOW_DUCKDB_PATH=~/.leapflow/profiles/default/db/leap.duckdb
# LEAPFLOW_PROFILE=default

# ═══════════════════════════════════════════════════════════════════════
# Memory Providers — storage and retrieval configuration
Expand All @@ -46,7 +49,7 @@
# Logging & Audit
# ═══════════════════════════════════════════════════════════════════════
LEAPFLOW_LOG_LEVEL=INFO
LEAPFLOW_AUDIT_LOG_PATH=~/.leapflow/audit.jsonl
LEAPFLOW_AUDIT_LOG_PATH=~/.leapflow/profiles/default/audit.jsonl

# ═══════════════════════════════════════════════════════════════════════
# Learning & Execution — SessionController behavior
Expand Down Expand Up @@ -96,7 +99,7 @@
# Max seconds per video segment file before auto-splitting.
# LEAPFLOW_VIDEO_MAX_SEGMENT_S=600
# Directory for video segment cache.
# LEAPFLOW_VIDEO_CACHE_DIR=~/.leapflow/cache/video
# LEAPFLOW_VIDEO_CACHE_DIR=~/.leapflow/profiles/default/cache/video

# ── Video Analysis ──
# LEAPFLOW_VIDEO_VLM_URL_SCHEME=base64
Expand Down Expand Up @@ -128,9 +131,11 @@

# ═══════════════════════════════════════════════════════════════════════
# Visual Track — screenshot-based perception (default/vision_only modes)
# Disabled by default on first run. Enable only after configuring
# LEAPFLOW_LLM_API_KEY or LEAPFLOW_VLM_API_KEY.
# ═══════════════════════════════════════════════════════════════════════
# LEAPFLOW_VISUAL_TRACK_ENABLED=true
# LEAPFLOW_VISUAL_FRAME_CACHE_DIR=~/.leapflow/cache/frames
# LEAPFLOW_VISUAL_TRACK_ENABLED=false
# LEAPFLOW_VISUAL_FRAME_CACHE_DIR=~/.leapflow/profiles/default/cache/frames
# LEAPFLOW_VLM_MODEL=
# LEAPFLOW_VLM_API_KEY=
# LEAPFLOW_VLM_BASE_URL=
Expand All @@ -149,7 +154,7 @@
# Perceptual Field — fine-grained per-context perception control
# ═══════════════════════════════════════════════════════════════════════
# LEAPFLOW_PERCEPTUAL_FIELD_ENABLED=false
# LEAPFLOW_PERCEPTUAL_FIELD_CONFIG=~/.leapflow/perceptual_fields.yaml
# LEAPFLOW_PERCEPTUAL_FIELD_CONFIG=~/.leapflow/profiles/default/perceptual_fields.yaml

# ═══════════════════════════════════════════════════════════════════════
# Context Learning Attention — signal/noise filtering during recording
Expand Down
162 changes: 101 additions & 61 deletions src/leapflow/cli/banner.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import time
from typing import Any, Dict, List, Optional, Sequence

from leapflow.cli.tui_app.theme import ResolvedTheme, Theme, detect_theme
from leapflow.version import __version__

VERSION = __version__
Expand All @@ -38,6 +39,8 @@
"hub_push": "hub",
"hub_pull": "hub",
"hub_search": "hub",
"gateway_connect": "gateway",
"gateway_send": "gateway",
}


Expand Down Expand Up @@ -68,13 +71,38 @@ def _categorize_skills(

# ── Rich banner (interactive REPL) ───────────────────────────────────

# Warm gold/amber/bronze palette (inspired by Hermes, adapted for LeapFlow)
_GOLD = "#FFD700"
_AMBER = "#FFBF00"
_BRONZE = "#CD7F32"
_CREAM = "#FFF8DC"
_DIM_GOLD = "#B8860B"
_WARM_GRAY = "#8B8682"
_MAX_PANEL_WIDTH = 132
_NARROW_WIDTH = 70
_MEDIUM_WIDTH = 100


class _BannerPalette:
def __init__(self, theme: Theme | ResolvedTheme) -> None:
self.accent = theme.accent
self.accent_dim = theme.accent_dim
self.text = theme.text
self.muted = theme.text_muted
self.border = theme.border
self.title = theme.panel_title
self.success = theme.success


def _trim(text: str, limit: int) -> str:
if limit <= 1:
return "…"
return text if len(text) <= limit else text[: limit - 1] + "…"


def _category_limit(width: int) -> int:
if width < _NARROW_WIDTH:
return 44
if width < _MEDIUM_WIDTH:
return 56
return 72


def _render_width(term_width: int) -> int:
return max(40, min(term_width, _MAX_PANEL_WIDTH))


def display_rich_banner(
Expand All @@ -87,34 +115,36 @@ def display_rich_banner(
skills: Optional[Sequence[Any]] = None,
context_length: int = 0,
mcp_tools: int = 0,
gateway_connected: Sequence[str] = (),
show_welcome: bool = True,
theme: Theme | ResolvedTheme | None = None,
) -> None:
"""Print the full-width Rich banner panel with tools/skills catalog."""
"""Print the adaptive Rich banner panel with tools/skills catalog."""
try:
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
except ImportError:
display_welcome()
return

console = Console(highlight=False, soft_wrap=True)
palette = _BannerPalette(theme or detect_theme())
term_width = shutil.get_terminal_size().columns
render_width = _render_width(term_width)
console = Console(highlight=False, soft_wrap=True, width=render_width)

# ── Left column: branding + session metadata ──
left_lines: list[str] = []
left_lines.append("")
left_lines.append(
f"[bold {_GOLD}]L[/] [dim {_DIM_GOLD}].[/] "
f"[bold {_GOLD}]E[/] [dim {_DIM_GOLD}].[/] "
f"[bold {_GOLD}]A[/] [dim {_DIM_GOLD}].[/] "
f"[bold {_GOLD}]P[/]"
f"[bold {palette.accent}]L[/] [{palette.accent_dim}].[/] "
f"[bold {palette.accent}]E[/] [{palette.accent_dim}].[/] "
f"[bold {palette.accent}]A[/] [{palette.accent_dim}].[/] "
f"[bold {palette.accent}]P[/]"
)
left_lines.append(f"[{_CREAM}]Learning and Evolving from Actual Practice[/]")
left_lines.append(f"[bold {_AMBER}]ModelScope[/]")
left_lines.append(f"[{palette.text}]Learning and Evolving from Actual Practice[/]")
left_lines.append(f"[bold {palette.accent}]ModelScope[/]")
if session_id:
left_lines.append(f"[{_WARM_GRAY}]Session: {session_id}[/]")
left_lines.append(f"[{palette.muted}]Session: {_trim(session_id, 24)}[/]")
left_lines.append("")

if model:
Expand All @@ -125,87 +155,97 @@ def display_rich_banner(
ctx_label = f"{context_length // 1000}K"
else:
ctx_label = str(context_length)
ctx_str = f" [dim {_DIM_GOLD}]({ctx_label} ctx)[/]" if context_length else ""
left_lines.append(f"[bold {_AMBER}]{model_short}[/]{ctx_str}")
ctx_str = f" [{palette.muted}]({ctx_label} ctx)[/]" if context_length else ""
left_lines.append(f"[bold {palette.accent}]{model_short}[/]{ctx_str}")

if cwd:
short_cwd = cwd.replace(os.path.expanduser("~"), "~")
left_lines.append(f"[{_WARM_GRAY}]{short_cwd}[/]")
left_lines.append(f"[{palette.muted}]{_trim(short_cwd, 40)}[/]")

left_lines.append("")
left_content = "\n".join(left_lines)

# ── Right column: tools + skills catalog ──
# ── Right column: compact capability overview ──
right_lines: list[str] = []

if tool_defs:
tool_groups = _categorize_tools(tool_defs)
right_lines.append(f"[bold {_AMBER}]Available Tools[/]")
for cat, names in tool_groups.items():
names_str = ", ".join(sorted(names))
if len(names_str) > 52:
names_str = names_str[:49] + "…"
right_lines.append(f"[{_DIM_GOLD}]{cat}:[/] [{_CREAM}]{names_str}[/]")
tool_count = len(tool_defs) if tool_defs else 0
skill_count = len(skills) if skills else 0

if tool_count:
tool_groups = _categorize_tools(tool_defs)
categories = ", ".join(
f"{cat}({len(names)})" for cat, names in tool_groups.items()
)
category_text = _trim(categories, _category_limit(render_width))
right_lines.append(
f"[bold {palette.accent}]Tools[/] [{palette.text}]{tool_count} available[/]"
)
right_lines.append(f"[{palette.accent_dim}]{category_text}[/]")
if mcp_tools > 0:
right_lines.append(f"[{_DIM_GOLD}]mcp:[/] [{_CREAM}]{mcp_tools} platform tools[/]")
right_lines.append(
f"[{palette.accent_dim}]mcp:[/] [{palette.text}]{mcp_tools} platform tools[/]"
)

if skills:
right_lines.append("")
right_lines.append(f"[bold {_AMBER}]Available Skills[/]")
if skill_count:
skill_groups = _categorize_skills(skills)
for cat, names in skill_groups.items():
names_str = ", ".join(sorted(names))
if len(names_str) > 52:
names_str = names_str[:49] + "…"
right_lines.append(f"[{_DIM_GOLD}]{cat}:[/] [{_CREAM}]{names_str}[/]")
categories = ", ".join(
f"{cat}({len(names)})" for cat, names in skill_groups.items()
)
category_text = _trim(categories, _category_limit(render_width))
right_lines.append(
f"[bold {palette.accent}]Skills[/] [{palette.text}]{skill_count} available[/]"
)
right_lines.append(f"[{palette.accent_dim}]{category_text}[/]")

if gateway_connected:
right_lines.append("")
right_lines.append(f"[bold {palette.accent}]Gateway[/]")
names_str = _trim(", ".join(gateway_connected), 52)
right_lines.append(f"[{palette.success}]●[/] [{palette.text}]{names_str}[/]")

if not tool_defs and not skills:
right_lines.append(f"[{_WARM_GRAY}]No tools or skills loaded[/]")
right_lines.append(f"[{palette.muted}]No tools or skills loaded[/]")

# Summary line
tool_count = len(tool_defs) if tool_defs else 0
skill_count = len(skills) if skills else 0
summary_parts = []
if tool_count:
summary_parts.append(f"{tool_count} tools")
if skill_count:
summary_parts.append(f"{skill_count} skills")
if mcp_tools:
summary_parts.append(f"{mcp_tools} mcp")
summary_parts.append("/help for commands")
if gateway_connected:
summary_parts.append(f"{len(gateway_connected)} gateway")
summary_parts.append("/help · /tools · /skills")
right_lines.append("")
right_lines.append(f"[{_WARM_GRAY}]{' · '.join(summary_parts)}[/]")
right_lines.append(f"[{palette.muted}]{' · '.join(summary_parts)}[/]")

right_content = "\n".join(right_lines)

# ── Assembly — full-width panel ──
if term_width < 70:
# ── Assembly — width-capped panel ──
version_label = f"LeapFlow v{VERSION}"
conn = f"[{palette.success}]●[/]" if platform_online else f"[{palette.muted}]○[/]"
if render_width < _NARROW_WIDTH:
# Narrow: single-column compact
combined = left_content + "\n" + right_content
version_label = f"LeapFlow v{VERSION}"
conn = "[green]●[/]" if platform_online else f"[{_WARM_GRAY}]○[/]"
panel = Panel(
combined,
title=f"[bold {_GOLD}]{version_label}[/] {conn}",
border_style=_BRONZE,
padding=(0, 2),
title=f"[{palette.title}]{version_label}[/] {conn}",
border_style=palette.border,
padding=(0, 1),
expand=True,
)
else:
layout = Table.grid(padding=(0, 3))
left_min = max(38, term_width // 3)
layout = Table.grid(padding=(0, 2))
left_min = max(28, min(44, render_width // 3))
layout.add_column("left", justify="center", min_width=left_min)
layout.add_column("right", justify="left", ratio=1)
layout.add_row(left_content, right_content)

version_label = f"LeapFlow v{VERSION}"
conn = "[green]●[/]" if platform_online else f"[{_WARM_GRAY}]○[/]"
panel = Panel(
layout,
title=f"[bold {_GOLD}]{version_label}[/] {conn}",
border_style=_BRONZE,
padding=(0, 2),
title=f"[{palette.title}]{version_label}[/] {conn}",
border_style=palette.border,
padding=(0, 1),
expand=True,
)

Expand All @@ -214,8 +254,8 @@ def display_rich_banner(

if show_welcome:
console.print(
f"\n[{_CREAM}]Welcome to LeapFlow! "
f"Type your message or [bold {_AMBER}]/help[/bold {_AMBER}] for commands.[/]\n",
f"\n[{palette.text}]Welcome to LeapFlow! "
f"Type your message or [bold {palette.accent}]/help[/bold {palette.accent}] for commands.[/]\n",
)


Expand Down
Loading