praisonai-code 0.0.1__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 (309) hide show
  1. praisonai_code/__init__.py +17 -0
  2. praisonai_code/cli/__init__.py +12 -0
  3. praisonai_code/cli/_forward_shim.py +10 -0
  4. praisonai_code/cli/_paths.py +88 -0
  5. praisonai_code/cli/_warnings.py +58 -0
  6. praisonai_code/cli/app.py +757 -0
  7. praisonai_code/cli/approval_backend.py +272 -0
  8. praisonai_code/cli/branding.py +94 -0
  9. praisonai_code/cli/commands/__init__.py +114 -0
  10. praisonai_code/cli/commands/acp.py +80 -0
  11. praisonai_code/cli/commands/agent.py +116 -0
  12. praisonai_code/cli/commands/agents.py +80 -0
  13. praisonai_code/cli/commands/app.py +139 -0
  14. praisonai_code/cli/commands/attach.py +95 -0
  15. praisonai_code/cli/commands/audit.py +102 -0
  16. praisonai_code/cli/commands/auth.py +508 -0
  17. praisonai_code/cli/commands/batch.py +848 -0
  18. praisonai_code/cli/commands/benchmark.py +286 -0
  19. praisonai_code/cli/commands/browser.py +299 -0
  20. praisonai_code/cli/commands/call.py +45 -0
  21. praisonai_code/cli/commands/chat.py +332 -0
  22. praisonai_code/cli/commands/checkpoint.py +170 -0
  23. praisonai_code/cli/commands/code.py +276 -0
  24. praisonai_code/cli/commands/command.py +114 -0
  25. praisonai_code/cli/commands/commit.py +47 -0
  26. praisonai_code/cli/commands/completion.py +333 -0
  27. praisonai_code/cli/commands/config.py +681 -0
  28. praisonai_code/cli/commands/context.py +414 -0
  29. praisonai_code/cli/commands/daemon.py +203 -0
  30. praisonai_code/cli/commands/debug.py +142 -0
  31. praisonai_code/cli/commands/deploy.py +71 -0
  32. praisonai_code/cli/commands/diag.py +55 -0
  33. praisonai_code/cli/commands/docs.py +1575 -0
  34. praisonai_code/cli/commands/doctor.py +332 -0
  35. praisonai_code/cli/commands/endpoints.py +51 -0
  36. praisonai_code/cli/commands/environment.py +179 -0
  37. praisonai_code/cli/commands/eval.py +131 -0
  38. praisonai_code/cli/commands/examples.py +953 -0
  39. praisonai_code/cli/commands/flow.py +436 -0
  40. praisonai_code/cli/commands/github.py +752 -0
  41. praisonai_code/cli/commands/hooks.py +74 -0
  42. praisonai_code/cli/commands/init.py +174 -0
  43. praisonai_code/cli/commands/knowledge.py +440 -0
  44. praisonai_code/cli/commands/langextract.py +120 -0
  45. praisonai_code/cli/commands/langfuse.py +984 -0
  46. praisonai_code/cli/commands/loop.py +211 -0
  47. praisonai_code/cli/commands/lsp.py +112 -0
  48. praisonai_code/cli/commands/managed.py +659 -0
  49. praisonai_code/cli/commands/mcp.py +763 -0
  50. praisonai_code/cli/commands/memory.py +298 -0
  51. praisonai_code/cli/commands/models.py +264 -0
  52. praisonai_code/cli/commands/n8n.py +326 -0
  53. praisonai_code/cli/commands/obs.py +19 -0
  54. praisonai_code/cli/commands/package.py +76 -0
  55. praisonai_code/cli/commands/paths.py +106 -0
  56. praisonai_code/cli/commands/permissions.py +272 -0
  57. praisonai_code/cli/commands/plugins.py +609 -0
  58. praisonai_code/cli/commands/port.py +530 -0
  59. praisonai_code/cli/commands/profile.py +466 -0
  60. praisonai_code/cli/commands/publish.py +193 -0
  61. praisonai_code/cli/commands/rag.py +913 -0
  62. praisonai_code/cli/commands/realtime.py +52 -0
  63. praisonai_code/cli/commands/recipe.py +684 -0
  64. praisonai_code/cli/commands/registry.py +59 -0
  65. praisonai_code/cli/commands/replay.py +830 -0
  66. praisonai_code/cli/commands/research.py +49 -0
  67. praisonai_code/cli/commands/retrieval.py +377 -0
  68. praisonai_code/cli/commands/rules.py +71 -0
  69. praisonai_code/cli/commands/run.py +1573 -0
  70. praisonai_code/cli/commands/sandbox.py +371 -0
  71. praisonai_code/cli/commands/schedule.py +529 -0
  72. praisonai_code/cli/commands/serve.py +690 -0
  73. praisonai_code/cli/commands/session.py +450 -0
  74. praisonai_code/cli/commands/setup.py +174 -0
  75. praisonai_code/cli/commands/skills.py +545 -0
  76. praisonai_code/cli/commands/standardise.py +711 -0
  77. praisonai_code/cli/commands/templates.py +54 -0
  78. praisonai_code/cli/commands/test.py +558 -0
  79. praisonai_code/cli/commands/todo.py +74 -0
  80. praisonai_code/cli/commands/tools.py +205 -0
  81. praisonai_code/cli/commands/traces.py +145 -0
  82. praisonai_code/cli/commands/tracker.py +852 -0
  83. praisonai_code/cli/commands/train.py +613 -0
  84. praisonai_code/cli/commands/ui.py +172 -0
  85. praisonai_code/cli/commands/up.py +354 -0
  86. praisonai_code/cli/commands/validate.py +291 -0
  87. praisonai_code/cli/commands/version.py +101 -0
  88. praisonai_code/cli/commands/workflow.py +97 -0
  89. praisonai_code/cli/config_loader.py +437 -0
  90. praisonai_code/cli/configuration/__init__.py +27 -0
  91. praisonai_code/cli/configuration/config.schema.json +57 -0
  92. praisonai_code/cli/configuration/credentials.py +446 -0
  93. praisonai_code/cli/configuration/loader.py +364 -0
  94. praisonai_code/cli/configuration/model_resolver.py +161 -0
  95. praisonai_code/cli/configuration/oauth.py +389 -0
  96. praisonai_code/cli/configuration/paths.py +224 -0
  97. praisonai_code/cli/configuration/resolver.py +687 -0
  98. praisonai_code/cli/configuration/schema.py +317 -0
  99. praisonai_code/cli/execution/__init__.py +99 -0
  100. praisonai_code/cli/execution/core.py +208 -0
  101. praisonai_code/cli/execution/profiler.py +898 -0
  102. praisonai_code/cli/execution/request.py +85 -0
  103. praisonai_code/cli/execution/result.py +74 -0
  104. praisonai_code/cli/fallback_schema.py +416 -0
  105. praisonai_code/cli/features/__init__.py +278 -0
  106. praisonai_code/cli/features/_endpoint_registry.py +64 -0
  107. praisonai_code/cli/features/_search_registry.py +43 -0
  108. praisonai_code/cli/features/acp.py +236 -0
  109. praisonai_code/cli/features/action_orchestrator.py +576 -0
  110. praisonai_code/cli/features/agent_scheduler.py +773 -0
  111. praisonai_code/cli/features/agent_tools.py +603 -0
  112. praisonai_code/cli/features/agents.py +397 -0
  113. praisonai_code/cli/features/at_mentions.py +471 -0
  114. praisonai_code/cli/features/audit_cli.py +270 -0
  115. praisonai_code/cli/features/auto_memory.py +182 -0
  116. praisonai_code/cli/features/auto_mode.py +552 -0
  117. praisonai_code/cli/features/autonomy_mode.py +546 -0
  118. praisonai_code/cli/features/background.py +356 -0
  119. praisonai_code/cli/features/base.py +168 -0
  120. praisonai_code/cli/features/benchmark.py +1462 -0
  121. praisonai_code/cli/features/capabilities.py +1326 -0
  122. praisonai_code/cli/features/checkpoints.py +345 -0
  123. praisonai_code/cli/features/cli_profiler.py +335 -0
  124. praisonai_code/cli/features/code_intelligence.py +666 -0
  125. praisonai_code/cli/features/compaction.py +294 -0
  126. praisonai_code/cli/features/compare.py +534 -0
  127. praisonai_code/cli/features/config_hierarchy.py +366 -0
  128. praisonai_code/cli/features/context_manager.py +597 -0
  129. praisonai_code/cli/features/cost_tracker.py +514 -0
  130. praisonai_code/cli/features/csv_test_runner.py +736 -0
  131. praisonai_code/cli/features/custom_definitions.py +790 -0
  132. praisonai_code/cli/features/debug.py +810 -0
  133. praisonai_code/cli/features/deploy.py +605 -0
  134. praisonai_code/cli/features/diag.py +289 -0
  135. praisonai_code/cli/features/display_jsonl.py +173 -0
  136. praisonai_code/cli/features/doctor/__init__.py +63 -0
  137. praisonai_code/cli/features/doctor/checks/__init__.py +29 -0
  138. praisonai_code/cli/features/doctor/checks/acp_checks.py +220 -0
  139. praisonai_code/cli/features/doctor/checks/bot_checks.py +340 -0
  140. praisonai_code/cli/features/doctor/checks/config_checks.py +373 -0
  141. praisonai_code/cli/features/doctor/checks/db_checks.py +366 -0
  142. praisonai_code/cli/features/doctor/checks/env_checks.py +637 -0
  143. praisonai_code/cli/features/doctor/checks/gateway_checks.py +387 -0
  144. praisonai_code/cli/features/doctor/checks/lsp_checks.py +231 -0
  145. praisonai_code/cli/features/doctor/checks/mcp_checks.py +367 -0
  146. praisonai_code/cli/features/doctor/checks/memory_checks.py +268 -0
  147. praisonai_code/cli/features/doctor/checks/network_checks.py +251 -0
  148. praisonai_code/cli/features/doctor/checks/obs_checks.py +328 -0
  149. praisonai_code/cli/features/doctor/checks/packaging_checks.py +422 -0
  150. praisonai_code/cli/features/doctor/checks/performance_checks.py +235 -0
  151. praisonai_code/cli/features/doctor/checks/permissions_checks.py +259 -0
  152. praisonai_code/cli/features/doctor/checks/runtime_checks.py +650 -0
  153. praisonai_code/cli/features/doctor/checks/runtime_migration_checks.py +220 -0
  154. praisonai_code/cli/features/doctor/checks/selftest_checks.py +322 -0
  155. praisonai_code/cli/features/doctor/checks/serve_checks.py +426 -0
  156. praisonai_code/cli/features/doctor/checks/skills_checks.py +327 -0
  157. praisonai_code/cli/features/doctor/checks/tools_checks.py +371 -0
  158. praisonai_code/cli/features/doctor/engine.py +266 -0
  159. praisonai_code/cli/features/doctor/formatters.py +377 -0
  160. praisonai_code/cli/features/doctor/handler.py +564 -0
  161. praisonai_code/cli/features/doctor/models.py +276 -0
  162. praisonai_code/cli/features/doctor/registry.py +239 -0
  163. praisonai_code/cli/features/endpoints.py +1016 -0
  164. praisonai_code/cli/features/eval.py +559 -0
  165. praisonai_code/cli/features/examples.py +707 -0
  166. praisonai_code/cli/features/external_agents.py +231 -0
  167. praisonai_code/cli/features/fast_context.py +410 -0
  168. praisonai_code/cli/features/file_history.py +320 -0
  169. praisonai_code/cli/features/flow_display.py +566 -0
  170. praisonai_code/cli/features/git_attribution.py +159 -0
  171. praisonai_code/cli/features/git_integration.py +651 -0
  172. praisonai_code/cli/features/guardrail.py +171 -0
  173. praisonai_code/cli/features/handoff.py +252 -0
  174. praisonai_code/cli/features/hooks.py +583 -0
  175. praisonai_code/cli/features/hybrid_workflow.py +391 -0
  176. praisonai_code/cli/features/image.py +384 -0
  177. praisonai_code/cli/features/interactive_core_headless.py +450 -0
  178. praisonai_code/cli/features/interactive_runtime.py +600 -0
  179. praisonai_code/cli/features/interactive_test_harness.py +537 -0
  180. praisonai_code/cli/features/interactive_tools.py +428 -0
  181. praisonai_code/cli/features/interactive_tui.py +603 -0
  182. praisonai_code/cli/features/job_workflow.py +906 -0
  183. praisonai_code/cli/features/jobs.py +632 -0
  184. praisonai_code/cli/features/knowledge.py +531 -0
  185. praisonai_code/cli/features/knowledge_cli.py +438 -0
  186. praisonai_code/cli/features/lite.py +244 -0
  187. praisonai_code/cli/features/logs.py +200 -0
  188. praisonai_code/cli/features/lsp_cli.py +225 -0
  189. praisonai_code/cli/features/lsp_diagnostics.py +185 -0
  190. praisonai_code/cli/features/mcp.py +344 -0
  191. praisonai_code/cli/features/message_queue.py +587 -0
  192. praisonai_code/cli/features/metrics.py +210 -0
  193. praisonai_code/cli/features/migrate.py +1329 -0
  194. praisonai_code/cli/features/migration_flow.py +463 -0
  195. praisonai_code/cli/features/migration_spec.py +276 -0
  196. praisonai_code/cli/features/n8n.py +703 -0
  197. praisonai_code/cli/features/observability.py +293 -0
  198. praisonai_code/cli/features/ollama.py +361 -0
  199. praisonai_code/cli/features/output_modes.py +155 -0
  200. praisonai_code/cli/features/output_style.py +273 -0
  201. praisonai_code/cli/features/package.py +631 -0
  202. praisonai_code/cli/features/performance.py +308 -0
  203. praisonai_code/cli/features/persistence.py +636 -0
  204. praisonai_code/cli/features/profiler/__init__.py +81 -0
  205. praisonai_code/cli/features/profiler/core.py +558 -0
  206. praisonai_code/cli/features/profiler/optimizations.py +652 -0
  207. praisonai_code/cli/features/profiler/suite.py +386 -0
  208. praisonai_code/cli/features/queue/__init__.py +73 -0
  209. praisonai_code/cli/features/queue/manager.py +435 -0
  210. praisonai_code/cli/features/queue/models.py +289 -0
  211. praisonai_code/cli/features/queue/persistence.py +564 -0
  212. praisonai_code/cli/features/queue/scheduler.py +529 -0
  213. praisonai_code/cli/features/queue/worker.py +400 -0
  214. praisonai_code/cli/features/recipe.py +2187 -0
  215. praisonai_code/cli/features/recipe_creator.py +996 -0
  216. praisonai_code/cli/features/recipe_optimizer.py +1364 -0
  217. praisonai_code/cli/features/recipe_prompts.py +226 -0
  218. praisonai_code/cli/features/registry.py +229 -0
  219. praisonai_code/cli/features/repo_map.py +860 -0
  220. praisonai_code/cli/features/router.py +466 -0
  221. praisonai_code/cli/features/safe_shell.py +427 -0
  222. praisonai_code/cli/features/sandbox_cli.py +283 -0
  223. praisonai_code/cli/features/sandbox_executor.py +536 -0
  224. praisonai_code/cli/features/sdk_knowledge.py +500 -0
  225. praisonai_code/cli/features/session.py +222 -0
  226. praisonai_code/cli/features/session_checkpoints.py +208 -0
  227. praisonai_code/cli/features/setup/__init__.py +9 -0
  228. praisonai_code/cli/features/setup/handler.py +355 -0
  229. praisonai_code/cli/features/setup/templates.py +62 -0
  230. praisonai_code/cli/features/skills.py +940 -0
  231. praisonai_code/cli/features/slash_commands.py +692 -0
  232. praisonai_code/cli/features/telemetry.py +179 -0
  233. praisonai_code/cli/features/templates.py +1390 -0
  234. praisonai_code/cli/features/thinking.py +343 -0
  235. praisonai_code/cli/features/todo.py +334 -0
  236. praisonai_code/cli/features/tools.py +680 -0
  237. praisonai_code/cli/features/tui/__init__.py +83 -0
  238. praisonai_code/cli/features/tui/app.py +871 -0
  239. praisonai_code/cli/features/tui/cli.py +580 -0
  240. praisonai_code/cli/features/tui/config.py +150 -0
  241. praisonai_code/cli/features/tui/debug.py +526 -0
  242. praisonai_code/cli/features/tui/events.py +99 -0
  243. praisonai_code/cli/features/tui/mock_provider.py +328 -0
  244. praisonai_code/cli/features/tui/orchestrator.py +652 -0
  245. praisonai_code/cli/features/tui/screens/__init__.py +50 -0
  246. praisonai_code/cli/features/tui/screens/help.py +157 -0
  247. praisonai_code/cli/features/tui/screens/main.py +568 -0
  248. praisonai_code/cli/features/tui/screens/queue.py +174 -0
  249. praisonai_code/cli/features/tui/screens/session.py +124 -0
  250. praisonai_code/cli/features/tui/screens/settings.py +148 -0
  251. praisonai_code/cli/features/tui/session_store.py +198 -0
  252. praisonai_code/cli/features/tui/widgets/__init__.py +56 -0
  253. praisonai_code/cli/features/tui/widgets/chat.py +263 -0
  254. praisonai_code/cli/features/tui/widgets/command_popup.py +258 -0
  255. praisonai_code/cli/features/tui/widgets/composer.py +292 -0
  256. praisonai_code/cli/features/tui/widgets/file_popup.py +207 -0
  257. praisonai_code/cli/features/tui/widgets/queue_panel.py +223 -0
  258. praisonai_code/cli/features/tui/widgets/status.py +181 -0
  259. praisonai_code/cli/features/tui/widgets/tool_panel.py +307 -0
  260. praisonai_code/cli/features/wizard.py +289 -0
  261. praisonai_code/cli/features/workflow.py +802 -0
  262. praisonai_code/cli/features/yaml_utils.py +321 -0
  263. praisonai_code/cli/interactive/__init__.py +48 -0
  264. praisonai_code/cli/interactive/async_tui.py +1218 -0
  265. praisonai_code/cli/interactive/config.py +139 -0
  266. praisonai_code/cli/interactive/core.py +618 -0
  267. praisonai_code/cli/interactive/events.py +131 -0
  268. praisonai_code/cli/interactive/frontends/__init__.py +31 -0
  269. praisonai_code/cli/interactive/frontends/rich_frontend.py +462 -0
  270. praisonai_code/cli/interactive/frontends/textual_frontend.py +157 -0
  271. praisonai_code/cli/interactive/praison_io.py +502 -0
  272. praisonai_code/cli/interactive/repl.py +297 -0
  273. praisonai_code/cli/interactive/split_tui.py +456 -0
  274. praisonai_code/cli/interactive/tui_app.py +457 -0
  275. praisonai_code/cli/langfuse_client.py +360 -0
  276. praisonai_code/cli/main.py +7421 -0
  277. praisonai_code/cli/output/__init__.py +25 -0
  278. praisonai_code/cli/output/console.py +456 -0
  279. praisonai_code/cli/output/event_bridge.py +191 -0
  280. praisonai_code/cli/schedule_cli.py +54 -0
  281. praisonai_code/cli/schema_provider.py +23 -0
  282. praisonai_code/cli/session/__init__.py +16 -0
  283. praisonai_code/cli/session/resume.py +148 -0
  284. praisonai_code/cli/session/unified.py +548 -0
  285. praisonai_code/cli/state/__init__.py +31 -0
  286. praisonai_code/cli/state/identifiers.py +161 -0
  287. praisonai_code/cli/state/project_sessions.py +383 -0
  288. praisonai_code/cli/state/sessions.py +390 -0
  289. praisonai_code/cli/ui/__init__.py +160 -0
  290. praisonai_code/cli/ui/config.py +46 -0
  291. praisonai_code/cli/ui/events.py +61 -0
  292. praisonai_code/cli/ui/mg_backend.py +342 -0
  293. praisonai_code/cli/ui/plain.py +133 -0
  294. praisonai_code/cli/ui/rich_backend.py +162 -0
  295. praisonai_code/cli/unified_schema.py +655 -0
  296. praisonai_code/cli/utils/env_utils.py +126 -0
  297. praisonai_code/cli/utils/project.py +131 -0
  298. praisonai_code/cli_backends/__init__.py +73 -0
  299. praisonai_code/cli_backends/claude.py +373 -0
  300. praisonai_code/cli_backends/registry.py +113 -0
  301. praisonai_code/runtime/__init__.py +36 -0
  302. praisonai_code/runtime/__main__.py +81 -0
  303. praisonai_code/runtime/client.py +131 -0
  304. praisonai_code/runtime/descriptor.py +209 -0
  305. praisonai_code/runtime/server.py +356 -0
  306. praisonai_code-0.0.1.dist-info/METADATA +80 -0
  307. praisonai_code-0.0.1.dist-info/RECORD +309 -0
  308. praisonai_code-0.0.1.dist-info/WHEEL +5 -0
  309. praisonai_code-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,263 @@
