fix(v8.0): 启用真·全屏 textual TUI 并修复 opacity 崩溃
- 修复崩溃: tui.py MessageBubble.on_mount 用 self.animate('opacity'),但
textual>=8 的 Widget.opacity 为只读属性会抛 'has no setter',导致每条消息
气泡崩溃 -> 改 styles.animate 并兜底直接设置。
- run_tui 改为默认真·全屏 textual XiaoliTUI(上方滚动聊天区+底部固定输入框+侧栏),
rich 面板仅作 textual 不可用时的回退(此前默认的是顺序面板,被指出是'假TUI')。
- 版本统一 v8.0.0: 欢迎框 + /about 改用 VERSION 常量。
- 状态栏联动 token 感知: 显示 used/context_window 进度条(引擎支持时)。
- 新增 /snapshot(/screenshot) 命令: 导出当前屏幕 SVG 到 .tui_snapshots/live.svg,
让实时 TUI 也能被 AI 自验。
- 新增 tools/tui_real_snapshot.py: textual run_test+Pilot 无头驱动真实 TUI,
喂对话、断言 DOM(用户/工具成功失败/助手/状态栏/token条/欢迎版本),导出 SVG。
7 项断言全过。
- 注: launcher 启动会重写 plugins_config.py/requirements.txt(含 tkinter 等错误项),
提交前已还原,避免误提交漂移。
This commit is contained in:
1 parent
79957eba0f
commit
db9203687d
3 files changed
+191
-15
No files matched your search
@@ -999,7 +999,13 @@ multi 操作支持一次修改多处:
|
||||
# ── CLI 主循环 ──
|
||||
|
||||
def run_tui(self):
|
||||
"""运行 TUI 模式(v8.0 默认 Rich 面板 TUI,支持快照自验;textual 作为可选回退)"""
|
||||
"""运行 TUI 模式(v8.0 默认真·全屏 textual TUI;textual 不可用时回退 rich 面板)"""
|
||||
if TEXTUAL_AVAILABLE:
|
||||
from .tui import XiaoliTUI
|
||||
app = XiaoliTUI(self)
|
||||
app.run()
|
||||
return
|
||||
# 回退 rich 面板 TUI(textual 不可用时)
|
||||
try:
|
||||
from .rich_tui import RichTUI
|
||||
from xcli_core.constants import VERSION as _V
|
||||
@@ -1013,15 +1019,8 @@ multi 操作支持一次修改多处:
|
||||
self.tui_output_callback = tui.push
|
||||
tui.run()
|
||||
self.tui_output_callback = None
|
||||
return
|
||||
except ImportError:
|
||||
# 回退到旧版 textual TUI
|
||||
if not TEXTUAL_AVAILABLE:
|
||||
print(f"{Fore.RED}TUI 不可用: 请安装 rich (pip install rich){Style.RESET_ALL}")
|
||||
return
|
||||
from .tui import XiaoliTUI
|
||||
app = XiaoliTUI(self)
|
||||
app.run()
|
||||
print(f"{Fore.RED}TUI 不可用: 请安装 textual (pip install textual){Style.RESET_ALL}")
|
||||
|
||||
def run(self):
|
||||
"""运行 CLI"""
|
||||
|
||||
+50
-6
@@ -1,6 +1,6 @@
|
||||
"""TUI 界面模块 - Textual TUI 实现 (动画增强版 + 多行输入)"""
|
||||
|
||||
from .constants import TEXTUAL_AVAILABLE
|
||||
from .constants import TEXTUAL_AVAILABLE, VERSION
|
||||
|
||||
if TEXTUAL_AVAILABLE:
|
||||
import re
|
||||
@@ -379,7 +379,12 @@ if TEXTUAL_AVAILABLE:
|
||||
self.styles.opacity = 0
|
||||
|
||||
def on_mount(self):
|
||||
self.animate("opacity", value=1.0, duration=0.3, easing="out_cubic")
|
||||
# textual >= 8: Widget.opacity 是只读属性,直接 animate 会抛
|
||||
# "property 'opacity' has no setter"。改为在 styles 上动画,失败则直接到位。
|
||||
try:
|
||||
self.styles.animate("opacity", value=1.0, duration=0.3, easing="out_cubic")
|
||||
except Exception:
|
||||
self.styles.opacity = 1.0
|
||||
|
||||
|
||||
class TypingIndicator(Static):
|
||||
@@ -498,7 +503,7 @@ if TEXTUAL_AVAILABLE:
|
||||
"",
|
||||
" ╔══════════════════════════════════════════════╗",
|
||||
" ║ ║",
|
||||
" ║ 小狸 Pro-CLI v5.4.1 ║",
|
||||
" ║ 小狸 Pro-CLI v8.0.0 ║",
|
||||
" ║ 智能编程助手 · 动画增强版 ║",
|
||||
" ║ ║",
|
||||
" ╚══════════════════════════════════════════════╝",
|
||||
@@ -542,7 +547,7 @@ if TEXTUAL_AVAILABLE:
|
||||
("", "msg-dim"),
|
||||
(" ╔══════════════════════════════════════════════╗", "msg-welcome"),
|
||||
(" ║ ║", "msg-welcome"),
|
||||
(" ║ 小狸 Pro-CLI v5.4.1 ║", "msg-welcome"),
|
||||
(" ║ 小狸 Pro-CLI v8.0.0 ║", "msg-welcome"),
|
||||
(" ║ 智能编程助手 · 动画增强版 ║", "msg-welcome"),
|
||||
(" ║ ║", "msg-welcome"),
|
||||
(" ╚══════════════════════════════════════════════╝", "msg-welcome"),
|
||||
@@ -587,7 +592,28 @@ if TEXTUAL_AVAILABLE:
|
||||
def _update_status(self, text: str):
|
||||
bar = self.query_one("#status-bar")
|
||||
engine = self.bridge.current_engine()
|
||||
bar.update(f" {text} | {engine} | {len(self.bridge.history())} 条对话")
|
||||
bar.update(f" {text} | {engine} | {len(self.bridge.history())} 条对话{self._token_str()}")
|
||||
|
||||
def _token_str(self):
|
||||
"""token 用量进度(v8.0 token 感知压缩联动,引擎支持时显示)"""
|
||||
e = getattr(self.cli, 'current_engine', None)
|
||||
if not e:
|
||||
return ""
|
||||
used = getattr(e, 'last_prompt_tokens', None)
|
||||
win = None
|
||||
if hasattr(e, 'context_window'):
|
||||
try:
|
||||
win = e.context_window()
|
||||
except Exception:
|
||||
win = None
|
||||
if used and win:
|
||||
ratio = min(1.0, used / win)
|
||||
filled = int(ratio * 10)
|
||||
bar_s = "▓" * filled + "░" * (10 - filled)
|
||||
return f" | {used}/{win} {bar_s}"
|
||||
if used:
|
||||
return f" | {used} tok"
|
||||
return ""
|
||||
|
||||
# ── 消息追加 ──
|
||||
|
||||
@@ -743,12 +769,14 @@ if TEXTUAL_AVAILABLE:
|
||||
'cls': lambda: self.action_clear(),
|
||||
'model': lambda: self._switch_model(args),
|
||||
'engine': lambda: self._switch_model(args),
|
||||
'about': lambda: self._system(" 小狸 Pro-CLI v5.4.1 - 智能编程助手"),
|
||||
'about': lambda: self._system(f" 小狸 Pro-CLI v{VERSION} - 智能编程助手"),
|
||||
'status': lambda: self._show_status(),
|
||||
'tools': lambda: self._show_tools(),
|
||||
'engines': lambda: self._show_engines(),
|
||||
'tui': lambda: self._system("已在 TUI 模式中"),
|
||||
'manual': lambda: self._handle_manual(args),
|
||||
'snapshot': lambda: self._save_screenshot(),
|
||||
'screenshot': lambda: self._save_screenshot(),
|
||||
}
|
||||
|
||||
handler = cmds.get(name)
|
||||
@@ -918,6 +946,22 @@ if TEXTUAL_AVAILABLE:
|
||||
self._system("已取消")
|
||||
self.is_generating = False
|
||||
|
||||
def _save_screenshot(self):
|
||||
"""导出当前 TUI 屏幕截图(SVG)——AI 可据此自验实时渲染(/snapshot)"""
|
||||
import os
|
||||
out_dir = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".tui_snapshots"
|
||||
)
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
path = os.path.join(out_dir, "live.svg")
|
||||
try:
|
||||
svg = self.export_screenshot()
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(svg)
|
||||
self._system(f"截图已保存: {path}")
|
||||
except Exception as e:
|
||||
self._error(f"截图失败: {e}")
|
||||
|
||||
else:
|
||||
# Textual 不可用时的占位类
|
||||
class XiaoliTUI:
|
||||
|
||||
Reference in new issue
Block a user