nexforge-cli 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/PKG-INFO +1 -1
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/pyproject.toml +1 -1
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/screens/chat.py +20 -3
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/widgets/messages.py +145 -134
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/.gitignore +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/= +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/INSTALL.md +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/README.md +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/assets/icons/agent.svg +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/assets/icons/cost.svg +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/assets/icons/edit.svg +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/assets/icons/err.svg +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/assets/icons/file.svg +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/assets/icons/model.svg +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/assets/icons/ok.svg +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/assets/icons/read.svg +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/assets/icons/running.svg +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/assets/icons/search.svg +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/assets/icons/shell.svg +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/assets/icons/thinking.svg +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/assets/icons/web.svg +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/assets/icons/write.svg +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/config.example.toml +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/devlog/2026-07-07.md +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//344/274/232/350/257/235/347/256/241/347/220/206.md" +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//345/221/275/344/273/244/344/270/216/345/277/253/346/215/267/351/224/256.md" +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//345/256/211/350/243/205/344/270/216/351/205/215/347/275/256.md" +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//345/257/271/346/257/224/345/210/206/346/236/220.md" +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//345/267/245/345/205/267/345/217/202/350/200/203.md" +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//346/200/273/350/247/210.md" +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//346/235/203/351/231/220/347/263/273/347/273/237.md" +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//350/256/241/345/210/222/346/250/241/345/274/217.md" +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//351/253/230/347/272/247/345/212/237/350/203/275.md" +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/scripts/publish.sh +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/scripts/smoke_provider.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/scripts/smoke_tui.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/__init__.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/__main__.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/app.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/cli.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/config.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/core/__init__.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/core/agent.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/core/jobs.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/core/permissions.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/core/router.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/core/session.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/core/types.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/icons.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/mcp/__init__.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/provider/__init__.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/provider/balance.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/provider/deepseek.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/skills/loader.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/store/__init__.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/store/plan_store.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/store/session_store.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/theme.tcss +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/__init__.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/_paths.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/background.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/base.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/edit_file.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/fs_ops.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/git.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/list_dir.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/multi_edit.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/read_file.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/registry.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/search_content.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/search_files.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/shell.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/web.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/tools/write_file.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/__init__.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/screens/__init__.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/widgets/__init__.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/widgets/apikey_modal.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/widgets/banner.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/widgets/confirm_modal.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/widgets/diff_modal.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/widgets/mode_warning.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/widgets/multiline_modal.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/widgets/multiline_prompt.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/widgets/plan_sidebar.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/widgets/sidebar.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/ui/widgets/statusbar.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/web/__init__.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/web/server.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/src/nexforge/web/static/index.html +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/tests/test_approval.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/tests/test_config.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/tests/test_icons.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/tests/test_m2_tools.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/tests/test_sidebar.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/tests/test_sidebar_actions.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/tests/test_tools.py +0 -0
- {nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/tests/test_tui.py +0 -0
|
@@ -100,6 +100,7 @@ class ChatScreen(Screen):
|
|
|
100
100
|
self._session_name: str = ""
|
|
101
101
|
self._follow: bool = True # 流式时自动跟底,用户上滚后暂停
|
|
102
102
|
self._scroll_pending: bool = False # 滚动节流:同一刷新周期只排一次
|
|
103
|
+
self._scroll_safety_active: bool = False # 安全定时器防重
|
|
103
104
|
self._agent_running: bool = False
|
|
104
105
|
|
|
105
106
|
@property
|
|
@@ -320,9 +321,6 @@ class ChatScreen(Screen):
|
|
|
320
321
|
else:
|
|
321
322
|
self._note(f"未知命令:{text}(/help 查看)", error=True)
|
|
322
323
|
self._refresh_status()
|
|
323
|
-
# 延迟滚动:等 SystemNote 被 call_later 挂载完成后再滚底
|
|
324
|
-
log = self._log
|
|
325
|
-
self.set_timer(0.15, lambda: log.scroll_end(animate=False))
|
|
326
324
|
|
|
327
325
|
# ---- Agent 事件流 -----------------------------------------------------
|
|
328
326
|
|
|
@@ -402,9 +400,19 @@ class ChatScreen(Screen):
|
|
|
402
400
|
|
|
403
401
|
def _do() -> None:
|
|
404
402
|
self._scroll_pending = False
|
|
403
|
+
self._scroll_safety_active = False
|
|
405
404
|
log.scroll_end(animate=False)
|
|
406
405
|
|
|
407
406
|
self.call_after_refresh(_do)
|
|
407
|
+
# 安全超时:如果回调500ms后仍未执行(可能因widget移除被丢弃),强制复位
|
|
408
|
+
if not self._scroll_safety_active:
|
|
409
|
+
self._scroll_safety_active = True
|
|
410
|
+
self.set_timer(0.5, self._reset_scroll_safety)
|
|
411
|
+
|
|
412
|
+
def _reset_scroll_safety(self) -> None:
|
|
413
|
+
"""安全定时器回调:强制复位 flags。"""
|
|
414
|
+
self._scroll_pending = False
|
|
415
|
+
self._scroll_safety_active = False
|
|
408
416
|
|
|
409
417
|
def _set_follow(self, on: bool) -> None:
|
|
410
418
|
"""on=True 时强制滚到底(新回合/回底键);on=False 不做事。
|
|
@@ -416,6 +424,15 @@ class ChatScreen(Screen):
|
|
|
416
424
|
self.call_after_refresh(lambda: log.scroll_end(animate=False))
|
|
417
425
|
except Exception:
|
|
418
426
|
pass
|
|
427
|
+
"""on=True 时强制滚到底(新回合/回底键);on=False 不做事。
|
|
428
|
+
不再用 anchor(),避免与鼠标拖动拉锄导致卡死。"""
|
|
429
|
+
self._follow = on
|
|
430
|
+
if on:
|
|
431
|
+
try:
|
|
432
|
+
log = self._log
|
|
433
|
+
self.call_after_refresh(lambda: log.scroll_end(animate=False))
|
|
434
|
+
except Exception:
|
|
435
|
+
pass
|
|
419
436
|
|
|
420
437
|
def _note(self, text: str, *, error: bool = False, scroll: bool = False) -> None:
|
|
421
438
|
if scroll:
|
|
@@ -1,134 +1,145 @@
|
|
|
1
|
-
"""消息组件:用户消息、助手回合(思考面板 + Markdown 正文)、工具卡片、系统提示。"""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from textual.
|
|
7
|
-
from textual.
|
|
8
|
-
from
|
|
9
|
-
from rich.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class SystemNote(Static):
|
|
13
|
-
"""系统 / 错误提示行。"""
|
|
14
|
-
|
|
15
|
-
def __init__(self, text: str, *, error: bool = False) -> None:
|
|
16
|
-
from nexforge.icons import icon
|
|
17
|
-
|
|
18
|
-
sym = icon("err") if error else icon("running")
|
|
19
|
-
color = "red" if error else "#22D3EE"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
self.
|
|
43
|
-
|
|
44
|
-
def
|
|
45
|
-
from nexforge.icons import icon
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
self.
|
|
62
|
-
|
|
63
|
-
self.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
self.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
self.query_one("#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
try:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
1
|
+
"""消息组件:用户消息、助手回合(思考面板 + Markdown 正文)、工具卡片、系统提示。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import Vertical
|
|
7
|
+
from textual.widgets import Collapsible, Static
|
|
8
|
+
from rich.markdown import Markdown as RichMarkdown
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SystemNote(Static):
|
|
13
|
+
"""系统 / 错误提示行。"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, text: str, *, error: bool = False) -> None:
|
|
16
|
+
from nexforge.icons import icon
|
|
17
|
+
|
|
18
|
+
sym = icon("err") if error else icon("running")
|
|
19
|
+
color = "red" if error else "#22D3EE"
|
|
20
|
+
t = Text()
|
|
21
|
+
t.append(f"{sym} ", style=color)
|
|
22
|
+
t.append(text)
|
|
23
|
+
super().__init__(t)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UserMessage(Static):
|
|
27
|
+
def __init__(self, text: str) -> None:
|
|
28
|
+
from nexforge.icons import icon
|
|
29
|
+
|
|
30
|
+
t = Text()
|
|
31
|
+
t.append(f"{icon('user')} ", style="bold #6D5EF5")
|
|
32
|
+
t.append(text)
|
|
33
|
+
super().__init__(t)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ToolCard(Static):
|
|
37
|
+
"""一次工具调用:执行中 → 完成(✔/✖)。"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, preview: str, icon_name: str = "running") -> None:
|
|
40
|
+
super().__init__()
|
|
41
|
+
self._preview = preview
|
|
42
|
+
self._icon = icon_name
|
|
43
|
+
|
|
44
|
+
def on_mount(self) -> None:
|
|
45
|
+
from nexforge.icons import icon
|
|
46
|
+
|
|
47
|
+
ic = icon(self._icon)
|
|
48
|
+
t = Text()
|
|
49
|
+
t.append(f"{ic} ", style="#22D3EE")
|
|
50
|
+
t.append(self._preview)
|
|
51
|
+
t.append(" 执行…", style="dim")
|
|
52
|
+
self.update(t)
|
|
53
|
+
|
|
54
|
+
def set_result(self, ok: bool, summary: str) -> None:
|
|
55
|
+
from nexforge.icons import icon
|
|
56
|
+
|
|
57
|
+
mark = icon("ok") if ok else icon("err")
|
|
58
|
+
color = "green" if ok else "red"
|
|
59
|
+
t = Text()
|
|
60
|
+
t.append(f"{mark} ", style=color)
|
|
61
|
+
t.append(f"{self._preview} ", style="#22D3EE")
|
|
62
|
+
t.append(summary, style="dim")
|
|
63
|
+
self.update(t)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AssistantTurn(Vertical):
|
|
67
|
+
"""一轮助手输出:可折叠思考面板 + Markdown 正文。"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, initial_content: str = "") -> None:
|
|
70
|
+
super().__init__()
|
|
71
|
+
self._reasoning = ""
|
|
72
|
+
self._content = initial_content
|
|
73
|
+
self._collapsed_once = False
|
|
74
|
+
self._body: Static | None = None # 思考面板引用缓存
|
|
75
|
+
self._txt: Static | None = None # 正文引用缓存
|
|
76
|
+
|
|
77
|
+
def compose(self) -> ComposeResult:
|
|
78
|
+
import re
|
|
79
|
+
from nexforge.icons import icon
|
|
80
|
+
from rich.markup import escape as rich_escape
|
|
81
|
+
|
|
82
|
+
with Collapsible(title=f"{icon('thinking')} 思考", collapsed=False, id="think"):
|
|
83
|
+
yield Static("", id="reasoning-body")
|
|
84
|
+
# 流式时用 Static 快速渲染(避免每 token 全量 Markdown 解析变慢)
|
|
85
|
+
init = re.sub(r"\n{3,}", "\n\n", self._content).lstrip("\n")
|
|
86
|
+
yield Static(rich_escape(init), id="content-txt")
|
|
87
|
+
|
|
88
|
+
def on_mount(self) -> None:
|
|
89
|
+
# 未产生思考内容前隐藏思考面板
|
|
90
|
+
self.query_one("#think", Collapsible).display = False
|
|
91
|
+
|
|
92
|
+
def add_reasoning(self, text: str) -> None:
|
|
93
|
+
self._reasoning += text
|
|
94
|
+
panel = self.query_one("#think", Collapsible)
|
|
95
|
+
if not panel.display:
|
|
96
|
+
panel.display = True
|
|
97
|
+
if self._body is None:
|
|
98
|
+
self._body = self.query_one("#reasoning-body", Static)
|
|
99
|
+
self._body.update(self._reasoning)
|
|
100
|
+
# 思考面板的 Contents 有 overflow-y:auto,每次更新后滚到底
|
|
101
|
+
try:
|
|
102
|
+
contents = panel.query_one("Contents")
|
|
103
|
+
contents.scroll_end(animate=False)
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
def add_content(self, text: str) -> None:
|
|
108
|
+
# 正文开始 → 折叠思考面板
|
|
109
|
+
if not self._collapsed_once and self._reasoning:
|
|
110
|
+
self.query_one("#think", Collapsible).collapsed = True
|
|
111
|
+
self._collapsed_once = True
|
|
112
|
+
self._content += text
|
|
113
|
+
from rich.markup import escape as rich_escape
|
|
114
|
+
if self._txt is None:
|
|
115
|
+
self._txt = self.query_one("#content-txt", Static)
|
|
116
|
+
self._txt.update(rich_escape(self._content))
|
|
117
|
+
|
|
118
|
+
def finalize_content(self) -> None:
|
|
119
|
+
"""回合结束:用 Rich Markdown 渲染到 Static(无 Textual Markdown bug)。"""
|
|
120
|
+
import re
|
|
121
|
+
try:
|
|
122
|
+
self.query_one("#content-txt", Static).remove()
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
try:
|
|
126
|
+
self.query_one("#think", Collapsible).remove()
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
normalized = re.sub(r"\n{3,}", "\n\n", self._content).strip("\n")
|
|
130
|
+
rich_md = RichMarkdown(normalized)
|
|
131
|
+
self.mount(Static(rich_md, id="content-rt"))
|
|
132
|
+
self._scroll_to_end()
|
|
133
|
+
|
|
134
|
+
def _scroll_to_end(self) -> None:
|
|
135
|
+
"""精确找到 #chat-log 并滚到底。"""
|
|
136
|
+
try:
|
|
137
|
+
from textual.containers import VerticalScroll
|
|
138
|
+
log = self.app.query_one("#chat-log", VerticalScroll)
|
|
139
|
+
log.scroll_end(animate=False)
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def is_empty(self) -> bool:
|
|
145
|
+
return not self._reasoning and not self._content
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//344/274/232/350/257/235/347/256/241/347/220/206.md"
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//345/257/271/346/257/224/345/210/206/346/236/220.md"
RENAMED
|
File without changes
|
{nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//345/267/245/345/205/267/345/217/202/350/200/203.md"
RENAMED
|
File without changes
|
|
File without changes
|
{nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//346/235/203/351/231/220/347/263/273/347/273/237.md"
RENAMED
|
File without changes
|
{nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//350/256/241/345/210/222/346/250/241/345/274/217.md"
RENAMED
|
File without changes
|
{nexforge_cli-0.2.2 → nexforge_cli-0.2.4}/doc//351/253/230/347/272/247/345/212/237/350/203/275.md"
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|