1
+ """
2
+ Chat Widget for PraisonAI TUI.
3
+
4
+ Displays chat history with streaming support.
5
+ """
6
+
7
+ from typing import List, Optional
8
+ from dataclasses import dataclass, field
9
+ import time
10
+
11
+ try:
12
+ from textual.widget import Widget
13
+ from textual.widgets import Static
14
+ from textual.containers import VerticalScroll
15
+ from textual.message import Message
16
+ from rich.markdown import Markdown
17
+ from rich.panel import Panel
18
+ from rich.text import Text
19
+ TEXTUAL_AVAILABLE = True
20
+ except ImportError:
21
+ TEXTUAL_AVAILABLE = False
22
+ Widget = object
23
+ Message = object
24
+
25
+
26
+ @dataclass
27
+ class ChatMessage:
28
+ """A single chat message."""
29
+ role: str # "user", "assistant", "system", "tool"
30
+ content: str
31
+ timestamp: float = field(default_factory=time.time)
32
+ run_id: Optional[str] = None
33
+ agent_name: Optional[str] = None
34
+ is_streaming: bool = False
35
+
36
+ @property
37
+ def display_role(self) -> str:
38
+ """Get display name for role."""
39
+ if self.role == "user":
40
+ return "You"
41
+ elif self.role == "assistant":
42
+ return self.agent_name or "Assistant"
43
+ elif self.role == "system":
44
+ return "System"
45
+ elif self.role == "tool":
46
+ return "Tool"
47
+ return self.role.title()
48
+
49
+
50
+ if TEXTUAL_AVAILABLE:
51
+ class ChatWidget(VerticalScroll):
52
+ """
53
+ Widget for displaying chat history with scrollbar.
54
+
55
+ Uses VerticalScroll for proper scrollbar support.
56
+ Messages are mounted directly to this container.
57
+ """
58
+
59
+ DEFAULT_CSS = """
60
+ ChatWidget {
61
+ height: 1fr;
62
+ border: solid $primary;
63
+ background: $surface;
64
+ padding: 0 1;
65
+ overflow-y: auto;
66
+ }
67
+
68
+ ChatWidget .message-user {
69
+ background: $primary-darken-2;
70
+ margin: 1 0;
71
+ padding: 1;
72
+ }
73
+
74
+ ChatWidget .message-assistant {
75
+ background: $surface-darken-1;
76
+ margin: 1 0;
77
+ padding: 1;
78
+ }
79
+
80
+ ChatWidget .message-system {
81
+ background: $warning-darken-3;
82
+ margin: 1 0;
83
+ padding: 1;
84
+ color: $text-muted;
85
+ }
86
+
87
+ ChatWidget .message-streaming {
88
+ border: dashed $accent;
89
+ }
90
+ """
91
+
92
+ class MessageAdded(Message):
93
+ """Event when a message is added."""
94
+ def __init__(self, message: ChatMessage):
95
+ self.message = message
96
+ super().__init__()
97
+
98
+ class StreamingUpdate(Message):
99
+ """Event when streaming content updates."""
100
+ def __init__(self, run_id: str, content: str):
101
+ self.run_id = run_id
102
+ self.content = content
103
+ super().__init__()
104
+
105
+ def __init__(
106
+ self,
107
+ max_messages: int = 1000,
108
+ name: Optional[str] = None,
109
+ id: Optional[str] = None,
110
+ classes: Optional[str] = None,
111
+ ):
112
+ super().__init__(name=name, id=id, classes=classes)
113
+ self._messages: List[ChatMessage] = []
114
+ self._max_messages = max_messages
115
+ self._streaming_widgets: dict = {}
116
+
117
+ def compose(self):
118
+ """Compose the widget - no inner container needed."""
119
+ # Messages are mounted directly to this VerticalScroll
120
+ # Must yield from empty iterable (not return None)
121
+ yield from ()
122
+
123
+ async def add_message(self, message: ChatMessage) -> None:
124
+ """Add a message to the chat.
125
+
126
+ NEW BEHAVIOR: Messages render NEWEST at TOP.
127
+ This guarantees visibility without relying on scroll.
128
+ """
129
+ self._messages.append(message)
130
+
131
+ # Trim old messages if needed
132
+ if len(self._messages) > self._max_messages:
133
+ self._messages = self._messages[-self._max_messages:]
134
+
135
+ # Render message and scroll to show it
136
+ await self._render_message(message)
137
+
138
+ self.post_message(self.MessageAdded(message))
139
+
140
+ async def _render_message(self, message: ChatMessage) -> None:
141
+ """Render a message and scroll to show it."""
142
+ # Create role label
143
+ role_style = {
144
+ "user": "bold cyan",
145
+ "assistant": "bold green",
146
+ "system": "bold yellow",
147
+ "tool": "bold magenta",
148
+ }.get(message.role, "bold")
149
+
150
+ role_text = Text(f"{message.display_role}:", style=role_style)
151
+
152
+ # Create content
153
+ try:
154
+ content = Markdown(message.content) if message.content else Text("")
155
+ except Exception:
156
+ content = Text(message.content)
157
+
158
+ # Create panel
159
+ css_class = f"message-{message.role}"
160
+ if message.is_streaming:
161
+ css_class += " message-streaming"
162
+
163
+ widget_id = f"msg-{message.run_id or id(message)}"
164
+
165
+ panel = Static(
166
+ Panel(content, title=str(role_text), border_style=role_style),
167
+ id=widget_id,
168
+ classes=css_class,
169
+ )
170
+
171
+ # Mount directly to this VerticalScroll container
172
+ await self.mount(panel)
173
+
174
+ # Scroll to end to show new message
175
+ self.scroll_end(animate=True)
176
+
177
+ if message.is_streaming:
178
+ self._streaming_widgets[message.run_id] = widget_id
179
+
180
+ async def update_streaming(self, run_id: str, content: str) -> None:
181
+ """Update a streaming message."""
182
+ if run_id not in self._streaming_widgets:
183
+ return
184
+
185
+ widget_id = self._streaming_widgets[run_id]
186
+
187
+ try:
188
+ widget = self.query_one(f"#{widget_id}", Static)
189
+
190
+ # Find the message
191
+ for msg in self._messages:
192
+ if msg.run_id == run_id:
193
+ msg.content = content
194
+ break
195
+
196
+ # Update content
197
+ try:
198
+ rendered = Markdown(content + " ▌")
199
+ except Exception:
200
+ rendered = Text(content + " ▌")
201
+
202
+ widget.update(Panel(rendered, title="Assistant", border_style="bold green"))
203
+
204
+ # Scroll to end to keep streaming content visible
205
+ self.scroll_end(animate=True)
206
+
207
+ except Exception:
208
+ pass
209
+
210
+ async def complete_streaming(self, run_id: str, final_content: str) -> None:
211
+ """Complete a streaming message."""
212
+ if run_id not in self._streaming_widgets:
213
+ return
214
+
215
+ widget_id = self._streaming_widgets.pop(run_id)
216
+
217
+ try:
218
+ widget = self.query_one(f"#{widget_id}", Static)
219
+
220
+ # Update message
221
+ for msg in self._messages:
222
+ if msg.run_id == run_id:
223
+ msg.content = final_content
224
+ msg.is_streaming = False
225
+ break
226
+
227
+ # Update content without cursor
228
+ try:
229
+ rendered = Markdown(final_content)
230
+ except Exception:
231
+ rendered = Text(final_content)
232
+
233
+ widget.update(Panel(rendered, title="Assistant", border_style="bold green"))
234
+ widget.remove_class("message-streaming")
235
+
236
+ except Exception:
237
+ pass
238
+
239
+ async def clear(self) -> None:
240
+ """Clear all messages."""
241
+ self._messages.clear()
242
+ self._streaming_widgets.clear()
243
+
244
+ # Remove all children from this container
245
+ await self.remove_children()
246
+
247
+ @property
248
+ def messages(self) -> List[ChatMessage]:
249
+ """Get all messages."""
250
+ return self._messages.copy()
251
+
252
+ @property
253
+ def message_count(self) -> int:
254
+ """Get message count."""
255
+ return len(self._messages)
256
+
257
+ else:
258
+ class ChatWidget:
259
+ """Placeholder when Textual is not available."""
260
+ def __init__(self, *args, **kwargs):
261
+ raise ImportError(
262
+ "Textual is required for TUI. Install with: pip install praisonai[tui]"
263
+ )
@@ -0,0 +1,258 @@
1
+ """
2
+ Command Popup Widget for PraisonAI TUI.
3
+
4
+ Shows a searchable list of available commands when user types backslash.
5
+ Inspired by Claude Code's command discovery UX.
6
+ """
7
+
8
+ from typing import List, Optional, Callable
9
+ from dataclasses import dataclass
10
+
11
+ try:
12
+ from textual.widget import Widget
13
+ from textual.widgets import Static, Input, OptionList
14
+ from textual.widgets.option_list import Option
15
+ from textual.containers import Vertical, Container
16
+ from textual.reactive import reactive
17
+ from textual.message import Message
18
+ from textual import events
19
+ from rich.text import Text
20
+ TEXTUAL_AVAILABLE = True
21
+ except ImportError:
22
+ TEXTUAL_AVAILABLE = False
23
+ Widget = object
24
+ Message = object
25
+
26
+
27
+ @dataclass
28
+ class CommandInfo:
29
+ """Information about a command for display."""
30
+ name: str
31
+ description: str
32
+ aliases: List[str]
33
+ category: str = "general"
34
+
35
+ @property
36
+ def display_name(self) -> str:
37
+ """Get display name with aliases."""
38
+ if self.aliases:
39
+ return f"{self.name} ({', '.join(self.aliases)})"
40
+ return self.name
41
+
42
+
43
+ if TEXTUAL_AVAILABLE:
44
+ class CommandPopupWidget(Container):
45
+ """
46
+ Popup widget for command discovery and selection.
47
+
48
+ Features:
49
+ - Shows all available commands
50
+ - Searchable/filterable
51
+ - Keyboard navigation
52
+ - Category grouping
53
+ """
54
+
55
+ DEFAULT_CSS = """
56
+ CommandPopupWidget {
57
+ layer: popup;
58
+ width: 60;
59
+ height: auto;
60
+ max-height: 20;
61
+ background: $surface;
62
+ border: solid $primary;
63
+ padding: 1;
64
+ margin: 0 0 0 2;
65
+ }
66
+
67
+ CommandPopupWidget #popup-title {
68
+ height: 1;
69
+ background: $primary;
70
+ color: $text;
71
+ text-align: center;
72
+ margin-bottom: 1;
73
+ }
74
+
75
+ CommandPopupWidget #popup-search {
76
+ height: 3;
77
+ margin-bottom: 1;
78
+ }
79
+
80
+ CommandPopupWidget #popup-list {
81
+ height: auto;
82
+ max-height: 12;
83
+ background: $surface-darken-1;
84
+ }
85
+
86
+ CommandPopupWidget .command-item {
87
+ padding: 0 1;
88
+ }
89
+
90
+ CommandPopupWidget .command-item:hover {
91
+ background: $primary-darken-1;
92
+ }
93
+
94
+ CommandPopupWidget #popup-hint {
95
+ height: 1;
96
+ color: $text-muted;
97
+ text-align: center;
98
+ margin-top: 1;
99
+ }
100
+ """
101
+
102
+ class CommandSelected(Message):
103
+ """Event when a command is selected."""
104
+ def __init__(self, command: str, args: str = ""):
105
+ self.command = command
106
+ self.args = args
107
+ super().__init__()
108
+
109
+ class Dismissed(Message):
110
+ """Event when popup is dismissed."""
111
+ pass
112
+
113
+ # Reactive properties
114
+ filter_text: reactive[str] = reactive("")
115
+
116
+ def __init__(
117
+ self,
118
+ commands: Optional[List[CommandInfo]] = None,
119
+ name: Optional[str] = None,
120
+ id: Optional[str] = None,
121
+ classes: Optional[str] = None,
122
+ ):
123
+ super().__init__(name=name, id=id, classes=classes)
124
+ self._commands = commands or []
125
+ self._filtered_commands: List[CommandInfo] = []
126
+
127
+ def compose(self):
128
+ """Compose the widget."""
129
+ yield Static("Commands", id="popup-title")
130
+ yield Input(placeholder="Type to filter...", id="popup-search")
131
+ yield OptionList(id="popup-list")
132
+ yield Static("↑↓ Navigate • Enter Select • Esc Cancel", id="popup-hint")
133
+
134
+ def on_mount(self) -> None:
135
+ """Handle mount."""
136
+ self._update_list()
137
+ # Focus the search input
138
+ search = self.query_one("#popup-search", Input)
139
+ search.focus()
140
+
141
+ def set_commands(self, commands: List[CommandInfo]) -> None:
142
+ """Set the available commands."""
143
+ self._commands = commands
144
+ self._update_list()
145
+
146
+ def watch_filter_text(self, value: str) -> None:
147
+ """React to filter text changes."""
148
+ self._update_list()
149
+
150
+ def _update_list(self) -> None:
151
+ """Update the command list based on filter."""
152
+ filter_lower = self.filter_text.lower()
153
+
154
+ if filter_lower:
155
+ self._filtered_commands = [
156
+ cmd for cmd in self._commands
157
+ if (filter_lower in cmd.name.lower() or
158
+ filter_lower in cmd.description.lower() or
159
+ any(filter_lower in alias.lower() for alias in cmd.aliases))
160
+ ]
161
+ else:
162
+ self._filtered_commands = self._commands.copy()
163
+
164
+ # Update the option list
165
+ try:
166
+ option_list = self.query_one("#popup-list", OptionList)
167
+ option_list.clear_options()
168
+
169
+ for cmd in self._filtered_commands:
170
+ # Create rich text for the option
171
+ text = Text()
172
+ text.append(f"/{cmd.name}", style="bold cyan")
173
+ if cmd.aliases:
174
+ text.append(f" ({', '.join(cmd.aliases)})", style="dim")
175
+ text.append(f"\n {cmd.description}", style="")
176
+
177
+ option_list.add_option(Option(text, id=cmd.name))
178
+ except Exception:
179
+ pass
180
+
181
+ def on_input_changed(self, event: Input.Changed) -> None:
182
+ """Handle search input changes."""
183
+ if event.input.id == "popup-search":
184
+ self.filter_text = event.value
185
+
186
+ def on_input_submitted(self, event: Input.Submitted) -> None:
187
+ """Handle Enter in search input."""
188
+ if event.input.id == "popup-search":
189
+ # Select first matching command
190
+ if self._filtered_commands:
191
+ self._select_command(self._filtered_commands[0].name)
192
+
193
+ def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
194
+ """Handle command selection from list."""
195
+ if event.option.id:
196
+ self._select_command(str(event.option.id))
197
+
198
+ def on_key(self, event: events.Key) -> None:
199
+ """Handle key events."""
200
+ if event.key == "escape":
201
+ self.post_message(self.Dismissed())
202
+ event.stop()
203
+ elif event.key == "down":
204
+ # Move focus to list if in search
205
+ try:
206
+ option_list = self.query_one("#popup-list", OptionList)
207
+ option_list.focus()
208
+ except Exception:
209
+ pass
210
+ elif event.key == "up":
211
+ # Move focus to search if at top of list
212
+ try:
213
+ search = self.query_one("#popup-search", Input)
214
+ search.focus()
215
+ except Exception:
216
+ pass
217
+
218
+ def _select_command(self, command_name: str) -> None:
219
+ """Select a command and emit event."""
220
+ self.post_message(self.CommandSelected(command_name))
221
+
222
+ def focus_search(self) -> None:
223
+ """Focus the search input."""
224
+ try:
225
+ search = self.query_one("#popup-search", Input)
226
+ search.focus()
227
+ except Exception:
228
+ pass
229
+
230
+
231
+ else:
232
+ class CommandPopupWidget:
233
+ """Placeholder when Textual is not available."""
234
+ def __init__(self, *args, **kwargs):
235
+ raise ImportError(
236
+ "Textual is required for TUI. Install with: pip install praisonai[tui]"
237
+ )
238
+
239
+
240
+ # Default commands list (matches slash_commands.py registry)
241
+ DEFAULT_COMMANDS = [
242
+ CommandInfo("help", "Show help for commands", ["h", "?"]),
243
+ CommandInfo("clear", "Clear conversation history", ["reset"]),
244
+ CommandInfo("model", "Show or change the current model", ["m"]),
245
+ CommandInfo("cost", "Show session cost and token usage", ["usage", "stats"]),
246
+ CommandInfo("tokens", "Show token usage breakdown", []),
247
+ CommandInfo("queue", "Show queue status", ["q"]),
248
+ CommandInfo("cancel", "Cancel current operation", ["c"]),
249
+ CommandInfo("settings", "Show current settings", ["set"]),
250
+ CommandInfo("sessions", "Browse saved sessions", ["sess"]),
251
+ CommandInfo("tools", "Toggle tools panel", ["t"]),
252
+ CommandInfo("exit", "Exit the TUI", ["quit", "q"]),
253
+ ]
254
+
255
+
256
+ def get_default_commands() -> List[CommandInfo]:
257
+ """Get the default command list."""
258
+ return DEFAULT_COMMANDS.copy()