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.
- praisonai_code/__init__.py +17 -0
- praisonai_code/cli/__init__.py +12 -0
- praisonai_code/cli/_forward_shim.py +10 -0
- praisonai_code/cli/_paths.py +88 -0
- praisonai_code/cli/_warnings.py +58 -0
- praisonai_code/cli/app.py +757 -0
- praisonai_code/cli/approval_backend.py +272 -0
- praisonai_code/cli/branding.py +94 -0
- praisonai_code/cli/commands/__init__.py +114 -0
- praisonai_code/cli/commands/acp.py +80 -0
- praisonai_code/cli/commands/agent.py +116 -0
- praisonai_code/cli/commands/agents.py +80 -0
- praisonai_code/cli/commands/app.py +139 -0
- praisonai_code/cli/commands/attach.py +95 -0
- praisonai_code/cli/commands/audit.py +102 -0
- praisonai_code/cli/commands/auth.py +508 -0
- praisonai_code/cli/commands/batch.py +848 -0
- praisonai_code/cli/commands/benchmark.py +286 -0
- praisonai_code/cli/commands/browser.py +299 -0
- praisonai_code/cli/commands/call.py +45 -0
- praisonai_code/cli/commands/chat.py +332 -0
- praisonai_code/cli/commands/checkpoint.py +170 -0
- praisonai_code/cli/commands/code.py +276 -0
- praisonai_code/cli/commands/command.py +114 -0
- praisonai_code/cli/commands/commit.py +47 -0
- praisonai_code/cli/commands/completion.py +333 -0
- praisonai_code/cli/commands/config.py +681 -0
- praisonai_code/cli/commands/context.py +414 -0
- praisonai_code/cli/commands/daemon.py +203 -0
- praisonai_code/cli/commands/debug.py +142 -0
- praisonai_code/cli/commands/deploy.py +71 -0
- praisonai_code/cli/commands/diag.py +55 -0
- praisonai_code/cli/commands/docs.py +1575 -0
- praisonai_code/cli/commands/doctor.py +332 -0
- praisonai_code/cli/commands/endpoints.py +51 -0
- praisonai_code/cli/commands/environment.py +179 -0
- praisonai_code/cli/commands/eval.py +131 -0
- praisonai_code/cli/commands/examples.py +953 -0
- praisonai_code/cli/commands/flow.py +436 -0
- praisonai_code/cli/commands/github.py +752 -0
- praisonai_code/cli/commands/hooks.py +74 -0
- praisonai_code/cli/commands/init.py +174 -0
- praisonai_code/cli/commands/knowledge.py +440 -0
- praisonai_code/cli/commands/langextract.py +120 -0
- praisonai_code/cli/commands/langfuse.py +984 -0
- praisonai_code/cli/commands/loop.py +211 -0
- praisonai_code/cli/commands/lsp.py +112 -0
- praisonai_code/cli/commands/managed.py +659 -0
- praisonai_code/cli/commands/mcp.py +763 -0
- praisonai_code/cli/commands/memory.py +298 -0
- praisonai_code/cli/commands/models.py +264 -0
- praisonai_code/cli/commands/n8n.py +326 -0
- praisonai_code/cli/commands/obs.py +19 -0
- praisonai_code/cli/commands/package.py +76 -0
- praisonai_code/cli/commands/paths.py +106 -0
- praisonai_code/cli/commands/permissions.py +272 -0
- praisonai_code/cli/commands/plugins.py +609 -0
- praisonai_code/cli/commands/port.py +530 -0
- praisonai_code/cli/commands/profile.py +466 -0
- praisonai_code/cli/commands/publish.py +193 -0
- praisonai_code/cli/commands/rag.py +913 -0
- praisonai_code/cli/commands/realtime.py +52 -0
- praisonai_code/cli/commands/recipe.py +684 -0
- praisonai_code/cli/commands/registry.py +59 -0
- praisonai_code/cli/commands/replay.py +830 -0
- praisonai_code/cli/commands/research.py +49 -0
- praisonai_code/cli/commands/retrieval.py +377 -0
- praisonai_code/cli/commands/rules.py +71 -0
- praisonai_code/cli/commands/run.py +1573 -0
- praisonai_code/cli/commands/sandbox.py +371 -0
- praisonai_code/cli/commands/schedule.py +529 -0
- praisonai_code/cli/commands/serve.py +690 -0
- praisonai_code/cli/commands/session.py +450 -0
- praisonai_code/cli/commands/setup.py +174 -0
- praisonai_code/cli/commands/skills.py +545 -0
- praisonai_code/cli/commands/standardise.py +711 -0
- praisonai_code/cli/commands/templates.py +54 -0
- praisonai_code/cli/commands/test.py +558 -0
- praisonai_code/cli/commands/todo.py +74 -0
- praisonai_code/cli/commands/tools.py +205 -0
- praisonai_code/cli/commands/traces.py +145 -0
- praisonai_code/cli/commands/tracker.py +852 -0
- praisonai_code/cli/commands/train.py +613 -0
- praisonai_code/cli/commands/ui.py +172 -0
- praisonai_code/cli/commands/up.py +354 -0
- praisonai_code/cli/commands/validate.py +291 -0
- praisonai_code/cli/commands/version.py +101 -0
- praisonai_code/cli/commands/workflow.py +97 -0
- praisonai_code/cli/config_loader.py +437 -0
- praisonai_code/cli/configuration/__init__.py +27 -0
- praisonai_code/cli/configuration/config.schema.json +57 -0
- praisonai_code/cli/configuration/credentials.py +446 -0
- praisonai_code/cli/configuration/loader.py +364 -0
- praisonai_code/cli/configuration/model_resolver.py +161 -0
- praisonai_code/cli/configuration/oauth.py +389 -0
- praisonai_code/cli/configuration/paths.py +224 -0
- praisonai_code/cli/configuration/resolver.py +687 -0
- praisonai_code/cli/configuration/schema.py +317 -0
- praisonai_code/cli/execution/__init__.py +99 -0
- praisonai_code/cli/execution/core.py +208 -0
- praisonai_code/cli/execution/profiler.py +898 -0
- praisonai_code/cli/execution/request.py +85 -0
- praisonai_code/cli/execution/result.py +74 -0
- praisonai_code/cli/fallback_schema.py +416 -0
- praisonai_code/cli/features/__init__.py +278 -0
- praisonai_code/cli/features/_endpoint_registry.py +64 -0
- praisonai_code/cli/features/_search_registry.py +43 -0
- praisonai_code/cli/features/acp.py +236 -0
- praisonai_code/cli/features/action_orchestrator.py +576 -0
- praisonai_code/cli/features/agent_scheduler.py +773 -0
- praisonai_code/cli/features/agent_tools.py +603 -0
- praisonai_code/cli/features/agents.py +397 -0
- praisonai_code/cli/features/at_mentions.py +471 -0
- praisonai_code/cli/features/audit_cli.py +270 -0
- praisonai_code/cli/features/auto_memory.py +182 -0
- praisonai_code/cli/features/auto_mode.py +552 -0
- praisonai_code/cli/features/autonomy_mode.py +546 -0
- praisonai_code/cli/features/background.py +356 -0
- praisonai_code/cli/features/base.py +168 -0
- praisonai_code/cli/features/benchmark.py +1462 -0
- praisonai_code/cli/features/capabilities.py +1326 -0
- praisonai_code/cli/features/checkpoints.py +345 -0
- praisonai_code/cli/features/cli_profiler.py +335 -0
- praisonai_code/cli/features/code_intelligence.py +666 -0
- praisonai_code/cli/features/compaction.py +294 -0
- praisonai_code/cli/features/compare.py +534 -0
- praisonai_code/cli/features/config_hierarchy.py +366 -0
- praisonai_code/cli/features/context_manager.py +597 -0
- praisonai_code/cli/features/cost_tracker.py +514 -0
- praisonai_code/cli/features/csv_test_runner.py +736 -0
- praisonai_code/cli/features/custom_definitions.py +790 -0
- praisonai_code/cli/features/debug.py +810 -0
- praisonai_code/cli/features/deploy.py +605 -0
- praisonai_code/cli/features/diag.py +289 -0
- praisonai_code/cli/features/display_jsonl.py +173 -0
- praisonai_code/cli/features/doctor/__init__.py +63 -0
- praisonai_code/cli/features/doctor/checks/__init__.py +29 -0
- praisonai_code/cli/features/doctor/checks/acp_checks.py +220 -0
- praisonai_code/cli/features/doctor/checks/bot_checks.py +340 -0
- praisonai_code/cli/features/doctor/checks/config_checks.py +373 -0
- praisonai_code/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai_code/cli/features/doctor/checks/env_checks.py +637 -0
- praisonai_code/cli/features/doctor/checks/gateway_checks.py +387 -0
- praisonai_code/cli/features/doctor/checks/lsp_checks.py +231 -0
- praisonai_code/cli/features/doctor/checks/mcp_checks.py +367 -0
- praisonai_code/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai_code/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai_code/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai_code/cli/features/doctor/checks/packaging_checks.py +422 -0
- praisonai_code/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai_code/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai_code/cli/features/doctor/checks/runtime_checks.py +650 -0
- praisonai_code/cli/features/doctor/checks/runtime_migration_checks.py +220 -0
- praisonai_code/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai_code/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai_code/cli/features/doctor/checks/skills_checks.py +327 -0
- praisonai_code/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai_code/cli/features/doctor/engine.py +266 -0
- praisonai_code/cli/features/doctor/formatters.py +377 -0
- praisonai_code/cli/features/doctor/handler.py +564 -0
- praisonai_code/cli/features/doctor/models.py +276 -0
- praisonai_code/cli/features/doctor/registry.py +239 -0
- praisonai_code/cli/features/endpoints.py +1016 -0
- praisonai_code/cli/features/eval.py +559 -0
- praisonai_code/cli/features/examples.py +707 -0
- praisonai_code/cli/features/external_agents.py +231 -0
- praisonai_code/cli/features/fast_context.py +410 -0
- praisonai_code/cli/features/file_history.py +320 -0
- praisonai_code/cli/features/flow_display.py +566 -0
- praisonai_code/cli/features/git_attribution.py +159 -0
- praisonai_code/cli/features/git_integration.py +651 -0
- praisonai_code/cli/features/guardrail.py +171 -0
- praisonai_code/cli/features/handoff.py +252 -0
- praisonai_code/cli/features/hooks.py +583 -0
- praisonai_code/cli/features/hybrid_workflow.py +391 -0
- praisonai_code/cli/features/image.py +384 -0
- praisonai_code/cli/features/interactive_core_headless.py +450 -0
- praisonai_code/cli/features/interactive_runtime.py +600 -0
- praisonai_code/cli/features/interactive_test_harness.py +537 -0
- praisonai_code/cli/features/interactive_tools.py +428 -0
- praisonai_code/cli/features/interactive_tui.py +603 -0
- praisonai_code/cli/features/job_workflow.py +906 -0
- praisonai_code/cli/features/jobs.py +632 -0
- praisonai_code/cli/features/knowledge.py +531 -0
- praisonai_code/cli/features/knowledge_cli.py +438 -0
- praisonai_code/cli/features/lite.py +244 -0
- praisonai_code/cli/features/logs.py +200 -0
- praisonai_code/cli/features/lsp_cli.py +225 -0
- praisonai_code/cli/features/lsp_diagnostics.py +185 -0
- praisonai_code/cli/features/mcp.py +344 -0
- praisonai_code/cli/features/message_queue.py +587 -0
- praisonai_code/cli/features/metrics.py +210 -0
- praisonai_code/cli/features/migrate.py +1329 -0
- praisonai_code/cli/features/migration_flow.py +463 -0
- praisonai_code/cli/features/migration_spec.py +276 -0
- praisonai_code/cli/features/n8n.py +703 -0
- praisonai_code/cli/features/observability.py +293 -0
- praisonai_code/cli/features/ollama.py +361 -0
- praisonai_code/cli/features/output_modes.py +155 -0
- praisonai_code/cli/features/output_style.py +273 -0
- praisonai_code/cli/features/package.py +631 -0
- praisonai_code/cli/features/performance.py +308 -0
- praisonai_code/cli/features/persistence.py +636 -0
- praisonai_code/cli/features/profiler/__init__.py +81 -0
- praisonai_code/cli/features/profiler/core.py +558 -0
- praisonai_code/cli/features/profiler/optimizations.py +652 -0
- praisonai_code/cli/features/profiler/suite.py +386 -0
- praisonai_code/cli/features/queue/__init__.py +73 -0
- praisonai_code/cli/features/queue/manager.py +435 -0
- praisonai_code/cli/features/queue/models.py +289 -0
- praisonai_code/cli/features/queue/persistence.py +564 -0
- praisonai_code/cli/features/queue/scheduler.py +529 -0
- praisonai_code/cli/features/queue/worker.py +400 -0
- praisonai_code/cli/features/recipe.py +2187 -0
- praisonai_code/cli/features/recipe_creator.py +996 -0
- praisonai_code/cli/features/recipe_optimizer.py +1364 -0
- praisonai_code/cli/features/recipe_prompts.py +226 -0
- praisonai_code/cli/features/registry.py +229 -0
- praisonai_code/cli/features/repo_map.py +860 -0
- praisonai_code/cli/features/router.py +466 -0
- praisonai_code/cli/features/safe_shell.py +427 -0
- praisonai_code/cli/features/sandbox_cli.py +283 -0
- praisonai_code/cli/features/sandbox_executor.py +536 -0
- praisonai_code/cli/features/sdk_knowledge.py +500 -0
- praisonai_code/cli/features/session.py +222 -0
- praisonai_code/cli/features/session_checkpoints.py +208 -0
- praisonai_code/cli/features/setup/__init__.py +9 -0
- praisonai_code/cli/features/setup/handler.py +355 -0
- praisonai_code/cli/features/setup/templates.py +62 -0
- praisonai_code/cli/features/skills.py +940 -0
- praisonai_code/cli/features/slash_commands.py +692 -0
- praisonai_code/cli/features/telemetry.py +179 -0
- praisonai_code/cli/features/templates.py +1390 -0
- praisonai_code/cli/features/thinking.py +343 -0
- praisonai_code/cli/features/todo.py +334 -0
- praisonai_code/cli/features/tools.py +680 -0
- praisonai_code/cli/features/tui/__init__.py +83 -0
- praisonai_code/cli/features/tui/app.py +871 -0
- praisonai_code/cli/features/tui/cli.py +580 -0
- praisonai_code/cli/features/tui/config.py +150 -0
- praisonai_code/cli/features/tui/debug.py +526 -0
- praisonai_code/cli/features/tui/events.py +99 -0
- praisonai_code/cli/features/tui/mock_provider.py +328 -0
- praisonai_code/cli/features/tui/orchestrator.py +652 -0
- praisonai_code/cli/features/tui/screens/__init__.py +50 -0
- praisonai_code/cli/features/tui/screens/help.py +157 -0
- praisonai_code/cli/features/tui/screens/main.py +568 -0
- praisonai_code/cli/features/tui/screens/queue.py +174 -0
- praisonai_code/cli/features/tui/screens/session.py +124 -0
- praisonai_code/cli/features/tui/screens/settings.py +148 -0
- praisonai_code/cli/features/tui/session_store.py +198 -0
- praisonai_code/cli/features/tui/widgets/__init__.py +56 -0
- praisonai_code/cli/features/tui/widgets/chat.py +263 -0
- praisonai_code/cli/features/tui/widgets/command_popup.py +258 -0
- praisonai_code/cli/features/tui/widgets/composer.py +292 -0
- praisonai_code/cli/features/tui/widgets/file_popup.py +207 -0
- praisonai_code/cli/features/tui/widgets/queue_panel.py +223 -0
- praisonai_code/cli/features/tui/widgets/status.py +181 -0
- praisonai_code/cli/features/tui/widgets/tool_panel.py +307 -0
- praisonai_code/cli/features/wizard.py +289 -0
- praisonai_code/cli/features/workflow.py +802 -0
- praisonai_code/cli/features/yaml_utils.py +321 -0
- praisonai_code/cli/interactive/__init__.py +48 -0
- praisonai_code/cli/interactive/async_tui.py +1218 -0
- praisonai_code/cli/interactive/config.py +139 -0
- praisonai_code/cli/interactive/core.py +618 -0
- praisonai_code/cli/interactive/events.py +131 -0
- praisonai_code/cli/interactive/frontends/__init__.py +31 -0
- praisonai_code/cli/interactive/frontends/rich_frontend.py +462 -0
- praisonai_code/cli/interactive/frontends/textual_frontend.py +157 -0
- praisonai_code/cli/interactive/praison_io.py +502 -0
- praisonai_code/cli/interactive/repl.py +297 -0
- praisonai_code/cli/interactive/split_tui.py +456 -0
- praisonai_code/cli/interactive/tui_app.py +457 -0
- praisonai_code/cli/langfuse_client.py +360 -0
- praisonai_code/cli/main.py +7421 -0
- praisonai_code/cli/output/__init__.py +25 -0
- praisonai_code/cli/output/console.py +456 -0
- praisonai_code/cli/output/event_bridge.py +191 -0
- praisonai_code/cli/schedule_cli.py +54 -0
- praisonai_code/cli/schema_provider.py +23 -0
- praisonai_code/cli/session/__init__.py +16 -0
- praisonai_code/cli/session/resume.py +148 -0
- praisonai_code/cli/session/unified.py +548 -0
- praisonai_code/cli/state/__init__.py +31 -0
- praisonai_code/cli/state/identifiers.py +161 -0
- praisonai_code/cli/state/project_sessions.py +383 -0
- praisonai_code/cli/state/sessions.py +390 -0
- praisonai_code/cli/ui/__init__.py +160 -0
- praisonai_code/cli/ui/config.py +46 -0
- praisonai_code/cli/ui/events.py +61 -0
- praisonai_code/cli/ui/mg_backend.py +342 -0
- praisonai_code/cli/ui/plain.py +133 -0
- praisonai_code/cli/ui/rich_backend.py +162 -0
- praisonai_code/cli/unified_schema.py +655 -0
- praisonai_code/cli/utils/env_utils.py +126 -0
- praisonai_code/cli/utils/project.py +131 -0
- praisonai_code/cli_backends/__init__.py +73 -0
- praisonai_code/cli_backends/claude.py +373 -0
- praisonai_code/cli_backends/registry.py +113 -0
- praisonai_code/runtime/__init__.py +36 -0
- praisonai_code/runtime/__main__.py +81 -0
- praisonai_code/runtime/client.py +131 -0
- praisonai_code/runtime/descriptor.py +209 -0
- praisonai_code/runtime/server.py +356 -0
- praisonai_code-0.0.1.dist-info/METADATA +80 -0
- praisonai_code-0.0.1.dist-info/RECORD +309 -0
- praisonai_code-0.0.1.dist-info/WHEEL +5 -0
- 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()
|