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.
Files changed (130) hide show
  1. xg/__init__.py +3 -0
  2. xg/__main__.py +3 -0
  3. xg/adaptive/__init__.py +27 -0
  4. xg/adaptive/calibrate.py +191 -0
  5. xg/adaptive/feedback.py +170 -0
  6. xg/adaptive/learned_rules.py +284 -0
  7. xg/adaptive/signals.py +125 -0
  8. xg/adaptive/store.py +164 -0
  9. xg/agent/__init__.py +0 -0
  10. xg/agent/plan.py +631 -0
  11. xg/agent/react.py +268 -0
  12. xg/agent/team.py +1793 -0
  13. xg/assets/router.lgb +0 -0
  14. xg/assets/router_semantics.json +21278 -0
  15. xg/assets/router_semantics.onnx +0 -0
  16. xg/cli/__init__.py +0 -0
  17. xg/cli/app.py +1372 -0
  18. xg/cli/commands.py +921 -0
  19. xg/cli/completion.py +740 -0
  20. xg/cli/help.py +185 -0
  21. xg/cli/train.py +200 -0
  22. xg/config/__init__.py +0 -0
  23. xg/config/env_writer.py +121 -0
  24. xg/config/manager.py +446 -0
  25. xg/config/mcp.py +229 -0
  26. xg/config/provider_service.py +267 -0
  27. xg/config/providers.py +46 -0
  28. xg/config/settings.py +258 -0
  29. xg/config/skills.py +100 -0
  30. xg/config/smart_router_service.py +121 -0
  31. xg/config/web.py +140 -0
  32. xg/input_history/__init__.py +7 -0
  33. xg/input_history/models.py +28 -0
  34. xg/input_history/persistence.py +126 -0
  35. xg/input_history/policy.py +39 -0
  36. xg/input_history/prompt_toolkit.py +36 -0
  37. xg/input_history/store.py +118 -0
  38. xg/llm/__init__.py +0 -0
  39. xg/llm/client.py +49 -0
  40. xg/llm/factory.py +40 -0
  41. xg/llm/openai_compat.py +275 -0
  42. xg/llm/types.py +98 -0
  43. xg/mcp/__init__.py +4 -0
  44. xg/mcp/http.py +192 -0
  45. xg/mcp/manager.py +726 -0
  46. xg/mcp/models.py +86 -0
  47. xg/mcp/protocol.py +62 -0
  48. xg/mcp/resources.py +72 -0
  49. xg/mcp/schema.py +137 -0
  50. xg/mcp/stdio.py +210 -0
  51. xg/mcp/transport.py +66 -0
  52. xg/memory/__init__.py +15 -0
  53. xg/memory/context.py +327 -0
  54. xg/memory/manager.py +111 -0
  55. xg/memory/models.py +41 -0
  56. xg/memory/project.py +187 -0
  57. xg/memory/store.py +144 -0
  58. xg/router/__init__.py +124 -0
  59. xg/router/features.py +66 -0
  60. xg/router/keywords.py +50 -0
  61. xg/router/ml_router.py +178 -0
  62. xg/router/model_tiers.py +73 -0
  63. xg/router/postprocess.py +167 -0
  64. xg/router/rule_router.py +77 -0
  65. xg/router/semantic.py +138 -0
  66. xg/safety/__init__.py +0 -0
  67. xg/safety/audit.py +96 -0
  68. xg/safety/guards.py +106 -0
  69. xg/safety/hitl.py +73 -0
  70. xg/skill/__init__.py +9 -0
  71. xg/skill/errors.py +45 -0
  72. xg/skill/loader.py +42 -0
  73. xg/skill/models.py +57 -0
  74. xg/skill/parser.py +93 -0
  75. xg/skill/policy.py +40 -0
  76. xg/skill/prompt.py +45 -0
  77. xg/skill/registry.py +169 -0
  78. xg/tool/__init__.py +0 -0
  79. xg/tool/builtin.py +356 -0
  80. xg/tool/registry.py +228 -0
  81. xg/tui/__init__.py +34 -0
  82. xg/tui/app.py +612 -0
  83. xg/tui/controller.py +1296 -0
  84. xg/tui/diagrams/__init__.py +22 -0
  85. xg/tui/diagrams/layout.py +110 -0
  86. xg/tui/diagrams/markdown.py +39 -0
  87. xg/tui/diagrams/model.py +36 -0
  88. xg/tui/diagrams/parser.py +119 -0
  89. xg/tui/diagrams/renderer.py +551 -0
  90. xg/tui/i18n.py +169 -0
  91. xg/tui/messages.py +45 -0
  92. xg/tui/plan_renderables.py +147 -0
  93. xg/tui/reducer.py +1029 -0
  94. xg/tui/renderables.py +252 -0
  95. xg/tui/state.py +240 -0
  96. xg/tui/theme.tcss +208 -0
  97. xg/tui/widgets/__init__.py +1 -0
  98. xg/tui/widgets/action_card.py +94 -0
  99. xg/tui/widgets/agent_group_card.py +39 -0
  100. xg/tui/widgets/approval_modal.py +59 -0
  101. xg/tui/widgets/collapsible_card.py +40 -0
  102. xg/tui/widgets/command_suggestions.py +128 -0
  103. xg/tui/widgets/composer.py +151 -0
  104. xg/tui/widgets/config_panel.py +198 -0
  105. xg/tui/widgets/confirm_modal.py +31 -0
  106. xg/tui/widgets/footer.py +9 -0
  107. xg/tui/widgets/header.py +118 -0
  108. xg/tui/widgets/inspector.py +378 -0
  109. xg/tui/widgets/plan_modal.py +53 -0
  110. xg/tui/widgets/provider_form.py +141 -0
  111. xg/tui/widgets/queue_status.py +30 -0
  112. xg/tui/widgets/smart_router_form.py +95 -0
  113. xg/tui/widgets/transcript.py +354 -0
  114. xg/tui/workers.py +14 -0
  115. xg/web/__init__.py +21 -0
  116. xg/web/errors.py +57 -0
  117. xg/web/extract.py +127 -0
  118. xg/web/fetch.py +106 -0
  119. xg/web/markdown.py +77 -0
  120. xg/web/models.py +91 -0
  121. xg/web/providers.py +79 -0
  122. xg/web/search.py +118 -0
  123. xg/web/searxng.py +27 -0
  124. xg/web/serpapi.py +29 -0
  125. xg/web/url_policy.py +110 -0
  126. xg/web/zhipu.py +27 -0
  127. xg_cli-1.0.dist-info/METADATA +284 -0
  128. xg_cli-1.0.dist-info/RECORD +130 -0
  129. xg_cli-1.0.dist-info/WHEEL +4 -0
  130. 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."""