xg-cli 1.0__py3-none-any.whl
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.
- xg/__init__.py +3 -0
- xg/__main__.py +3 -0
- xg/adaptive/__init__.py +27 -0
- xg/adaptive/calibrate.py +191 -0
- xg/adaptive/feedback.py +170 -0
- xg/adaptive/learned_rules.py +284 -0
- xg/adaptive/signals.py +125 -0
- xg/adaptive/store.py +164 -0
- xg/agent/__init__.py +0 -0
- xg/agent/plan.py +631 -0
- xg/agent/react.py +268 -0
- xg/agent/team.py +1793 -0
- xg/assets/router.lgb +0 -0
- xg/assets/router_semantics.json +21278 -0
- xg/assets/router_semantics.onnx +0 -0
- xg/cli/__init__.py +0 -0
- xg/cli/app.py +1372 -0
- xg/cli/commands.py +921 -0
- xg/cli/completion.py +740 -0
- xg/cli/help.py +185 -0
- xg/cli/train.py +200 -0
- xg/config/__init__.py +0 -0
- xg/config/env_writer.py +121 -0
- xg/config/manager.py +446 -0
- xg/config/mcp.py +229 -0
- xg/config/provider_service.py +267 -0
- xg/config/providers.py +46 -0
- xg/config/settings.py +258 -0
- xg/config/skills.py +100 -0
- xg/config/smart_router_service.py +121 -0
- xg/config/web.py +140 -0
- xg/input_history/__init__.py +7 -0
- xg/input_history/models.py +28 -0
- xg/input_history/persistence.py +126 -0
- xg/input_history/policy.py +39 -0
- xg/input_history/prompt_toolkit.py +36 -0
- xg/input_history/store.py +118 -0
- xg/llm/__init__.py +0 -0
- xg/llm/client.py +49 -0
- xg/llm/factory.py +40 -0
- xg/llm/openai_compat.py +275 -0
- xg/llm/types.py +98 -0
- xg/mcp/__init__.py +4 -0
- xg/mcp/http.py +192 -0
- xg/mcp/manager.py +726 -0
- xg/mcp/models.py +86 -0
- xg/mcp/protocol.py +62 -0
- xg/mcp/resources.py +72 -0
- xg/mcp/schema.py +137 -0
- xg/mcp/stdio.py +210 -0
- xg/mcp/transport.py +66 -0
- xg/memory/__init__.py +15 -0
- xg/memory/context.py +327 -0
- xg/memory/manager.py +111 -0
- xg/memory/models.py +41 -0
- xg/memory/project.py +187 -0
- xg/memory/store.py +144 -0
- xg/router/__init__.py +124 -0
- xg/router/features.py +66 -0
- xg/router/keywords.py +50 -0
- xg/router/ml_router.py +178 -0
- xg/router/model_tiers.py +73 -0
- xg/router/postprocess.py +167 -0
- xg/router/rule_router.py +77 -0
- xg/router/semantic.py +138 -0
- xg/safety/__init__.py +0 -0
- xg/safety/audit.py +96 -0
- xg/safety/guards.py +106 -0
- xg/safety/hitl.py +73 -0
- xg/skill/__init__.py +9 -0
- xg/skill/errors.py +45 -0
- xg/skill/loader.py +42 -0
- xg/skill/models.py +57 -0
- xg/skill/parser.py +93 -0
- xg/skill/policy.py +40 -0
- xg/skill/prompt.py +45 -0
- xg/skill/registry.py +169 -0
- xg/tool/__init__.py +0 -0
- xg/tool/builtin.py +356 -0
- xg/tool/registry.py +228 -0
- xg/tui/__init__.py +34 -0
- xg/tui/app.py +612 -0
- xg/tui/controller.py +1296 -0
- xg/tui/diagrams/__init__.py +22 -0
- xg/tui/diagrams/layout.py +110 -0
- xg/tui/diagrams/markdown.py +39 -0
- xg/tui/diagrams/model.py +36 -0
- xg/tui/diagrams/parser.py +119 -0
- xg/tui/diagrams/renderer.py +551 -0
- xg/tui/i18n.py +169 -0
- xg/tui/messages.py +45 -0
- xg/tui/plan_renderables.py +147 -0
- xg/tui/reducer.py +1029 -0
- xg/tui/renderables.py +252 -0
- xg/tui/state.py +240 -0
- xg/tui/theme.tcss +208 -0
- xg/tui/widgets/__init__.py +1 -0
- xg/tui/widgets/action_card.py +94 -0
- xg/tui/widgets/agent_group_card.py +39 -0
- xg/tui/widgets/approval_modal.py +59 -0
- xg/tui/widgets/collapsible_card.py +40 -0
- xg/tui/widgets/command_suggestions.py +128 -0
- xg/tui/widgets/composer.py +151 -0
- xg/tui/widgets/config_panel.py +198 -0
- xg/tui/widgets/confirm_modal.py +31 -0
- xg/tui/widgets/footer.py +9 -0
- xg/tui/widgets/header.py +118 -0
- xg/tui/widgets/inspector.py +378 -0
- xg/tui/widgets/plan_modal.py +53 -0
- xg/tui/widgets/provider_form.py +141 -0
- xg/tui/widgets/queue_status.py +30 -0
- xg/tui/widgets/smart_router_form.py +95 -0
- xg/tui/widgets/transcript.py +354 -0
- xg/tui/workers.py +14 -0
- xg/web/__init__.py +21 -0
- xg/web/errors.py +57 -0
- xg/web/extract.py +127 -0
- xg/web/fetch.py +106 -0
- xg/web/markdown.py +77 -0
- xg/web/models.py +91 -0
- xg/web/providers.py +79 -0
- xg/web/search.py +118 -0
- xg/web/searxng.py +27 -0
- xg/web/serpapi.py +29 -0
- xg/web/url_policy.py +110 -0
- xg/web/zhipu.py +27 -0
- xg_cli-1.0.dist-info/METADATA +284 -0
- xg_cli-1.0.dist-info/RECORD +130 -0
- xg_cli-1.0.dist-info/WHEEL +4 -0
- xg_cli-1.0.dist-info/entry_points.txt +2 -0
xg/tui/renderables.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Rich renderables for transcript items."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from rich.console import Console, ConsoleOptions, Group, RenderResult
|
|
8
|
+
from rich.markdown import Markdown
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from xg.tui.diagrams import FlowchartModel, FlowchartParseError, parse_flowchart, render_flowchart, split_mermaid_blocks
|
|
13
|
+
from xg.tui.plan_renderables import PlanReviewCard
|
|
14
|
+
from xg.tui.state import AgentGroupState, TranscriptItem
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _truncate(value: str, limit: int = 20_000) -> str:
|
|
18
|
+
return value if len(value) <= limit else value[:limit] + "\n… (输出已截断)"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _mermaid_source(source: str) -> str:
|
|
22
|
+
return "```mermaid\n" + _truncate(source) + "\n```"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _trace_status(item: TranscriptItem) -> str:
|
|
26
|
+
return {
|
|
27
|
+
"streaming": "进行中",
|
|
28
|
+
"running": "执行中",
|
|
29
|
+
"success": "成功",
|
|
30
|
+
"failed": "失败",
|
|
31
|
+
"blocked": "已阻塞",
|
|
32
|
+
"needs_input": "等待输入",
|
|
33
|
+
"skipped": "已跳过",
|
|
34
|
+
"cancelled": "已取消",
|
|
35
|
+
"done": "已完成",
|
|
36
|
+
}.get(item.status, item.status)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _trace_summary(item: TranscriptItem) -> str:
|
|
40
|
+
if item.kind == "thinking":
|
|
41
|
+
label = "思考"
|
|
42
|
+
elif item.kind == "tool_call":
|
|
43
|
+
label = f"工具调用 · {item.tool_name}"
|
|
44
|
+
elif item.kind == "tool_result":
|
|
45
|
+
label = f"工具结果 · {item.tool_name}"
|
|
46
|
+
else:
|
|
47
|
+
label = item.kind
|
|
48
|
+
source = item.text or item.tool_args or "无详细内容"
|
|
49
|
+
first_line = next((line.strip() for line in source.splitlines() if line.strip()), "无详细内容")
|
|
50
|
+
return f"{label} · {_trace_status(item)} · {first_line[:100]}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def trace_renderable(item: TranscriptItem):
|
|
54
|
+
"""Render a collapsible trace item; the widget owns click behavior."""
|
|
55
|
+
marker = "▶" if item.collapsed else "▼"
|
|
56
|
+
if item.kind == "thinking":
|
|
57
|
+
label = "思考"
|
|
58
|
+
detail = _truncate(item.text, 12_000)
|
|
59
|
+
elif item.kind == "tool_call":
|
|
60
|
+
label = f"工具调用 · {item.tool_name}"
|
|
61
|
+
try:
|
|
62
|
+
detail = json.dumps(json.loads(item.tool_args), ensure_ascii=False, indent=2)
|
|
63
|
+
except (TypeError, ValueError):
|
|
64
|
+
detail = item.tool_args
|
|
65
|
+
detail = _truncate(detail, 4_000)
|
|
66
|
+
elif item.kind == "tool_result":
|
|
67
|
+
label = f"工具结果 · {item.tool_name}"
|
|
68
|
+
detail = _truncate(item.text, 12_000)
|
|
69
|
+
else:
|
|
70
|
+
label = f"审批 · {item.tool_name}"
|
|
71
|
+
detail = _truncate(item.text or "工具审批结果", 2_000)
|
|
72
|
+
body = detail if not item.collapsed else _trace_summary(item)
|
|
73
|
+
style = {
|
|
74
|
+
"streaming": "yellow",
|
|
75
|
+
"running": "yellow",
|
|
76
|
+
"success": "green",
|
|
77
|
+
"failed": "red",
|
|
78
|
+
"blocked": "yellow",
|
|
79
|
+
"skipped": "cyan",
|
|
80
|
+
"cancelled": "yellow",
|
|
81
|
+
}.get(item.status, "cyan")
|
|
82
|
+
title = f"{marker} {label} · {_trace_status(item)}"
|
|
83
|
+
return Panel(Text(body), title=title, border_style=style)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _agent_group_status(status: str) -> str:
|
|
87
|
+
return {
|
|
88
|
+
"pending": "待执行",
|
|
89
|
+
"running": "执行中",
|
|
90
|
+
"reviewing": "审查中",
|
|
91
|
+
"repairing": "修复中",
|
|
92
|
+
"done": "已完成",
|
|
93
|
+
"failed": "失败",
|
|
94
|
+
"blocked": "已阻塞",
|
|
95
|
+
"needs_input": "等待输入",
|
|
96
|
+
"skipped": "已跳过",
|
|
97
|
+
"cancelled": "已取消",
|
|
98
|
+
}.get(status, status or "未知")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def agent_group_renderable(group: AgentGroupState):
|
|
102
|
+
"""Render one AgentRun as a collapsible group in the shared transcript."""
|
|
103
|
+
marker = "▶" if group.collapsed else "▼"
|
|
104
|
+
identity = f"{group.role}/{group.task_id}" if group.task_id else group.role
|
|
105
|
+
stats = f"工具 {group.tool_count} · Artifact {group.artifact_count}"
|
|
106
|
+
header = f"{marker} [{identity}] {group.task_title} · {_agent_group_status(group.status)} · {group.resource_scope_mode} · {stats}"
|
|
107
|
+
if group.effective_steps:
|
|
108
|
+
header += f" · 预算 {group.effective_steps} 步"
|
|
109
|
+
if group.attempt > 1:
|
|
110
|
+
header += f" · 第 {group.attempt} 次"
|
|
111
|
+
if group.failure_category:
|
|
112
|
+
header += f" · {group.failure_category}"
|
|
113
|
+
if group.repair_attempt:
|
|
114
|
+
header += f" · 修复 {group.repair_attempt}"
|
|
115
|
+
latest = group.latest_error or group.latest_summary
|
|
116
|
+
if group.collapsed and latest:
|
|
117
|
+
header += f"\n {latest.replace(chr(10), ' ')[:180]}"
|
|
118
|
+
parts: list[object] = [Text(header)]
|
|
119
|
+
if not group.collapsed:
|
|
120
|
+
if group.entries:
|
|
121
|
+
parts.append(Group(*(render_item(item) for item in group.entries)))
|
|
122
|
+
else:
|
|
123
|
+
parts.append(Text("(尚无事件)", style="dim"))
|
|
124
|
+
style = {
|
|
125
|
+
"running": "yellow",
|
|
126
|
+
"reviewing": "yellow",
|
|
127
|
+
"repairing": "magenta",
|
|
128
|
+
"done": "green",
|
|
129
|
+
"failed": "red",
|
|
130
|
+
"blocked": "yellow",
|
|
131
|
+
"skipped": "cyan",
|
|
132
|
+
"cancelled": "yellow",
|
|
133
|
+
}.get(group.status, "cyan")
|
|
134
|
+
return Panel(Group(*parts), title="Agent", border_style=style)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class DiagramCard:
|
|
138
|
+
"""Rich renderable for one Mermaid block inside the transcript."""
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
source: str,
|
|
143
|
+
model: FlowchartModel | None,
|
|
144
|
+
*,
|
|
145
|
+
error: str = "",
|
|
146
|
+
source_visible: bool = False,
|
|
147
|
+
) -> None:
|
|
148
|
+
self.source = source
|
|
149
|
+
self.model = model
|
|
150
|
+
self.error = error
|
|
151
|
+
self.source_visible = source_visible
|
|
152
|
+
|
|
153
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
154
|
+
available = max(1, options.max_width - 4)
|
|
155
|
+
if self.model is None:
|
|
156
|
+
body = f"Mermaid flowchart 暂无法渲染:{self.error}\n\n{_mermaid_source(self.source)}"
|
|
157
|
+
title = "Flowchart · parse failed"
|
|
158
|
+
yield Panel(Text(_truncate(body)), title=title, border_style="bright_blue")
|
|
159
|
+
return
|
|
160
|
+
else:
|
|
161
|
+
result = render_flowchart(self.model, width=available)
|
|
162
|
+
title = f"Flowchart · {self.model.direction} · {len(self.model.nodes)} nodes · {len(self.model.edges)} edges"
|
|
163
|
+
if result.mode != "unicode":
|
|
164
|
+
title += f" · {result.mode}"
|
|
165
|
+
parts: list[object] = [Text(result.text, no_wrap=True, overflow="crop")]
|
|
166
|
+
if result.warnings:
|
|
167
|
+
parts.append(Text("\n\n⚠ " + ";".join(result.warnings)))
|
|
168
|
+
if self.source_visible:
|
|
169
|
+
parts.append(Text("\n\n" + _mermaid_source(self.source)))
|
|
170
|
+
parts.append(Text("\n\n[d] 查看/隐藏 Mermaid 源码 · [r] 重新布局"))
|
|
171
|
+
yield Panel(Group(*parts), title=title, border_style="bright_blue")
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _assistant_renderable(item: TranscriptItem):
|
|
176
|
+
parts: list[object] = []
|
|
177
|
+
for text, block in split_mermaid_blocks(_truncate(item.text)):
|
|
178
|
+
if block is None:
|
|
179
|
+
if text:
|
|
180
|
+
try:
|
|
181
|
+
parts.append(Markdown(text))
|
|
182
|
+
except Exception:
|
|
183
|
+
parts.append(Text(text))
|
|
184
|
+
continue
|
|
185
|
+
try:
|
|
186
|
+
model = parse_flowchart(block.source)
|
|
187
|
+
parts.append(DiagramCard(block.source, model, source_visible=item.diagram_source_visible))
|
|
188
|
+
except FlowchartParseError as exc:
|
|
189
|
+
# An unsupported block remains visible as source; a malformed
|
|
190
|
+
# diagram must never prevent the rest of the assistant message.
|
|
191
|
+
parts.append(DiagramCard(block.source, None, error=str(exc), source_visible=True))
|
|
192
|
+
if not parts:
|
|
193
|
+
parts.append(Text("…"))
|
|
194
|
+
return Panel(Group(*parts), title="XG", border_style="green" if not item.streaming else "yellow")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def render_item(item: TranscriptItem):
|
|
198
|
+
if item.kind == "user":
|
|
199
|
+
return Panel(Text(item.text), title="你", border_style="cyan")
|
|
200
|
+
if item.kind == "progress":
|
|
201
|
+
return Panel(
|
|
202
|
+
Text(f"… {item.text}"),
|
|
203
|
+
title="XG · Working",
|
|
204
|
+
border_style="yellow",
|
|
205
|
+
)
|
|
206
|
+
if item.kind == "assistant":
|
|
207
|
+
return _assistant_renderable(item)
|
|
208
|
+
if item.kind in ("thinking", "tool_call", "tool_result", "approval") and item.collapsible:
|
|
209
|
+
return trace_renderable(item)
|
|
210
|
+
if item.kind == "tool_call":
|
|
211
|
+
try:
|
|
212
|
+
args = json.dumps(json.loads(item.tool_args), ensure_ascii=False, indent=2)
|
|
213
|
+
except (TypeError, ValueError):
|
|
214
|
+
args = item.tool_args
|
|
215
|
+
return Panel(Text(_truncate(args, 4_000)), title=f"工具调用 · {item.tool_name}", border_style="cyan")
|
|
216
|
+
if item.kind == "tool_result":
|
|
217
|
+
style = "green" if item.tool_ok else "red"
|
|
218
|
+
return Panel(Text(_truncate(item.text)), title=f"工具结果 · {item.tool_name}", border_style=style)
|
|
219
|
+
if item.kind == "approval":
|
|
220
|
+
return Panel(Text(item.text or "工具审批结果"), title=f"审批 · {item.tool_name}", border_style="yellow")
|
|
221
|
+
if item.kind == "context":
|
|
222
|
+
return Panel(Text(item.text), title="上下文", border_style="blue")
|
|
223
|
+
if item.kind == "help":
|
|
224
|
+
return Panel(Text(_truncate(item.text)), title="命令帮助", border_style="cyan")
|
|
225
|
+
if item.kind == "plan":
|
|
226
|
+
plan = item.plan
|
|
227
|
+
if plan is None:
|
|
228
|
+
return Panel(Text(item.text), title="计划", border_style="magenta")
|
|
229
|
+
if item.plan_review:
|
|
230
|
+
return PlanReviewCard(item)
|
|
231
|
+
lines = [f"目标:{plan.goal}", f"共 {len(plan.batches)} 轮"]
|
|
232
|
+
for batch_no, batch in enumerate(plan.batches, 1):
|
|
233
|
+
lines.append(f"第 {batch_no} 轮:{', '.join(batch)}")
|
|
234
|
+
for task_id in batch:
|
|
235
|
+
task = plan.task_by_id(task_id)
|
|
236
|
+
if task is None:
|
|
237
|
+
continue
|
|
238
|
+
lines.append(f" [{task.status}] {task.id} {task.title}")
|
|
239
|
+
if not item.collapsed:
|
|
240
|
+
deps = f"(依赖:{', '.join(task.deps)})" if task.deps else ""
|
|
241
|
+
lines.append(f" {task.description}{deps}")
|
|
242
|
+
mode = getattr(task, "resource_scope_mode", "")
|
|
243
|
+
if mode:
|
|
244
|
+
lines.append(f" 资源模式:{mode}")
|
|
245
|
+
for claim in getattr(task, "resource_claims", ()):
|
|
246
|
+
lines.append(f" 资源声明:{claim.access} {claim.pattern}")
|
|
247
|
+
lines.append("")
|
|
248
|
+
lines.append("Enter 执行 · d 展开/折叠详情 · r 重规划 · Esc 取消")
|
|
249
|
+
return Panel(Text("\n".join(lines)), title="计划审阅", border_style="magenta")
|
|
250
|
+
if item.kind == "error":
|
|
251
|
+
return Panel(Text(item.text), title="错误", border_style="red")
|
|
252
|
+
return Text(item.text, style="dim")
|
xg/tui/state.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Pure state types consumed by the Textual widgets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from xg.tui.i18n import UiLanguage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
TranscriptKind = Literal[
|
|
12
|
+
"user", "assistant", "thinking", "tool_call", "tool_result", "approval",
|
|
13
|
+
"context", "plan", "help", "error", "system", "progress", "agent_group",
|
|
14
|
+
]
|
|
15
|
+
TranscriptStatus = Literal[
|
|
16
|
+
"streaming", "running", "success", "failed", "cancelled", "done",
|
|
17
|
+
]
|
|
18
|
+
ProgressKind = Literal["response", "plan", "context"]
|
|
19
|
+
QueueItemKind = Literal["task", "plan", "command"]
|
|
20
|
+
InspectorView = Literal["session", "plan", "memory", "safety"]
|
|
21
|
+
TuiPhase = Literal[
|
|
22
|
+
"idle", "running", "awaiting_approval", "awaiting_plan_review",
|
|
23
|
+
"awaiting_team_input", "error",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class TranscriptItem:
|
|
29
|
+
id: str
|
|
30
|
+
kind: TranscriptKind
|
|
31
|
+
progress_kind: ProgressKind = "response"
|
|
32
|
+
text: str = ""
|
|
33
|
+
tool_name: str = ""
|
|
34
|
+
tool_args: str = ""
|
|
35
|
+
tool_call_id: str = ""
|
|
36
|
+
plan: Any | None = None
|
|
37
|
+
plan_review: bool = False
|
|
38
|
+
tool_ok: bool | None = None
|
|
39
|
+
collapsed: bool = True
|
|
40
|
+
collapsible: bool = False
|
|
41
|
+
user_collapsed: bool | None = None
|
|
42
|
+
diagram_source_visible: bool = False
|
|
43
|
+
streaming: bool = False
|
|
44
|
+
turn_id: str = ""
|
|
45
|
+
trace_id: str = ""
|
|
46
|
+
parent_call_id: str = ""
|
|
47
|
+
status: TranscriptStatus = "done"
|
|
48
|
+
elapsed_ms: int | None = None
|
|
49
|
+
agent_group_id: str = ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class AgentGroupState:
|
|
54
|
+
"""One logical AgentRun rendered inside the shared Team transcript."""
|
|
55
|
+
|
|
56
|
+
group_id: str
|
|
57
|
+
team_id: str
|
|
58
|
+
agent_id: str
|
|
59
|
+
role: str
|
|
60
|
+
task_id: str
|
|
61
|
+
task_title: str
|
|
62
|
+
resource_scope_mode: str = "targeted"
|
|
63
|
+
status: str = "pending"
|
|
64
|
+
collapsed: bool = True
|
|
65
|
+
user_toggled: bool = False
|
|
66
|
+
event_count: int = 0
|
|
67
|
+
tool_count: int = 0
|
|
68
|
+
artifact_count: int = 0
|
|
69
|
+
repair_attempt: int = 0
|
|
70
|
+
attempt: int = 0
|
|
71
|
+
effective_steps: int = 0
|
|
72
|
+
failure_category: str = ""
|
|
73
|
+
latest_summary: str = ""
|
|
74
|
+
latest_error: str = ""
|
|
75
|
+
entries: list[TranscriptItem] = field(default_factory=list)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class QueueItem:
|
|
80
|
+
id: str
|
|
81
|
+
text: str
|
|
82
|
+
kind: QueueItemKind
|
|
83
|
+
status: Literal["queued"] = "queued"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class UsageSnapshot:
|
|
88
|
+
"""Current context estimate plus provider request accounting."""
|
|
89
|
+
|
|
90
|
+
estimated_prompt_tokens: int = 0
|
|
91
|
+
context_window: int = 0
|
|
92
|
+
request_token_limit: int = 0
|
|
93
|
+
window_ratio: float = 0.0
|
|
94
|
+
budget_usage_ratio: float = 0.0
|
|
95
|
+
last_prompt_tokens: int = 0
|
|
96
|
+
last_completion_tokens: int = 0
|
|
97
|
+
last_total_tokens: int = 0
|
|
98
|
+
session_prompt_tokens: int = 0
|
|
99
|
+
session_completion_tokens: int = 0
|
|
100
|
+
session_total_tokens: int = 0
|
|
101
|
+
usage_source: Literal["provider", "estimated", "unavailable"] = "unavailable"
|
|
102
|
+
compaction_count: int = 0
|
|
103
|
+
last_compaction_before: int = 0
|
|
104
|
+
last_compaction_after: int = 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class SessionInspectorSnapshot:
|
|
109
|
+
provider: str = ""
|
|
110
|
+
model: str = ""
|
|
111
|
+
status: str = "idle"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class PlanTaskSnapshot:
|
|
116
|
+
id: str
|
|
117
|
+
title: str
|
|
118
|
+
status: str = "pending"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class PlanInspectorSnapshot:
|
|
123
|
+
goal: str = ""
|
|
124
|
+
status: str = "idle"
|
|
125
|
+
current_round: int = 0
|
|
126
|
+
total_rounds: int = 0
|
|
127
|
+
completed_tasks: int = 0
|
|
128
|
+
total_tasks: int = 0
|
|
129
|
+
failure_count: int = 0
|
|
130
|
+
tasks: tuple[PlanTaskSnapshot, ...] = ()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class MemoryInspectorSnapshot:
|
|
135
|
+
project_root: str = ""
|
|
136
|
+
xg_loaded: bool = False
|
|
137
|
+
xg_local_loaded: bool = False
|
|
138
|
+
warning_count: int = 0
|
|
139
|
+
memory_count: int = 0
|
|
140
|
+
store_available: bool = True
|
|
141
|
+
last_operation: str = ""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True)
|
|
145
|
+
class SafetyInspectorSnapshot:
|
|
146
|
+
hitl_enabled: bool = True
|
|
147
|
+
session_allow_all: bool = False
|
|
148
|
+
approval_status: str = "idle"
|
|
149
|
+
current_tool: str = ""
|
|
150
|
+
current_level: str = ""
|
|
151
|
+
last_decision: str = ""
|
|
152
|
+
last_reason: str = ""
|
|
153
|
+
path_guard_active: bool = True
|
|
154
|
+
command_guard_active: bool = True
|
|
155
|
+
last_rejection: str = ""
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True)
|
|
159
|
+
class SmartRouterTierSnapshot:
|
|
160
|
+
"""One tier entry rendered on the Header routing row."""
|
|
161
|
+
|
|
162
|
+
tier: str
|
|
163
|
+
provider: str
|
|
164
|
+
model: str
|
|
165
|
+
is_active: bool = False
|
|
166
|
+
configured: bool = False
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass(frozen=True)
|
|
170
|
+
class SmartRouterSnapshot:
|
|
171
|
+
"""SmartRouter routing state consumed by HeaderBar (phase-02).
|
|
172
|
+
|
|
173
|
+
Off 态 Header 渲染不读取该快照,因此默认值不影响既有渲染路径。
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
enabled: bool = False
|
|
177
|
+
tiers: tuple[SmartRouterTierSnapshot, ...] = ()
|
|
178
|
+
active_tier: str = ""
|
|
179
|
+
# 第 3 期校准后的置信度展示位,本期恒为 None。
|
|
180
|
+
confidence: float | None = None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass
|
|
184
|
+
class InspectorState:
|
|
185
|
+
active_view: InspectorView = "session"
|
|
186
|
+
session: SessionInspectorSnapshot = field(default_factory=SessionInspectorSnapshot)
|
|
187
|
+
plan: PlanInspectorSnapshot = field(default_factory=PlanInspectorSnapshot)
|
|
188
|
+
memory: MemoryInspectorSnapshot = field(default_factory=MemoryInspectorSnapshot)
|
|
189
|
+
safety: SafetyInspectorSnapshot = field(default_factory=SafetyInspectorSnapshot)
|
|
190
|
+
smart_router: SmartRouterSnapshot = field(default_factory=SmartRouterSnapshot)
|
|
191
|
+
provider: str = ""
|
|
192
|
+
model: str = ""
|
|
193
|
+
usage: UsageSnapshot = field(default_factory=UsageSnapshot)
|
|
194
|
+
# Compatibility fields retained for adapters written before UsageSnapshot.
|
|
195
|
+
context_tokens: int = 0
|
|
196
|
+
context_window: int = 0
|
|
197
|
+
memory_count: int = 0
|
|
198
|
+
hitl_enabled: bool = True
|
|
199
|
+
plan_status: str = "idle"
|
|
200
|
+
batch: str = ""
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass
|
|
204
|
+
class TuiState:
|
|
205
|
+
phase: TuiPhase = "idle"
|
|
206
|
+
transcript: list[TranscriptItem] = field(default_factory=list)
|
|
207
|
+
inspector: InspectorState = field(default_factory=InspectorState)
|
|
208
|
+
active_turn_id: str = ""
|
|
209
|
+
active_input: str = ""
|
|
210
|
+
pending_approval: Any | None = None
|
|
211
|
+
pending_plan: Any | None = None
|
|
212
|
+
pending_confirmation: ConfirmationRequest | None = None
|
|
213
|
+
team_input_task_id: str = ""
|
|
214
|
+
team_input_category: str = ""
|
|
215
|
+
team_input_message: str = ""
|
|
216
|
+
team_input_scope: list[Any] = field(default_factory=list)
|
|
217
|
+
notification: str = ""
|
|
218
|
+
notification_level: Literal["info", "warning", "error"] = "info"
|
|
219
|
+
plan_tasks: dict[str, str] = field(default_factory=dict)
|
|
220
|
+
agent_groups: dict[str, AgentGroupState] = field(default_factory=dict)
|
|
221
|
+
agent_group_order: list[str] = field(default_factory=list)
|
|
222
|
+
queue: list[QueueItem] = field(default_factory=list)
|
|
223
|
+
# UI preference only; never serialized into Agent messages or memory.
|
|
224
|
+
ui_language: UiLanguage = "en"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(frozen=True)
|
|
228
|
+
class ApprovalRequest:
|
|
229
|
+
tool_name: str
|
|
230
|
+
level: str
|
|
231
|
+
args: dict
|
|
232
|
+
turn_id: str = ""
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@dataclass(frozen=True)
|
|
236
|
+
class ConfirmationRequest:
|
|
237
|
+
kind: str
|
|
238
|
+
title: str
|
|
239
|
+
body: str
|
|
240
|
+
payload: object | None = None
|
xg/tui/theme.tcss
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Screen {
|
|
2
|
+
background: $surface;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
#shell {
|
|
6
|
+
width: 1fr;
|
|
7
|
+
height: 1fr;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
#main-column {
|
|
11
|
+
width: 1fr;
|
|
12
|
+
height: 1fr;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#header {
|
|
16
|
+
height: 2fr;
|
|
17
|
+
min-height: 5;
|
|
18
|
+
max-height: 20;
|
|
19
|
+
color: $text;
|
|
20
|
+
background: $panel;
|
|
21
|
+
padding: 0 2;
|
|
22
|
+
content-align: center middle;
|
|
23
|
+
text-align: center;
|
|
24
|
+
text-style: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#header.usage-warning {
|
|
28
|
+
color: $warning;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#header.usage-error {
|
|
32
|
+
color: $error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#transcript {
|
|
36
|
+
width: 1fr;
|
|
37
|
+
height: 8fr;
|
|
38
|
+
min-height: 8;
|
|
39
|
+
padding: 1 2;
|
|
40
|
+
background: $surface;
|
|
41
|
+
scrollbar-size: 1 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.transcript-empty-state {
|
|
45
|
+
width: 1fr;
|
|
46
|
+
height: auto;
|
|
47
|
+
margin-top: 6;
|
|
48
|
+
padding: 1 2;
|
|
49
|
+
color: $text-muted;
|
|
50
|
+
text-align: center;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.inline-action-card {
|
|
54
|
+
width: 1fr;
|
|
55
|
+
height: auto;
|
|
56
|
+
margin: 1 0 1 0;
|
|
57
|
+
padding: 1 2;
|
|
58
|
+
background: $panel;
|
|
59
|
+
border: round $warning;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.inline-approval-card {
|
|
63
|
+
border: round $error;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.confirmation-memory_clear {
|
|
67
|
+
border: round $accent;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.inline-action-card > Static {
|
|
71
|
+
width: 1fr;
|
|
72
|
+
height: auto;
|
|
73
|
+
margin-bottom: 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#inspector {
|
|
77
|
+
width: 36;
|
|
78
|
+
min-width: 32;
|
|
79
|
+
height: 1fr;
|
|
80
|
+
border-left: solid $primary;
|
|
81
|
+
padding: 1;
|
|
82
|
+
color: $text-muted;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#inspector-tabs {
|
|
86
|
+
height: 2;
|
|
87
|
+
width: 1fr;
|
|
88
|
+
color: $text-muted;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
InspectorTab {
|
|
92
|
+
width: 1fr;
|
|
93
|
+
height: 2;
|
|
94
|
+
content-align: center middle;
|
|
95
|
+
padding: 0;
|
|
96
|
+
text-wrap: nowrap;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
InspectorTab:hover {
|
|
101
|
+
color: $text;
|
|
102
|
+
background: $panel;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
InspectorTab.active {
|
|
106
|
+
color: $accent;
|
|
107
|
+
text-style: bold;
|
|
108
|
+
border-bottom: heavy $accent;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#inspector-content {
|
|
112
|
+
height: 1fr;
|
|
113
|
+
overflow-y: auto;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#inspector-content > Static {
|
|
117
|
+
width: 1fr;
|
|
118
|
+
height: auto;
|
|
119
|
+
color: $text;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#notification {
|
|
123
|
+
height: auto;
|
|
124
|
+
max-height: 2;
|
|
125
|
+
padding: 0 1;
|
|
126
|
+
color: $warning;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#composer-area {
|
|
130
|
+
width: 1fr;
|
|
131
|
+
height: auto;
|
|
132
|
+
margin: 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#queue-status {
|
|
136
|
+
height: auto;
|
|
137
|
+
max-height: 5;
|
|
138
|
+
padding: 0 1;
|
|
139
|
+
color: $text-muted;
|
|
140
|
+
background: $panel;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#command-suggestions {
|
|
144
|
+
width: 1fr;
|
|
145
|
+
height: auto;
|
|
146
|
+
max-height: 8;
|
|
147
|
+
padding: 0 1;
|
|
148
|
+
color: $text-muted;
|
|
149
|
+
background: $panel;
|
|
150
|
+
border: round $primary;
|
|
151
|
+
scrollbar-size: 1 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#command-suggestions > .option-list--option-highlighted {
|
|
155
|
+
color: $text;
|
|
156
|
+
background: $accent;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#composer-label {
|
|
160
|
+
height: 1;
|
|
161
|
+
padding: 0 1;
|
|
162
|
+
color: $text;
|
|
163
|
+
background: $panel;
|
|
164
|
+
text-style: bold;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#composer {
|
|
168
|
+
height: 3;
|
|
169
|
+
margin: 0;
|
|
170
|
+
border: round $primary;
|
|
171
|
+
background: $surface;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#composer:focus {
|
|
175
|
+
border: double $accent;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#footer {
|
|
179
|
+
width: 1fr;
|
|
180
|
+
height: 1;
|
|
181
|
+
color: $text-muted;
|
|
182
|
+
background: $panel;
|
|
183
|
+
padding: 0 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
ApprovalModal, PlanModal {
|
|
187
|
+
align: center middle;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#approval-dialog {
|
|
191
|
+
width: 70;
|
|
192
|
+
max-width: 90%;
|
|
193
|
+
height: auto;
|
|
194
|
+
max-height: 80%;
|
|
195
|
+
padding: 1 2;
|
|
196
|
+
background: $panel;
|
|
197
|
+
border: thick $warning;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#confirm-dialog {
|
|
201
|
+
width: 70;
|
|
202
|
+
max-width: 90%;
|
|
203
|
+
height: auto;
|
|
204
|
+
max-height: 80%;
|
|
205
|
+
padding: 1 2;
|
|
206
|
+
background: $panel;
|
|
207
|
+
border: thick $warning;
|
|
208
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Reusable fullscreen UI widgets."""
|