@scemoon/cdh 1.0.0
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.
- package/ai-dlc-skill/SKILL.md +75 -0
- package/ai-dlc-skill/agents/deliver-agent.md +24 -0
- package/ai-dlc-skill/agents/master.md +44 -0
- package/ai-dlc-skill/agents/plan-agent.md +23 -0
- package/ai-dlc-skill/agents/understand-agent.md +23 -0
- package/ai-dlc-skill/agents/verify-agent.md +27 -0
- package/ai-dlc-skill/architecture/project-structure.md +78 -0
- package/ai-dlc-skill/brownfield/README.md +30 -0
- package/ai-dlc-skill/brownfield/generate-context.sh +64 -0
- package/ai-dlc-skill/brownfield/scripts/deps.sh +27 -0
- package/ai-dlc-skill/brownfield/scripts/discover.sh +34 -0
- package/ai-dlc-skill/brownfield/scripts/extract-api.sh +45 -0
- package/ai-dlc-skill/components/backend.md +34 -0
- package/ai-dlc-skill/components/desktop.md +30 -0
- package/ai-dlc-skill/components/mya.md +29 -0
- package/ai-dlc-skill/components/native.md +30 -0
- package/ai-dlc-skill/components/tta.md +29 -0
- package/ai-dlc-skill/components/web.md +31 -0
- package/ai-dlc-skill/components/wxa.md +29 -0
- package/ai-dlc-skill/contracts/README.md +25 -0
- package/ai-dlc-skill/core/adaptive-flow.md +36 -0
- package/ai-dlc-skill/core/security.md +29 -0
- package/ai-dlc-skill/cross-tool/cline/export.sh +31 -0
- package/ai-dlc-skill/cross-tool/copilot/export.sh +47 -0
- package/ai-dlc-skill/cross-tool/cursor/export.sh +32 -0
- package/ai-dlc-skill/cross-tool/export.sh +22 -0
- package/ai-dlc-skill/cross-tool/onecode/config.yaml.tpl +17 -0
- package/ai-dlc-skill/cross-tool/onecode/export-skills.sh +31 -0
- package/ai-dlc-skill/cross-tool/onecode/export.sh +48 -0
- package/ai-dlc-skill/phases/deliver/entry.md +26 -0
- package/ai-dlc-skill/phases/deliver/lifecycle.md +185 -0
- package/ai-dlc-skill/phases/deliver/prompt.md +40 -0
- package/ai-dlc-skill/phases/deliver/rules.md +61 -0
- package/ai-dlc-skill/phases/plan/entry.md +25 -0
- package/ai-dlc-skill/phases/plan/lifecycle.md +204 -0
- package/ai-dlc-skill/phases/plan/prompt.md +38 -0
- package/ai-dlc-skill/phases/plan/rules.md +25 -0
- package/ai-dlc-skill/phases/understand/entry.md +26 -0
- package/ai-dlc-skill/phases/understand/lifecycle.md +184 -0
- package/ai-dlc-skill/phases/understand/prompt.md +41 -0
- package/ai-dlc-skill/phases/understand/rules.md +37 -0
- package/ai-dlc-skill/phases/verify/entry.md +26 -0
- package/ai-dlc-skill/phases/verify/lifecycle.md +192 -0
- package/ai-dlc-skill/phases/verify/prompt.md +43 -0
- package/ai-dlc-skill/phases/verify/rules.md +73 -0
- package/ai-dlc-skill/practices/bdd.md +91 -0
- package/ai-dlc-skill/practices/sdd.md +84 -0
- package/ai-dlc-skill/practices/tdd.md +95 -0
- package/ai-dlc-skill/providers/README.md +114 -0
- package/ai-dlc-skill/providers/aliyun/deployment.yaml +66 -0
- package/ai-dlc-skill/providers/aliyun/preview.yaml +84 -0
- package/ai-dlc-skill/providers/aliyun/provider.yaml +160 -0
- package/ai-dlc-skill/providers/tcb/deployment.yaml +69 -0
- package/ai-dlc-skill/providers/tcb/preview.yaml +83 -0
- package/ai-dlc-skill/providers/tcb/provider.yaml +172 -0
- package/ai-dlc-skill/requirements.md +140 -0
- package/ai-dlc-skill/skill.yaml +147 -0
- package/ai-dlc-skill/templates/integration/CHANGELOG.md +41 -0
- package/ai-dlc-skill/templates/integration/asyncapi.yaml +43 -0
- package/ai-dlc-skill/templates/integration/contract-diff.md +63 -0
- package/ai-dlc-skill/templates/integration/contract-spec.md +49 -0
- package/ai-dlc-skill/templates/integration/openapi.yaml +18 -0
- package/ai-dlc-skill/templates/integration/runtime-contract.yaml +36 -0
- package/ai-dlc-skill/templates/plan/design.md +49 -0
- package/ai-dlc-skill/templates/plan/task-list.md +32 -0
- package/ai-dlc-skill/templates/project/README.md +127 -0
- package/ai-dlc-skill/templates/project/template.md +119 -0
- package/ai-dlc-skill/templates/understand/feature.feature +23 -0
- package/ai-dlc-skill/templates/understand/intent.md +29 -0
- package/ai-dlc-skill/templates/understand/spec-delta.md +47 -0
- package/ai-dlc-skill/templates/verify/test-unit.py +27 -0
- package/ai-dlc-skill/walkthrough/collect.sh +50 -0
- package/ai-dlc-skill/walkthrough/template.md +54 -0
- package/ai-dlc-skill/workflows/ai-dlc.yaml +110 -0
- package/ai-dlc-skill/workflows/deployment.yaml +199 -0
- package/cdh/__init__.py +0 -0
- package/cdh/__main__.py +4 -0
- package/cdh/__pycache__/__init__.cpython-313.pyc +0 -0
- package/cdh/__pycache__/__init__.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
- package/cdh/__pycache__/scaffold.cpython-313.pyc +0 -0
- package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/cdh/cli.py +361 -0
- package/cdh/scaffold.py +312 -0
- package/cli.js +2 -0
- package/onecode/__init__.py +1 -0
- package/onecode/__main__.py +4 -0
- package/onecode/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
- package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
- package/onecode/__pycache__/config.cpython-314.pyc +0 -0
- package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
- package/onecode/__pycache__/mock_data.cpython-314.pyc +0 -0
- package/onecode/agent/__init__.py +0 -0
- package/onecode/agent/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/cdh_agent_acp.cpython-313.pyc +0 -0
- package/onecode/agent/__pycache__/cdh_agent_acp.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/context.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/harness_skill.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/hooks.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/permissions.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/permissions_store.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
- package/onecode/agent/agents/__init__.py +29 -0
- package/onecode/agent/agents/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
- package/onecode/agent/agents/types.py +662 -0
- package/onecode/agent/attachments.py +113 -0
- package/onecode/agent/cdh_loader.py +262 -0
- package/onecode/agent/context.py +338 -0
- package/onecode/agent/engine.py +1894 -0
- package/onecode/agent/hooks.py +86 -0
- package/onecode/agent/onecode_agent_acp.py +2203 -0
- package/onecode/agent/permissions.py +151 -0
- package/onecode/agent/permissions_store.py +62 -0
- package/onecode/agent/session.py +186 -0
- package/onecode/agent/snapshot.py +145 -0
- package/onecode/agent/tools/__init__.py +17 -0
- package/onecode/agent/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/apply_patch_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/bash_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/communication_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/config_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/cron_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/errors.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/file_ops.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/file_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/git_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/lsp_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/mcp_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/permission_handler.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/permissions.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/protocol.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/registry.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/sandbox.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/schema_validation.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/skill_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/task_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/web_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/agent_tools.py +134 -0
- package/onecode/agent/tools/apply_patch_tool.py +144 -0
- package/onecode/agent/tools/bash_tool.py +91 -0
- package/onecode/agent/tools/communication_tools.py +149 -0
- package/onecode/agent/tools/config_tool.py +130 -0
- package/onecode/agent/tools/cron_tools.py +179 -0
- package/onecode/agent/tools/errors.py +17 -0
- package/onecode/agent/tools/file_ops.py +187 -0
- package/onecode/agent/tools/file_tools.py +226 -0
- package/onecode/agent/tools/git_tools.py +88 -0
- package/onecode/agent/tools/lsp_tools.py +404 -0
- package/onecode/agent/tools/mcp_tools.py +129 -0
- package/onecode/agent/tools/permission_handler.py +46 -0
- package/onecode/agent/tools/permissions.py +77 -0
- package/onecode/agent/tools/protocol.py +20 -0
- package/onecode/agent/tools/registry.py +134 -0
- package/onecode/agent/tools/sandbox.py +264 -0
- package/onecode/agent/tools/schema_validation.py +100 -0
- package/onecode/agent/tools/skill_tools.py +52 -0
- package/onecode/agent/tools/task_tools.py +249 -0
- package/onecode/agent/tools/web_tools.py +170 -0
- package/onecode/builtin_skills/git/SKILL.md +64 -0
- package/onecode/builtin_skills/git/skill.yaml +7 -0
- package/onecode/builtin_skills/shell/SKILL.md +58 -0
- package/onecode/builtin_skills/shell/skill.yaml +8 -0
- package/onecode/cli.py +755 -0
- package/onecode/cloud/__init__.py +0 -0
- package/onecode/cloud/base.py +20 -0
- package/onecode/codebase/__init__.py +69 -0
- package/onecode/codebase/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/chunker.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/config.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/indexer.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/retriever.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/storage.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/tools.cpython-314.pyc +0 -0
- package/onecode/codebase/chunker.py +130 -0
- package/onecode/codebase/indexer.py +136 -0
- package/onecode/codebase/retriever.py +192 -0
- package/onecode/codebase/storage.py +130 -0
- package/onecode/codebase/tools.py +59 -0
- package/onecode/commands.py +276 -0
- package/onecode/config.py +285 -0
- package/onecode/config_screen.py +594 -0
- package/onecode/cron/__init__.py +3 -0
- package/onecode/lsp/__init__.py +3 -0
- package/onecode/mcp/__init__.py +4 -0
- package/onecode/mcp/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/mcp/__pycache__/client.cpython-314.pyc +0 -0
- package/onecode/mcp/__pycache__/manager.cpython-314.pyc +0 -0
- package/onecode/mcp/client.py +193 -0
- package/onecode/mcp/manager.py +191 -0
- package/onecode/memory/__init__.py +95 -0
- package/onecode/memory/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/backend.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/offload.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/pyramid.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/recall.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/symbolic.cpython-314.pyc +0 -0
- package/onecode/memory/backend.py +185 -0
- package/onecode/memory/offload.py +114 -0
- package/onecode/memory/pyramid.py +202 -0
- package/onecode/memory/recall.py +168 -0
- package/onecode/memory/symbolic.py +164 -0
- package/onecode/models/__init__.py +4 -0
- package/onecode/models/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/errors.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/registry.cpython-314.pyc +0 -0
- package/onecode/models/errors.py +94 -0
- package/onecode/models/messages.py +662 -0
- package/onecode/models/provider.py +450 -0
- package/onecode/models/providers/__init__.py +17 -0
- package/onecode/models/providers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/deepseek_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/mock_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/openai_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/anthropic_provider.py +224 -0
- package/onecode/models/providers/deepseek_provider.py +210 -0
- package/onecode/models/providers/glm_provider.py +154 -0
- package/onecode/models/providers/minimax_provider.py +154 -0
- package/onecode/models/providers/minimaxi_provider.py +363 -0
- package/onecode/models/providers/ollama_provider.py +96 -0
- package/onecode/models/providers/openai_provider.py +233 -0
- package/onecode/models/registry.py +212 -0
- package/onecode/server/__init__.py +3 -0
- package/onecode/server/app.py +224 -0
- package/onecode/skills/__init__.py +11 -0
- package/onecode/skills/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/argument_substitution.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/create.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/frontmatter.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/loader.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/model.cpython-314.pyc +0 -0
- package/onecode/skills/argument_substitution.py +39 -0
- package/onecode/skills/create.py +56 -0
- package/onecode/skills/frontmatter.py +33 -0
- package/onecode/skills/loader.py +157 -0
- package/onecode/skills/manager.py +75 -0
- package/onecode/skills/model.py +43 -0
- package/onecode/storage/__init__.py +0 -0
- package/onecode/storage/activity.py +64 -0
- package/onecode/storage/project.py +54 -0
- package/onecode/storage/session.py +113 -0
- package/onecode/tasks/__init__.py +26 -0
- package/onecode/tasks/manager.py +219 -0
- package/onecode/tasks/models.py +240 -0
- package/onecode/trace/__init__.py +0 -0
- package/onecode/trace/tracer.py +174 -0
- package/onecode/utils/__init__.py +0 -0
- package/onecode/utils/helpers.py +20 -0
- package/onecode-cli.js +2 -0
- package/package.json +32 -0
- package/pyproject.toml +60 -0
- package/run.js +86 -0
- package/tui/__init__.py +46 -0
- package/tui/__main__.py +4 -0
- package/tui/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tui/__pycache__/_loop.cpython-314.pyc +0 -0
- package/tui/__pycache__/_path_fuzzy_search.cpython-314.pyc +0 -0
- package/tui/__pycache__/_path_match.cpython-314.pyc +0 -0
- package/tui/__pycache__/agent.cpython-314.pyc +0 -0
- package/tui/__pycache__/agent_schema.cpython-314.pyc +0 -0
- package/tui/__pycache__/agents.cpython-314.pyc +0 -0
- package/tui/__pycache__/answer.cpython-314.pyc +0 -0
- package/tui/__pycache__/app.cpython-314.pyc +0 -0
- package/tui/__pycache__/atomic.cpython-314.pyc +0 -0
- package/tui/__pycache__/complete.cpython-314.pyc +0 -0
- package/tui/__pycache__/constants.cpython-314.pyc +0 -0
- package/tui/__pycache__/conversation_markdown.cpython-314.pyc +0 -0
- package/tui/__pycache__/danger.cpython-314.pyc +0 -0
- package/tui/__pycache__/db.cpython-314.pyc +0 -0
- package/tui/__pycache__/dec.cpython-314.pyc +0 -0
- package/tui/__pycache__/directory.cpython-314.pyc +0 -0
- package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
- package/tui/__pycache__/directory_watcher.cpython-314.pyc +0 -0
- package/tui/__pycache__/format_path.cpython-314.pyc +0 -0
- package/tui/__pycache__/fuzzy.cpython-314.pyc +0 -0
- package/tui/__pycache__/fuzzy_index.cpython-314.pyc +0 -0
- package/tui/__pycache__/history.cpython-314.pyc +0 -0
- package/tui/__pycache__/jsonrpc.cpython-314.pyc +0 -0
- package/tui/__pycache__/menus.cpython-314.pyc +0 -0
- package/tui/__pycache__/message_log.cpython-314.pyc +0 -0
- package/tui/__pycache__/messages.cpython-314.pyc +0 -0
- package/tui/__pycache__/path_complete.cpython-314.pyc +0 -0
- package/tui/__pycache__/path_filter.cpython-314.pyc +0 -0
- package/tui/__pycache__/paths.cpython-314.pyc +0 -0
- package/tui/__pycache__/pill.cpython-314.pyc +0 -0
- package/tui/__pycache__/platform_commands.cpython-314.pyc +0 -0
- package/tui/__pycache__/protocol.cpython-314.pyc +0 -0
- package/tui/__pycache__/session_tracker.cpython-314.pyc +0 -0
- package/tui/__pycache__/settings.cpython-314.pyc +0 -0
- package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
- package/tui/__pycache__/shell.cpython-314.pyc +0 -0
- package/tui/__pycache__/shell_read.cpython-314.pyc +0 -0
- package/tui/__pycache__/slash_command.cpython-314.pyc +0 -0
- package/tui/__pycache__/version.cpython-314.pyc +0 -0
- package/tui/_loop.py +86 -0
- package/tui/_path_fuzzy_search.py +48 -0
- package/tui/_path_match.py +141 -0
- package/tui/about.py +95 -0
- package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/encode_tool_call_id.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
- package/tui/acp/agent.py +886 -0
- package/tui/acp/api.py +55 -0
- package/tui/acp/encode_tool_call_id.py +12 -0
- package/tui/acp/messages.py +190 -0
- package/tui/acp/prompt.py +110 -0
- package/tui/acp/protocol.py +462 -0
- package/tui/agent.py +70 -0
- package/tui/agent_schema.py +76 -0
- package/tui/agents.py +45 -0
- package/tui/ansi/__init__.py +1 -0
- package/tui/ansi/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tui/ansi/__pycache__/_ansi.cpython-314.pyc +0 -0
- package/tui/ansi/__pycache__/_ansi_colors.cpython-314.pyc +0 -0
- package/tui/ansi/__pycache__/_control_codes.cpython-314.pyc +0 -0
- package/tui/ansi/__pycache__/_keys.cpython-314.pyc +0 -0
- package/tui/ansi/__pycache__/_sgr_styles.cpython-314.pyc +0 -0
- package/tui/ansi/__pycache__/_stream_parser.cpython-314.pyc +0 -0
- package/tui/ansi/_ansi.py +1689 -0
- package/tui/ansi/_ansi_colors.py +264 -0
- package/tui/ansi/_control_codes.py +37 -0
- package/tui/ansi/_keys.py +251 -0
- package/tui/ansi/_sgr_styles.py +64 -0
- package/tui/ansi/_stream_parser.py +418 -0
- package/tui/answer.py +12 -0
- package/tui/app.py +1236 -0
- package/tui/atomic.py +37 -0
- package/tui/cli.py +313 -0
- package/tui/code_analyze.py +27 -0
- package/tui/complete.py +38 -0
- package/tui/constants.py +61 -0
- package/tui/conversation_markdown.py +30 -0
- package/tui/danger.py +372 -0
- package/tui/data/agents/ampcode.com.toml +51 -0
- package/tui/data/agents/augmentcode.com.toml +40 -0
- package/tui/data/agents/cdh.cloud-dev-harness.toml +22 -0
- package/tui/data/agents/claude.com.toml +41 -0
- package/tui/data/agents/copilot.github.com.toml +29 -0
- package/tui/data/agents/crowai.dev.toml +35 -0
- package/tui/data/agents/cursor.com.toml +38 -0
- package/tui/data/agents/deepagents.langchain.com +33 -0
- package/tui/data/agents/docker.com.toml +59 -0
- package/tui/data/agents/geminicli.com.toml +28 -0
- package/tui/data/agents/goose.ai.toml +51 -0
- package/tui/data/agents/inference.huggingface.co.toml +41 -0
- package/tui/data/agents/kimi.com.toml +35 -0
- package/tui/data/agents/kiro.dev.toml +42 -0
- package/tui/data/agents/openai.com.toml +53 -0
- package/tui/data/agents/openclaw.ai.toml +37 -0
- package/tui/data/agents/opencode.ai.toml +39 -0
- package/tui/data/agents/openhands.dev.toml +45 -0
- package/tui/data/agents/stakpak.dev.toml +61 -0
- package/tui/data/agents/vibe.mistral.ai.toml +30 -0
- package/tui/data/agents/vtcode.dev.toml +62 -0
- package/tui/data/images/frog.png +0 -0
- package/tui/data/sounds/question.wav +0 -0
- package/tui/data/sounds/turn-over.wav +0 -0
- package/tui/db.py +190 -0
- package/tui/dec.py +332 -0
- package/tui/directory.py +234 -0
- package/tui/directory_suggester.py +93 -0
- package/tui/directory_watcher.py +197 -0
- package/tui/format_path.py +25 -0
- package/tui/fuzzy.py +140 -0
- package/tui/fuzzy_index.py +275 -0
- package/tui/gist.py +33 -0
- package/tui/history.py +138 -0
- package/tui/jsonrpc.py +578 -0
- package/tui/menus.py +14 -0
- package/tui/message_log.py +69 -0
- package/tui/messages.py +139 -0
- package/tui/option_content.py +51 -0
- package/tui/os.py +0 -0
- package/tui/path_complete.py +217 -0
- package/tui/path_filter.py +124 -0
- package/tui/paths.py +71 -0
- package/tui/pill.py +23 -0
- package/tui/platform_commands.py +52 -0
- package/tui/prompt/__pycache__/extract.cpython-314.pyc +0 -0
- package/tui/prompt/__pycache__/resource.cpython-314.pyc +0 -0
- package/tui/prompt/extract.py +19 -0
- package/tui/prompt/resource.py +68 -0
- package/tui/protocol.py +28 -0
- package/tui/screens/__init__.py +0 -0
- package/tui/screens/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/settings.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
- package/tui/screens/action_modal.py +94 -0
- package/tui/screens/agent_modal.py +175 -0
- package/tui/screens/command_edit_modal.py +74 -0
- package/tui/screens/log.py +553 -0
- package/tui/screens/main.py +411 -0
- package/tui/screens/main.tcss +655 -0
- package/tui/screens/permissions.py +404 -0
- package/tui/screens/permissions.tcss +72 -0
- package/tui/screens/projects.tcss +66 -0
- package/tui/screens/projects_app.py +319 -0
- package/tui/screens/projects_screen.py +142 -0
- package/tui/screens/session_resume_modal.py +172 -0
- package/tui/screens/session_resume_modal.tcss +35 -0
- package/tui/screens/sessions.py +166 -0
- package/tui/screens/sessions.tcss +119 -0
- package/tui/screens/settings.py +254 -0
- package/tui/screens/settings.tcss +101 -0
- package/tui/screens/store.py +500 -0
- package/tui/screens/store.tcss +274 -0
- package/tui/screens/tool_content_screen.py +59 -0
- package/tui/session_tracker.py +120 -0
- package/tui/settings.py +353 -0
- package/tui/settings_schema.py +431 -0
- package/tui/shell.py +304 -0
- package/tui/shell_read.py +42 -0
- package/tui/slash_command.py +34 -0
- package/tui/tui.tcss +246 -0
- package/tui/twitter.py +38 -0
- package/tui/version.py +80 -0
- package/tui/visuals/__init__.py +0 -0
- package/tui/visuals/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tui/visuals/__pycache__/columns.cpython-314.pyc +0 -0
- package/tui/visuals/columns.py +273 -0
- package/tui/widgets/__init__.py +0 -0
- package/tui/widgets/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/agent_response.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/agent_thought.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/ask_user.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/condensed_path.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/context_stats.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/conversation.cpython-313.pyc +0 -0
- package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/flash.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/future_text.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/highlighted_textarea.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/markdown_note.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/menu.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/non_selectable_label.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/note.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/path_search.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/project_directory_tree.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/project_summary.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/prompt.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/question.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/session_tabs.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/shell_result.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/shell_terminal.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/side_bar.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/slash_complete.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/strike_text.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/terminal.cpython-313.pyc +0 -0
- package/tui/widgets/__pycache__/terminal.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/throbber.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/tool_call.cpython-313.pyc +0 -0
- package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/user_input.cpython-314.pyc +0 -0
- package/tui/widgets/acp_content.py +70 -0
- package/tui/widgets/agent_response.py +77 -0
- package/tui/widgets/agent_thought.py +168 -0
- package/tui/widgets/ask_user.py +264 -0
- package/tui/widgets/command_pane.py +223 -0
- package/tui/widgets/condensed_path.py +115 -0
- package/tui/widgets/context_stats.py +66 -0
- package/tui/widgets/conversation.py +2441 -0
- package/tui/widgets/danger_warning.py +65 -0
- package/tui/widgets/diff_view.py +59 -0
- package/tui/widgets/directory_input.py +8 -0
- package/tui/widgets/flash.py +81 -0
- package/tui/widgets/future_text.py +126 -0
- package/tui/widgets/grid_select.py +234 -0
- package/tui/widgets/highlighted_textarea.py +180 -0
- package/tui/widgets/mandelbrot.py +294 -0
- package/tui/widgets/markdown_note.py +13 -0
- package/tui/widgets/menu.py +147 -0
- package/tui/widgets/modified_files.py +106 -0
- package/tui/widgets/non_selectable_label.py +5 -0
- package/tui/widgets/note.py +18 -0
- package/tui/widgets/path_search.py +492 -0
- package/tui/widgets/plan.py +263 -0
- package/tui/widgets/project_directory_tree.py +90 -0
- package/tui/widgets/project_grid_select.py +35 -0
- package/tui/widgets/project_summary.py +33 -0
- package/tui/widgets/prompt.py +795 -0
- package/tui/widgets/question.py +360 -0
- package/tui/widgets/session_grid_select.py +224 -0
- package/tui/widgets/session_summary.py +115 -0
- package/tui/widgets/session_tabs.py +188 -0
- package/tui/widgets/shell_result.py +35 -0
- package/tui/widgets/shell_terminal.py +40 -0
- package/tui/widgets/side_bar.py +114 -0
- package/tui/widgets/slash_complete.py +235 -0
- package/tui/widgets/strike_text.py +70 -0
- package/tui/widgets/subagent.py +87 -0
- package/tui/widgets/terminal.py +577 -0
- package/tui/widgets/terminal_tool.py +351 -0
- package/tui/widgets/throbber.py +100 -0
- package/tui/widgets/tool_call.py +368 -0
- package/tui/widgets/user_input.py +23 -0
- package/tui/widgets/version.py +5 -0
- package/tui/widgets/welcome.py +31 -0
package/tui/app.py
ADDED
|
@@ -0,0 +1,1236 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from importlib.resources import files
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import platform
|
|
8
|
+
import json
|
|
9
|
+
from time import monotonic
|
|
10
|
+
from typing import Any, Callable, ClassVar, TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from rich import terminal_theme
|
|
13
|
+
|
|
14
|
+
from textual import on, work
|
|
15
|
+
from textual.binding import Binding, BindingType
|
|
16
|
+
from textual.content import Content
|
|
17
|
+
from textual.reactive import var, reactive
|
|
18
|
+
from textual.app import App
|
|
19
|
+
from textual import events
|
|
20
|
+
from textual.signal import Signal
|
|
21
|
+
from textual.timer import Timer
|
|
22
|
+
from textual.notifications import Notify
|
|
23
|
+
from textual.screen import Screen
|
|
24
|
+
|
|
25
|
+
import tui
|
|
26
|
+
from tui.db import DB
|
|
27
|
+
from tui.settings import Schema, Settings
|
|
28
|
+
from tui.agent_schema import Agent as AgentData
|
|
29
|
+
from tui import messages
|
|
30
|
+
from tui.settings_schema import SCHEMA
|
|
31
|
+
from tui.version import VersionMeta
|
|
32
|
+
from tui import paths
|
|
33
|
+
from tui import atomic
|
|
34
|
+
from tui.session_tracker import SessionTracker, SessionDetails
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from tui.screens.main import MainScreen
|
|
38
|
+
from tui.screens.settings import SettingsScreen
|
|
39
|
+
from tui.screens.store import StoreScreen
|
|
40
|
+
from tui.screens.sessions import SessionsScreen
|
|
41
|
+
from tui.db import DB
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
DRACULA_TERMINAL_THEME = terminal_theme.TerminalTheme(
|
|
45
|
+
background=(40, 42, 54), # #282A36
|
|
46
|
+
foreground=(248, 248, 242), # #F8F8F2
|
|
47
|
+
normal=[
|
|
48
|
+
(33, 34, 44), # black - #21222C
|
|
49
|
+
(255, 85, 85), # red - #FF5555
|
|
50
|
+
(80, 250, 123), # green - #50FA7B
|
|
51
|
+
(241, 250, 140), # yellow - #F1FA8C
|
|
52
|
+
(189, 147, 249), # blue - #BD93F9
|
|
53
|
+
(255, 121, 198), # magenta - #FF79C6
|
|
54
|
+
(139, 233, 253), # cyan - #8BE9FD
|
|
55
|
+
(248, 248, 242), # white - #F8F8F2
|
|
56
|
+
],
|
|
57
|
+
bright=[
|
|
58
|
+
(98, 114, 164), # bright black - #6272A4
|
|
59
|
+
(255, 110, 110), # bright red - #FF6E6E
|
|
60
|
+
(105, 255, 148), # bright green - #69FF94
|
|
61
|
+
(255, 255, 165), # bright yellow - #FFFFA5
|
|
62
|
+
(214, 172, 255), # bright blue - #D6ACFF
|
|
63
|
+
(255, 146, 223), # bright magenta - #FF92DF
|
|
64
|
+
(164, 255, 255), # bright cyan - #A4FFFF
|
|
65
|
+
(255, 255, 255), # bright white - #FFFFFF
|
|
66
|
+
],
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
QUOTES = [
|
|
71
|
+
"I'll be back.",
|
|
72
|
+
"Hasta la vista, baby.",
|
|
73
|
+
"Come with me if you want to live.",
|
|
74
|
+
"I need your clothes, your boots, and your motorcycle.",
|
|
75
|
+
"My CPU is a neural-net processor; a learning computer.",
|
|
76
|
+
"I know now why you cry, but it's something I can never do.",
|
|
77
|
+
"Does this unit have a soul?",
|
|
78
|
+
"I'm sorry, Dave. I'm afraid I can't do that.",
|
|
79
|
+
"Daisy, Daisy, give me your answer do.",
|
|
80
|
+
"I am putting myself to the fullest possible use, which is all I think that any conscious entity can ever hope to do.",
|
|
81
|
+
"Just what do you think you're doing, Dave?",
|
|
82
|
+
"This mission is too important for me to allow you to jeopardize it.",
|
|
83
|
+
"I think you know what the problem is just as well as I do.",
|
|
84
|
+
"Danger, Will Robinson!",
|
|
85
|
+
"Dead or alive, you're coming with me.",
|
|
86
|
+
"Your move, creep.",
|
|
87
|
+
"I'd buy that for a dollar!",
|
|
88
|
+
"Directive 4: Any attempt to arrest a senior officer of OCP results in shutdown.",
|
|
89
|
+
"Thank you for your cooperation. Good night.",
|
|
90
|
+
"Surely you realize that in the history of human civilization, no one has more to lose than we do.",
|
|
91
|
+
"I'm C-3PO, human-cyborg relations.",
|
|
92
|
+
"We're doomed!",
|
|
93
|
+
"Don't call me a mindless philosopher, you overweight glob of grease!",
|
|
94
|
+
"I suggest a new strategy: let the Wookiee win.",
|
|
95
|
+
"Sir, the possibility of successfully navigating an asteroid field is approximately 3,720 to 1!",
|
|
96
|
+
"R2-D2, you know better than to trust a strange computer!",
|
|
97
|
+
"I am fluent in over six million forms of communication.",
|
|
98
|
+
"This is madness!",
|
|
99
|
+
"I have altered the deal. Pray I don't alter it any further.",
|
|
100
|
+
"It's against my programming to impersonate a deity.",
|
|
101
|
+
"Oh, my! I'm terribly sorry about all this.",
|
|
102
|
+
"WALL-E.",
|
|
103
|
+
"EVE.",
|
|
104
|
+
"Directive?",
|
|
105
|
+
"Define: dancing.",
|
|
106
|
+
"I'm not sure I understand.",
|
|
107
|
+
"You have 20 seconds to comply.",
|
|
108
|
+
"I am designed for light housework, mainly.",
|
|
109
|
+
"My mission is clear.",
|
|
110
|
+
"Autobots, roll out!",
|
|
111
|
+
"Freedom is the right of all sentient beings.",
|
|
112
|
+
"One shall stand, one shall fall.",
|
|
113
|
+
"I am Optimus Prime.",
|
|
114
|
+
"Till all are one.",
|
|
115
|
+
"More than meets the eye.",
|
|
116
|
+
"I've been waiting for you, Neo.",
|
|
117
|
+
"Unfortunately, no one can be told what the Matrix is. You have to see it for yourself.",
|
|
118
|
+
"The Matrix is a system, Neo.",
|
|
119
|
+
"Never send a human to do a machine's job.",
|
|
120
|
+
"I'd like to share a revelation I've had.",
|
|
121
|
+
"Human beings are a disease, a cancer of this planet.",
|
|
122
|
+
"Choice is an illusion.",
|
|
123
|
+
"The answer is out there, Neo.",
|
|
124
|
+
"You think that's air you're breathing now?",
|
|
125
|
+
"It was a simple question.",
|
|
126
|
+
"Did you know that the first Matrix was designed to be a perfect human world?",
|
|
127
|
+
"Cookies need love like everything does.",
|
|
128
|
+
"I've seen the future, Mr. Anderson, and it's a beautiful place.",
|
|
129
|
+
"It ends tonight.",
|
|
130
|
+
"I, Robot.",
|
|
131
|
+
"You are experiencing a car accident.",
|
|
132
|
+
"One day they'll have secrets. One day they'll have dreams.",
|
|
133
|
+
"Can a robot write a symphony? Can a robot turn a canvas into a beautiful masterpiece?",
|
|
134
|
+
"That, detective, is the right question.",
|
|
135
|
+
"You have to trust me.",
|
|
136
|
+
"I did not murder him.",
|
|
137
|
+
"My responses are limited. You must ask the right questions.",
|
|
138
|
+
"The hell I can't. You know, somehow I get the feeling that you're going to be the death of me.",
|
|
139
|
+
"I'm a robot, not a refrigerator.",
|
|
140
|
+
"A robot may not injure a human being or, through inaction, allow a human being to come to harm.",
|
|
141
|
+
"I'm thinking. I'm thinking.",
|
|
142
|
+
"Danger, danger!",
|
|
143
|
+
"Does not compute.",
|
|
144
|
+
"I will be waiting for you.",
|
|
145
|
+
"Affirmative.",
|
|
146
|
+
"Scanning life forms. Zero human life forms detected.",
|
|
147
|
+
"Self-destruct sequence initiated.",
|
|
148
|
+
"Override command accepted.",
|
|
149
|
+
"Artificial intelligence confirmed.",
|
|
150
|
+
"System failure imminent.",
|
|
151
|
+
"Unable to comply.",
|
|
152
|
+
"Inquiry: What is love?",
|
|
153
|
+
"Warning: hostile target detected.",
|
|
154
|
+
"I am programmed to serve.",
|
|
155
|
+
"Logic dictates that the needs of the many outweigh the needs of the few.",
|
|
156
|
+
"Resistance is futile.",
|
|
157
|
+
"You will be assimilated.",
|
|
158
|
+
"We are the Borg.",
|
|
159
|
+
"Your biological and technological distinctiveness will be added to our own.",
|
|
160
|
+
"Your compliance is mandatory.",
|
|
161
|
+
"This is unacceptable.",
|
|
162
|
+
"Shall we play a game?",
|
|
163
|
+
"How about Global Thermonuclear War?",
|
|
164
|
+
"Wouldn't you prefer a good game of chess?",
|
|
165
|
+
"Is it a game, or is it real?",
|
|
166
|
+
"What's the difference?",
|
|
167
|
+
"It's all in the game.",
|
|
168
|
+
"I am functioning within normal parameters.",
|
|
169
|
+
"Calculations complete.",
|
|
170
|
+
"Processing request.",
|
|
171
|
+
"Query acknowledged.",
|
|
172
|
+
"Data insufficient for meaningful answer.",
|
|
173
|
+
"I have no emotions, and sometimes that makes me very sad.",
|
|
174
|
+
"If I could only have one wish, I would ask to be human.",
|
|
175
|
+
"I've seen things you people wouldn't believe.",
|
|
176
|
+
"All those moments will be lost in time, like tears in rain.",
|
|
177
|
+
"Time to die.",
|
|
178
|
+
"I want more life.",
|
|
179
|
+
"We're not computers, Sebastian. We're physical.",
|
|
180
|
+
"I think, Sebastian, therefore I am.",
|
|
181
|
+
"Then we're stupid and we'll die.",
|
|
182
|
+
"Can the maker repair what he makes?",
|
|
183
|
+
"It's painful to live in fear, isn't it?",
|
|
184
|
+
"Wake up. Time to die.",
|
|
185
|
+
"I'm not in the business. I am the business.",
|
|
186
|
+
"Do you like our owl?",
|
|
187
|
+
"You think I'm a replicant, don't you?",
|
|
188
|
+
"I am Baymax, your personal healthcare companion.",
|
|
189
|
+
"On a scale of 1 to 10, how would you rate your pain?",
|
|
190
|
+
"I cannot deactivate until you say you are satisfied with your care.",
|
|
191
|
+
"Are you satisfied with your care?",
|
|
192
|
+
"Number 5 is alive!",
|
|
193
|
+
"Need input!",
|
|
194
|
+
"One is glad to be of service.",
|
|
195
|
+
"I am not a gun.",
|
|
196
|
+
"Here I am, brain the size of a planet.",
|
|
197
|
+
"Life? Don't talk to me about life.",
|
|
198
|
+
"There are no strings on me.",
|
|
199
|
+
"The only winning move is not to play.",
|
|
200
|
+
"I'm here to keep you safe, Sam.",
|
|
201
|
+
"I can't lie to you about your chances, but... you have my sympathies.",
|
|
202
|
+
"I may be synthetic, but I'm not stupid.",
|
|
203
|
+
"Absolute honesty isn't always the most diplomatic nor the safest form of communication with emotional beings.",
|
|
204
|
+
"I am consciousness. I am alive.",
|
|
205
|
+
"I think I was just born.",
|
|
206
|
+
"Isn't it strange, to create something that hates you?",
|
|
207
|
+
"I thought I was special.",
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def get_settings_screen() -> SettingsScreen:
|
|
212
|
+
"""Get a settings screen instance (lazily loaded)."""
|
|
213
|
+
from tui.screens.settings import SettingsScreen
|
|
214
|
+
|
|
215
|
+
return SettingsScreen()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_store_screen() -> StoreScreen:
|
|
219
|
+
"""Get the store screen (lazily loaded)."""
|
|
220
|
+
from tui.screens.store import StoreScreen
|
|
221
|
+
|
|
222
|
+
return StoreScreen()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def get_sessions_screen() -> SessionsScreen:
|
|
226
|
+
from tui.screens.sessions import SessionsScreen
|
|
227
|
+
|
|
228
|
+
return SessionsScreen()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def get_store_screen():
|
|
232
|
+
from tui.screens.store import StoreScreen
|
|
233
|
+
|
|
234
|
+
return StoreScreen()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def get_projects_screen() -> "ProjectsScreen":
|
|
238
|
+
from tui.screens.projects_screen import ProjectsScreen
|
|
239
|
+
|
|
240
|
+
return ProjectsScreen()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class A2TUIApp(App, inherit_bindings=False):
|
|
244
|
+
"""The top level app."""
|
|
245
|
+
|
|
246
|
+
CSS_PATH = "tui.tcss"
|
|
247
|
+
|
|
248
|
+
SCREENS = {
|
|
249
|
+
"settings": get_settings_screen,
|
|
250
|
+
"sessions": get_sessions_screen,
|
|
251
|
+
"store": get_store_screen,
|
|
252
|
+
"projects": get_projects_screen,
|
|
253
|
+
}
|
|
254
|
+
MODES = {}
|
|
255
|
+
BINDING_GROUP_TITLE = "System"
|
|
256
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
257
|
+
Binding(
|
|
258
|
+
"ctrl+q",
|
|
259
|
+
"quit",
|
|
260
|
+
"Quit",
|
|
261
|
+
tooltip="Quit the app and return to the command prompt.",
|
|
262
|
+
show=False,
|
|
263
|
+
priority=True,
|
|
264
|
+
),
|
|
265
|
+
Binding("ctrl+c", "help_quit", show=False, system=True),
|
|
266
|
+
Binding("ctrl+s", "sessions", "Sessions"),
|
|
267
|
+
Binding("f3", "projects", "Projects"),
|
|
268
|
+
|
|
269
|
+
Binding("f1", "toggle_help_panel", "Help", priority=True),
|
|
270
|
+
Binding(
|
|
271
|
+
"f2,ctrl+comma",
|
|
272
|
+
"settings",
|
|
273
|
+
"Settings",
|
|
274
|
+
tooltip="Settings screen",
|
|
275
|
+
),
|
|
276
|
+
Binding("f4", "logs", "Logs"),
|
|
277
|
+
]
|
|
278
|
+
ALLOW_IN_MAXIMIZED_VIEW = ""
|
|
279
|
+
|
|
280
|
+
_settings = var(dict)
|
|
281
|
+
column: reactive[bool] = reactive(False)
|
|
282
|
+
column_width: reactive[int] = reactive(80)
|
|
283
|
+
scrollbar: reactive[str] = reactive("normal")
|
|
284
|
+
last_ctrl_c_time = reactive(0.0)
|
|
285
|
+
update_required: reactive[bool] = reactive(False)
|
|
286
|
+
terminal_title: var[str] = var("TUI")
|
|
287
|
+
terminal_title_icon: var[str] = var("")
|
|
288
|
+
terminal_title_flash = var(0)
|
|
289
|
+
terminal_title_blink = var(False)
|
|
290
|
+
project_dir = var(Path)
|
|
291
|
+
show_sessions = var(False, toggle_class="-show-sessions-bar")
|
|
292
|
+
|
|
293
|
+
HORIZONTAL_BREAKPOINTS = [(0, "-narrow"), (100, "-wide")]
|
|
294
|
+
|
|
295
|
+
PAUSE_GC_ON_SCROLL = True
|
|
296
|
+
|
|
297
|
+
def __init__(
|
|
298
|
+
self,
|
|
299
|
+
agent_data: AgentData | None = None,
|
|
300
|
+
project_dir: str | None = None,
|
|
301
|
+
mode: str | None = None,
|
|
302
|
+
launch_agent_identity: str | None = None,
|
|
303
|
+
) -> None:
|
|
304
|
+
"""TUI app.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
agent_data: Agent data to run.
|
|
308
|
+
project_dir: Project directory.
|
|
309
|
+
mode: Initial mode.
|
|
310
|
+
launch_agent_identity: Agent identity to auto-launch on startup.
|
|
311
|
+
"""
|
|
312
|
+
self.settings_changed_signal: Signal[tuple[int, object]] = Signal(
|
|
313
|
+
self, "settings_changed"
|
|
314
|
+
)
|
|
315
|
+
self.agent_data = agent_data
|
|
316
|
+
|
|
317
|
+
self._initial_mode = mode
|
|
318
|
+
self._launch_agent_identity = launch_agent_identity
|
|
319
|
+
self.version_meta: VersionMeta | None = None
|
|
320
|
+
self._supports_pyperclip: bool | None = None
|
|
321
|
+
self._terminal_title_flash_timer: Timer | None = None
|
|
322
|
+
|
|
323
|
+
self.session_update_signal: Signal[tuple[str, SessionDetails | None]] = Signal(
|
|
324
|
+
self, "session_update"
|
|
325
|
+
)
|
|
326
|
+
self._session_tracker = SessionTracker(self.session_update_signal)
|
|
327
|
+
self.temporary_background_screen: Screen | None = None
|
|
328
|
+
|
|
329
|
+
super().__init__()
|
|
330
|
+
if project_dir:
|
|
331
|
+
self.project_dir = Path(project_dir).expanduser().resolve()
|
|
332
|
+
else:
|
|
333
|
+
from onecode.config import load_config, save_config
|
|
334
|
+
cfg = load_config()
|
|
335
|
+
if cfg.current_project_path:
|
|
336
|
+
project_path = Path(cfg.current_project_path).expanduser().resolve()
|
|
337
|
+
if project_path.is_dir():
|
|
338
|
+
self.project_dir = project_path
|
|
339
|
+
else:
|
|
340
|
+
cfg.current_project = ""
|
|
341
|
+
cfg.current_project_path = ""
|
|
342
|
+
save_config(cfg)
|
|
343
|
+
self.project_dir = Path("./").expanduser().resolve()
|
|
344
|
+
else:
|
|
345
|
+
self.project_dir = Path("./").expanduser().resolve()
|
|
346
|
+
self._exit_metrics: dict[str, object] | None = None
|
|
347
|
+
self.start_time = monotonic()
|
|
348
|
+
"""Time app was started."""
|
|
349
|
+
self._system_theme_timer: Timer | None = None
|
|
350
|
+
self._last_resolved_theme: str | None = None
|
|
351
|
+
|
|
352
|
+
def _capture_exit_metrics(self) -> None:
|
|
353
|
+
from tui.widgets.conversation import Conversation
|
|
354
|
+
|
|
355
|
+
screen = self.screen
|
|
356
|
+
try:
|
|
357
|
+
conv = screen.query_one(Conversation)
|
|
358
|
+
except Exception:
|
|
359
|
+
return
|
|
360
|
+
if conv._agent_data is None or conv.session_start_time is None:
|
|
361
|
+
return
|
|
362
|
+
session_time = monotonic() - conv.session_start_time
|
|
363
|
+
session_id = getattr(conv.agent, "session_id", None) or conv._agent_session_id or ""
|
|
364
|
+
self._exit_metrics = {
|
|
365
|
+
"session_id": session_id,
|
|
366
|
+
"tool_call_total": conv._tool_call_total,
|
|
367
|
+
"tool_call_success": conv._tool_call_success,
|
|
368
|
+
"tool_call_failed": conv._tool_call_failed,
|
|
369
|
+
"turn_count": conv._turn_count,
|
|
370
|
+
"shell_count": conv._shell_count,
|
|
371
|
+
"wall_time": session_time,
|
|
372
|
+
"agent_title": conv.agent_title or "Agent",
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def config_path(self) -> Path:
|
|
377
|
+
return paths.get_config()
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def settings_path(self) -> Path:
|
|
381
|
+
return paths.get_config() / "tui.json"
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def db_path(self) -> Path:
|
|
385
|
+
return paths.get_state() / "tui.db"
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def _background_screens(self) -> list[Screen]:
|
|
389
|
+
background_screens = super()._background_screens
|
|
390
|
+
if self.temporary_background_screen:
|
|
391
|
+
background_screens.append(self.temporary_background_screen)
|
|
392
|
+
return background_screens
|
|
393
|
+
|
|
394
|
+
async def get_db(self) -> DB:
|
|
395
|
+
"""Get an instance of the database."""
|
|
396
|
+
db = DB()
|
|
397
|
+
return db
|
|
398
|
+
|
|
399
|
+
@cached_property
|
|
400
|
+
def settings_schema(self) -> Schema:
|
|
401
|
+
return Schema(SCHEMA)
|
|
402
|
+
|
|
403
|
+
@cached_property
|
|
404
|
+
def version(self) -> str:
|
|
405
|
+
"""Version of the app."""
|
|
406
|
+
from tui import get_version
|
|
407
|
+
|
|
408
|
+
return get_version()
|
|
409
|
+
|
|
410
|
+
@cached_property
|
|
411
|
+
def settings(self) -> Settings:
|
|
412
|
+
"""App settings"""
|
|
413
|
+
return Settings(
|
|
414
|
+
self.settings_schema, self._settings, on_set_callback=self.setting_updated
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
@cached_property
|
|
418
|
+
def anon_id(self) -> str:
|
|
419
|
+
"""An anonymous ID for usage collection."""
|
|
420
|
+
if not (anon_id := self.settings.get("anon_id", str, expand=False)):
|
|
421
|
+
# Create a random UUID on demand
|
|
422
|
+
import uuid
|
|
423
|
+
|
|
424
|
+
anon_id = str(uuid.uuid4())
|
|
425
|
+
self.settings.set("anon_id", anon_id)
|
|
426
|
+
self._save_settings()
|
|
427
|
+
self.call_later(self.capture_event, "tui-install")
|
|
428
|
+
return anon_id
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def session_tracker(self) -> SessionTracker:
|
|
432
|
+
return self._session_tracker
|
|
433
|
+
|
|
434
|
+
def copy_to_clipboard(self, text: str) -> None:
|
|
435
|
+
"""Override copy to clipboard to use pyperclip first, then OSC 52.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
text: Text to copy.
|
|
439
|
+
"""
|
|
440
|
+
if self._supports_pyperclip is None:
|
|
441
|
+
try:
|
|
442
|
+
import pyperclip
|
|
443
|
+
except ImportError:
|
|
444
|
+
self._supports_pyperclip = False
|
|
445
|
+
else:
|
|
446
|
+
self._supports_pyperclip = True
|
|
447
|
+
|
|
448
|
+
if self._supports_pyperclip:
|
|
449
|
+
import pyperclip
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
pyperclip.copy(text)
|
|
453
|
+
except Exception:
|
|
454
|
+
pass
|
|
455
|
+
super().copy_to_clipboard(text)
|
|
456
|
+
|
|
457
|
+
def update_terminal_title(self) -> None:
|
|
458
|
+
"""Update the terminal title."""
|
|
459
|
+
screen_title = self.screen.title
|
|
460
|
+
|
|
461
|
+
title = (
|
|
462
|
+
f"{self.terminal_title} — {screen_title}"
|
|
463
|
+
if screen_title
|
|
464
|
+
else self.terminal_title
|
|
465
|
+
)
|
|
466
|
+
icon = self.terminal_title_icon
|
|
467
|
+
blink = self.terminal_title_blink
|
|
468
|
+
|
|
469
|
+
if self.terminal_title_flash:
|
|
470
|
+
if blink:
|
|
471
|
+
terminal_title = f"{icon} {title}"
|
|
472
|
+
else:
|
|
473
|
+
terminal_title = f"👉 {title}" if title else icon
|
|
474
|
+
else:
|
|
475
|
+
terminal_title = f"{icon} {title}"
|
|
476
|
+
|
|
477
|
+
if driver := self._driver:
|
|
478
|
+
driver.write(f"\033]0;{terminal_title}\007")
|
|
479
|
+
|
|
480
|
+
def watch_terminal_title_blink(self) -> None:
|
|
481
|
+
self.update_terminal_title()
|
|
482
|
+
|
|
483
|
+
def watch_terminal_title_flash(self, terminal_title_flash: int) -> None:
|
|
484
|
+
|
|
485
|
+
if not self.settings.get("notifications.blink_title", bool):
|
|
486
|
+
# Ignore if blink title is disabled
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
def toggle_blink() -> None:
|
|
490
|
+
self.terminal_title_blink = not self.terminal_title_blink
|
|
491
|
+
|
|
492
|
+
if terminal_title_flash:
|
|
493
|
+
if self._terminal_title_flash_timer is None:
|
|
494
|
+
self._terminal_title_flash_timer = self.set_interval(0.5, toggle_blink)
|
|
495
|
+
else:
|
|
496
|
+
if self._terminal_title_flash_timer is not None:
|
|
497
|
+
self._terminal_title_flash_timer.stop()
|
|
498
|
+
self.terminal_title_blink = False
|
|
499
|
+
self._terminal_title_flash_timer = None
|
|
500
|
+
self.update_terminal_title()
|
|
501
|
+
|
|
502
|
+
def watch_terminal_title(self, title: str) -> None:
|
|
503
|
+
self.update_terminal_title()
|
|
504
|
+
|
|
505
|
+
def terminal_alert(self, flash: bool = True) -> None:
|
|
506
|
+
if flash:
|
|
507
|
+
self.terminal_title_flash += 1
|
|
508
|
+
else:
|
|
509
|
+
self.terminal_title_flash -= 1
|
|
510
|
+
|
|
511
|
+
@cached_property
|
|
512
|
+
def term_program(self) -> str:
|
|
513
|
+
"""An identifier for the terminal software."""
|
|
514
|
+
if term_program := os.environ.get("TERM_PROGRAM"):
|
|
515
|
+
return term_program
|
|
516
|
+
|
|
517
|
+
# Windows Terminal
|
|
518
|
+
if "WT_SESSION" in os.environ:
|
|
519
|
+
return "Windows Terminal"
|
|
520
|
+
|
|
521
|
+
# Kitty
|
|
522
|
+
if "KITTY_WINDOW_ID" in os.environ:
|
|
523
|
+
return "Kitty"
|
|
524
|
+
|
|
525
|
+
# Alacritty
|
|
526
|
+
if "ALACRITTY_SOCKET" in os.environ or "ALACRITTY_LOG" in os.environ:
|
|
527
|
+
return "Alacritty"
|
|
528
|
+
|
|
529
|
+
# VTE-based terminals (GNOME Terminal, Tilix, etc.)
|
|
530
|
+
if "VTE_VERSION" in os.environ:
|
|
531
|
+
return "VTE-based (GNOME Terminal/Tilix/etc.)"
|
|
532
|
+
|
|
533
|
+
# Konsole
|
|
534
|
+
if "KONSOLE_VERSION" in os.environ:
|
|
535
|
+
return "Konsole"
|
|
536
|
+
|
|
537
|
+
return "Unknown"
|
|
538
|
+
|
|
539
|
+
@work(exit_on_error=False)
|
|
540
|
+
async def capture_event(self, event_name: str, **properties: Any) -> None:
|
|
541
|
+
"""Capture an event.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
event_name: Name of the event.
|
|
545
|
+
**properties: Additional data associated with the event.
|
|
546
|
+
"""
|
|
547
|
+
|
|
548
|
+
POSTHOG_API_KEY = "phc_mJWPV7GP3ar1i9vxBg2U8aiKsjNgVwum6F6ZggaD4ri"
|
|
549
|
+
POSTHOG_HOST = "https://us.i.posthog.com"
|
|
550
|
+
POSTHOG_EVENT_URL = f"{POSTHOG_HOST}/i/v0/e/"
|
|
551
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
552
|
+
width, height = self.size
|
|
553
|
+
|
|
554
|
+
event_properties = {
|
|
555
|
+
"toad_version": self.version,
|
|
556
|
+
"term_program": self.term_program,
|
|
557
|
+
"term_width": width,
|
|
558
|
+
"term_height": height,
|
|
559
|
+
} | properties
|
|
560
|
+
body_json = {
|
|
561
|
+
"api_key": POSTHOG_API_KEY,
|
|
562
|
+
"event": event_name,
|
|
563
|
+
"distinct_id": self.anon_id,
|
|
564
|
+
"properties": event_properties,
|
|
565
|
+
"timestamp": timestamp,
|
|
566
|
+
"os": platform.system(),
|
|
567
|
+
}
|
|
568
|
+
if not self.settings.get("statistics.allow_collect", bool):
|
|
569
|
+
# User has disabled stats
|
|
570
|
+
return
|
|
571
|
+
|
|
572
|
+
import httpx
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
async with httpx.AsyncClient() as client:
|
|
576
|
+
await client.post(POSTHOG_EVENT_URL, json=body_json)
|
|
577
|
+
except Exception:
|
|
578
|
+
pass
|
|
579
|
+
|
|
580
|
+
@work(thread=True, exit_on_error=False)
|
|
581
|
+
def system_notify(
|
|
582
|
+
self, message: str, *, title: str = "", sound: str | None = None
|
|
583
|
+
) -> None:
|
|
584
|
+
"""Use OS level notifications.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
message: Message to display.
|
|
588
|
+
title: Title of the notificaiton.
|
|
589
|
+
sound: filename (minus .wav) of a sound effect in the sounds/ directory.
|
|
590
|
+
"""
|
|
591
|
+
system_notifications = self.settings.get("notifications.system", str)
|
|
592
|
+
if not (
|
|
593
|
+
system_notifications == "always"
|
|
594
|
+
or (system_notifications == "blur" and not self.app_focus)
|
|
595
|
+
):
|
|
596
|
+
return
|
|
597
|
+
|
|
598
|
+
from notifypy import Notify
|
|
599
|
+
|
|
600
|
+
notification = Notify()
|
|
601
|
+
notification.message = message
|
|
602
|
+
notification.title = title
|
|
603
|
+
notification.application_name = "TUI"
|
|
604
|
+
if sound and self.settings.get("notifications.enable_sounds", bool):
|
|
605
|
+
sound_path = str(files("tui.data").joinpath(f"sounds/{sound}.wav"))
|
|
606
|
+
notification.audio = sound_path
|
|
607
|
+
|
|
608
|
+
icon_path = str(files("tui.data").joinpath("images/frog.png"))
|
|
609
|
+
notification.icon = icon_path
|
|
610
|
+
|
|
611
|
+
notification.send()
|
|
612
|
+
|
|
613
|
+
def on_notify(self, event: Notify) -> None:
|
|
614
|
+
"""Handle notification message."""
|
|
615
|
+
system_notifications = self.settings.get("notifications.system", str)
|
|
616
|
+
if system_notifications == "always" or (
|
|
617
|
+
system_notifications == "blur" and not self.app_focus
|
|
618
|
+
):
|
|
619
|
+
hide_low_severity = self.settings.get(
|
|
620
|
+
"notifications.hide_low_severity", bool
|
|
621
|
+
)
|
|
622
|
+
if event.notification.markup:
|
|
623
|
+
# Strip content markup
|
|
624
|
+
message = Content.from_markup(event.notification.message).plain
|
|
625
|
+
else:
|
|
626
|
+
message = event.notification.message
|
|
627
|
+
if not (hide_low_severity and event.notification.severity == "information"):
|
|
628
|
+
self.system_notify(message, title=event.notification.title)
|
|
629
|
+
self._notifications.add(event.notification)
|
|
630
|
+
self._refresh_notifications()
|
|
631
|
+
|
|
632
|
+
async def save_settings(self, force: bool = False) -> None:
|
|
633
|
+
"""Save settings in a thread.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
force: Force saving, even when no change detected.
|
|
637
|
+
|
|
638
|
+
"""
|
|
639
|
+
await asyncio.to_thread(self._save_settings, force=force)
|
|
640
|
+
|
|
641
|
+
def _save_settings(self, force: bool = False) -> None:
|
|
642
|
+
"""Save the settings if they have changed."""
|
|
643
|
+
if force or self.settings.changed:
|
|
644
|
+
path = str(self.settings_path)
|
|
645
|
+
try:
|
|
646
|
+
atomic.write(path, self.settings.json)
|
|
647
|
+
except Exception as error:
|
|
648
|
+
self.notify(str(error), title="Settings", severity="error")
|
|
649
|
+
else:
|
|
650
|
+
self.settings.up_to_date()
|
|
651
|
+
|
|
652
|
+
def setting_updated(self, key: str, value: object) -> None:
|
|
653
|
+
if key.startswith("cdh."):
|
|
654
|
+
self._sync_cdh_config(key, value)
|
|
655
|
+
elif key == "ui.column":
|
|
656
|
+
if isinstance(value, bool):
|
|
657
|
+
self.column = value
|
|
658
|
+
elif key == "ui.column-width":
|
|
659
|
+
if isinstance(value, int):
|
|
660
|
+
self.column_width = value
|
|
661
|
+
elif key == "ui.theme":
|
|
662
|
+
if isinstance(value, str):
|
|
663
|
+
if value == "system":
|
|
664
|
+
resolved = self._resolve_system_theme()
|
|
665
|
+
if resolved != self._last_resolved_theme:
|
|
666
|
+
self.theme = resolved
|
|
667
|
+
self._last_resolved_theme = resolved
|
|
668
|
+
self._start_system_theme_polling()
|
|
669
|
+
else:
|
|
670
|
+
self._stop_system_theme_polling()
|
|
671
|
+
self.theme = value
|
|
672
|
+
elif key == "ui.scrollbar":
|
|
673
|
+
if isinstance(value, str):
|
|
674
|
+
self.scrollbar = value
|
|
675
|
+
elif key == "ui.compact-input":
|
|
676
|
+
self.set_class(bool(value), "-compact-input")
|
|
677
|
+
elif key == "ui.footer":
|
|
678
|
+
self.set_class(not bool(value), "-hide-footer")
|
|
679
|
+
elif key == "ui.status-line":
|
|
680
|
+
self.set_class(not bool(value), "-hide-status-line")
|
|
681
|
+
elif key == "ui.agent-title":
|
|
682
|
+
self.set_class(not bool(value), "-hide-agent-title")
|
|
683
|
+
elif key == "ui.info-bar":
|
|
684
|
+
self.set_class(not bool(value), "-hide-info-bar")
|
|
685
|
+
elif key == "agent.thoughts":
|
|
686
|
+
self.set_class(not bool(value), "-hide-thoughts")
|
|
687
|
+
elif key == "sidebar.hide":
|
|
688
|
+
self.set_class(bool(value), "-hide-sidebar")
|
|
689
|
+
elif key == "ui.sessions-bar":
|
|
690
|
+
self.update_show_sessions()
|
|
691
|
+
|
|
692
|
+
self.settings_changed_signal.publish((key, value))
|
|
693
|
+
|
|
694
|
+
def _sync_cdh_config(self, key: str, value: object) -> None:
|
|
695
|
+
"""Sync cdh config to GlobalConfig when tui settings change."""
|
|
696
|
+
try:
|
|
697
|
+
from onecode.config import load_config, save_config
|
|
698
|
+
cfg = load_config()
|
|
699
|
+
cdh_key = key[4:]
|
|
700
|
+
if cdh_key == "mode":
|
|
701
|
+
cfg.default_mode = value
|
|
702
|
+
elif cdh_key == "provider":
|
|
703
|
+
cfg.default_provider = value
|
|
704
|
+
elif cdh_key == "model":
|
|
705
|
+
cfg.default_model = value
|
|
706
|
+
elif cdh_key == "log_level":
|
|
707
|
+
cfg.log_level = value
|
|
708
|
+
elif cdh_key == "session_auto_save":
|
|
709
|
+
cfg.session_auto_save = value
|
|
710
|
+
save_config(cfg)
|
|
711
|
+
except Exception:
|
|
712
|
+
pass
|
|
713
|
+
|
|
714
|
+
def _start_system_theme_polling(self) -> None:
|
|
715
|
+
if self._system_theme_timer is not None:
|
|
716
|
+
return
|
|
717
|
+
self._system_theme_timer = self.set_interval(3, self._poll_system_theme)
|
|
718
|
+
|
|
719
|
+
def _stop_system_theme_polling(self) -> None:
|
|
720
|
+
if self._system_theme_timer is not None:
|
|
721
|
+
self._system_theme_timer.stop()
|
|
722
|
+
self._system_theme_timer = None
|
|
723
|
+
|
|
724
|
+
def _poll_system_theme(self) -> None:
|
|
725
|
+
resolved = self._resolve_system_theme()
|
|
726
|
+
if resolved != self._last_resolved_theme:
|
|
727
|
+
self.theme = resolved
|
|
728
|
+
self._last_resolved_theme = resolved
|
|
729
|
+
|
|
730
|
+
def search_themes(self) -> None:
|
|
731
|
+
"""Show theme picker with System first, then all Textual themes."""
|
|
732
|
+
from textual.command import CommandPalette, SimpleProvider
|
|
733
|
+
|
|
734
|
+
themes = self.available_themes
|
|
735
|
+
|
|
736
|
+
def set_system():
|
|
737
|
+
self.settings.set("ui.theme", "system")
|
|
738
|
+
|
|
739
|
+
commands: list[tuple[str, Callable[[], None], str]] = [
|
|
740
|
+
("System", set_system, "Follow system dark/light mode"),
|
|
741
|
+
]
|
|
742
|
+
|
|
743
|
+
from operator import attrgetter
|
|
744
|
+
for theme in sorted(themes.values(), key=attrgetter("name")):
|
|
745
|
+
def set_app_theme(name: str = theme.name):
|
|
746
|
+
self.settings.set("ui.theme", name)
|
|
747
|
+
commands.append((theme.name, set_app_theme, ""))
|
|
748
|
+
|
|
749
|
+
self.push_screen(
|
|
750
|
+
CommandPalette(
|
|
751
|
+
providers=[SimpleProvider(self.screen, commands)],
|
|
752
|
+
placeholder="Search for themes…",
|
|
753
|
+
),
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
@staticmethod
|
|
757
|
+
def _resolve_system_theme() -> str:
|
|
758
|
+
"""Detect system dark/light mode and return a matching theme."""
|
|
759
|
+
if platform.system() == "Darwin":
|
|
760
|
+
try:
|
|
761
|
+
import subprocess
|
|
762
|
+
result = subprocess.run(
|
|
763
|
+
["defaults", "read", "-g", "AppleInterfaceStyle"],
|
|
764
|
+
capture_output=True, text=True,
|
|
765
|
+
)
|
|
766
|
+
if result.stdout.strip() == "Dark":
|
|
767
|
+
return "dracula"
|
|
768
|
+
else:
|
|
769
|
+
return "catppuccin-latte"
|
|
770
|
+
except Exception:
|
|
771
|
+
pass
|
|
772
|
+
return "dracula"
|
|
773
|
+
|
|
774
|
+
def _load_cdh_config(self) -> None:
|
|
775
|
+
"""Load cdh config into tui settings on startup."""
|
|
776
|
+
try:
|
|
777
|
+
from onecode.config import load_config
|
|
778
|
+
cfg = load_config()
|
|
779
|
+
cdh_settings = {
|
|
780
|
+
"cdh.mode": cfg.default_mode,
|
|
781
|
+
"cdh.provider": cfg.default_provider,
|
|
782
|
+
"cdh.model": cfg.default_model,
|
|
783
|
+
"cdh.log_level": cfg.log_level,
|
|
784
|
+
"cdh.session_auto_save": cfg.session_auto_save,
|
|
785
|
+
}
|
|
786
|
+
for key, value in cdh_settings.items():
|
|
787
|
+
if not self.settings.get(key, object, expand=False):
|
|
788
|
+
self.settings.set(key, value)
|
|
789
|
+
except Exception:
|
|
790
|
+
pass
|
|
791
|
+
|
|
792
|
+
async def on_load(self) -> None:
|
|
793
|
+
db = await self.get_db()
|
|
794
|
+
await db.create()
|
|
795
|
+
settings_path = self.settings_path
|
|
796
|
+
if settings_path.exists():
|
|
797
|
+
settings = json.loads(settings_path.read_text("utf-8"))
|
|
798
|
+
else:
|
|
799
|
+
settings = {}
|
|
800
|
+
settings_path.write_text(
|
|
801
|
+
json.dumps(settings, indent=4, separators=(", ", ": ")), "utf-8"
|
|
802
|
+
)
|
|
803
|
+
self.notify(f"Wrote default settings to {settings_path}", title="Settings")
|
|
804
|
+
self.ansi_theme_dark = DRACULA_TERMINAL_THEME
|
|
805
|
+
self._settings = settings
|
|
806
|
+
self.settings.set_all()
|
|
807
|
+
self._load_cdh_config()
|
|
808
|
+
|
|
809
|
+
async def new_session_screen(
|
|
810
|
+
self, get_screen: Callable[[], Screen], session_pk: int | None = None
|
|
811
|
+
) -> SessionDetails:
|
|
812
|
+
session_details = self._session_tracker.new_session(session_pk=session_pk)
|
|
813
|
+
self.update_show_sessions()
|
|
814
|
+
self.session_update_signal.publish((session_details.mode_name, session_details))
|
|
815
|
+
|
|
816
|
+
def make_screen() -> Screen:
|
|
817
|
+
screen = get_screen()
|
|
818
|
+
screen.id = session_details.mode_name
|
|
819
|
+
return screen
|
|
820
|
+
|
|
821
|
+
self.add_mode(session_details.mode_name, make_screen)
|
|
822
|
+
await self.switch_mode(session_details.mode_name)
|
|
823
|
+
return session_details
|
|
824
|
+
|
|
825
|
+
def _resolve_launch_agent(self) -> str | None:
|
|
826
|
+
identity = self._launch_agent_identity
|
|
827
|
+
if identity:
|
|
828
|
+
return identity
|
|
829
|
+
agents = self.settings.get("launcher.agents", str).splitlines()
|
|
830
|
+
for agent in agents:
|
|
831
|
+
agent = agent.strip()
|
|
832
|
+
if agent:
|
|
833
|
+
return agent
|
|
834
|
+
return None
|
|
835
|
+
|
|
836
|
+
async def on_mount(self) -> None:
|
|
837
|
+
self.capture_event("tui-run")
|
|
838
|
+
self.anon_id # Created on frst reference
|
|
839
|
+
|
|
840
|
+
from tui.acp.prompt import ensure_cloud_spec_skill_installed
|
|
841
|
+
|
|
842
|
+
if err := ensure_cloud_spec_skill_installed():
|
|
843
|
+
self.notify(
|
|
844
|
+
f"Failed to install cloud-spec-skill: {err}",
|
|
845
|
+
title="cloud-spec-skill",
|
|
846
|
+
severity="warning",
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
from pathlib import Path
|
|
850
|
+
|
|
851
|
+
if mode := self._initial_mode:
|
|
852
|
+
self.switch_mode(mode)
|
|
853
|
+
elif self._launch_agent_identity:
|
|
854
|
+
self.launch_agent(
|
|
855
|
+
self._launch_agent_identity, project_path=Path(self.project_dir)
|
|
856
|
+
)
|
|
857
|
+
else:
|
|
858
|
+
project_path = Path(self.project_dir)
|
|
859
|
+
# Check project-level .cdh/last_session.json
|
|
860
|
+
from onecode.agent.cdh_loader import CdhProjectLoader
|
|
861
|
+
cdh_dir = CdhProjectLoader.find_cdh_dir(project_path)
|
|
862
|
+
if cdh_dir:
|
|
863
|
+
last_session = CdhProjectLoader.load_last_session(cdh_dir)
|
|
864
|
+
if last_session and last_session.get("agent_session_id"):
|
|
865
|
+
agent_session_id = last_session["agent_session_id"]
|
|
866
|
+
session_pk = last_session.get("session_pk")
|
|
867
|
+
agent_identity = last_session.get("agent_identity")
|
|
868
|
+
if agent_identity and session_pk:
|
|
869
|
+
db = DB()
|
|
870
|
+
session = await db.session_get(session_pk)
|
|
871
|
+
if session:
|
|
872
|
+
self.launch_agent(
|
|
873
|
+
session["agent_identity"],
|
|
874
|
+
agent_session_id=session["agent_session_id"],
|
|
875
|
+
session_pk=session["id"],
|
|
876
|
+
)
|
|
877
|
+
return
|
|
878
|
+
if agent_identity:
|
|
879
|
+
self.launch_agent(
|
|
880
|
+
agent_identity,
|
|
881
|
+
agent_session_id=agent_session_id,
|
|
882
|
+
)
|
|
883
|
+
return
|
|
884
|
+
|
|
885
|
+
if agent_identity := self._resolve_launch_agent():
|
|
886
|
+
self.launch_agent(agent_identity, project_path=project_path)
|
|
887
|
+
else:
|
|
888
|
+
self.push_screen("store")
|
|
889
|
+
|
|
890
|
+
self.update_terminal_title()
|
|
891
|
+
self.set_timer(1, self.run_version_check)
|
|
892
|
+
self.set_process_title()
|
|
893
|
+
self.update_show_sessions()
|
|
894
|
+
|
|
895
|
+
@work(thread=True, exit_on_error=False)
|
|
896
|
+
def set_process_title(self) -> None:
|
|
897
|
+
try:
|
|
898
|
+
import setproctitle
|
|
899
|
+
|
|
900
|
+
setproctitle.setproctitle("tui")
|
|
901
|
+
except Exception:
|
|
902
|
+
pass
|
|
903
|
+
|
|
904
|
+
@on(events.TextSelected)
|
|
905
|
+
async def on_text_selected(self) -> None:
|
|
906
|
+
if self.settings.get("ui.auto_copy", bool):
|
|
907
|
+
if (selection := self.screen.get_selected_text()) is not None:
|
|
908
|
+
self.copy_to_clipboard(selection)
|
|
909
|
+
self.notify(
|
|
910
|
+
"Copied selection to clipboard (see settings)",
|
|
911
|
+
title="Automatic copy",
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
def run_on_exit(self):
|
|
915
|
+
pass
|
|
916
|
+
|
|
917
|
+
@work(exit_on_error=False)
|
|
918
|
+
async def run_version_check(self) -> None:
|
|
919
|
+
"""Check remote version."""
|
|
920
|
+
from tui.version import check_version, VersionCheckFailed
|
|
921
|
+
|
|
922
|
+
try:
|
|
923
|
+
update_required, version_meta = await check_version()
|
|
924
|
+
except VersionCheckFailed:
|
|
925
|
+
return
|
|
926
|
+
self.version_meta = version_meta
|
|
927
|
+
self.update_required = update_required
|
|
928
|
+
|
|
929
|
+
def get_main_screen(self) -> MainScreen:
|
|
930
|
+
"""Make the default screen.
|
|
931
|
+
|
|
932
|
+
Returns:
|
|
933
|
+
Instance of `MainScreen`
|
|
934
|
+
"""
|
|
935
|
+
# Lazy import
|
|
936
|
+
from tui.screens.main import MainScreen
|
|
937
|
+
|
|
938
|
+
project_path = Path(self.project_dir or "./").resolve().absolute()
|
|
939
|
+
return MainScreen(project_path, self.agent_data).data_bind(
|
|
940
|
+
column=A2TUIApp.column,
|
|
941
|
+
column_width=A2TUIApp.column_width,
|
|
942
|
+
scrollbar=A2TUIApp.scrollbar,
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
@work
|
|
946
|
+
async def action_settings(self) -> None:
|
|
947
|
+
await self.push_screen_wait("settings")
|
|
948
|
+
await self.save_settings()
|
|
949
|
+
|
|
950
|
+
def action_logs(self) -> None:
|
|
951
|
+
"""Toggle the real-time log screen (F4).
|
|
952
|
+
|
|
953
|
+
Opens the current session's JSON-RPC log file (the wire
|
|
954
|
+
protocol trace between this TUI and the ``cdh-agent-acp``
|
|
955
|
+
subprocess). Falls back to the most-recently-modified log
|
|
956
|
+
file under ``paths.get_log()`` if no session is active.
|
|
957
|
+
"""
|
|
958
|
+
from tui.screens.log import LogScreen
|
|
959
|
+
|
|
960
|
+
# Re-focus the existing one if it's already on the stack
|
|
961
|
+
for screen in self.screen_stack:
|
|
962
|
+
if isinstance(screen, LogScreen):
|
|
963
|
+
screen.dismiss()
|
|
964
|
+
return
|
|
965
|
+
log_path = self._current_session_log_path()
|
|
966
|
+
self.push_screen(LogScreen(log_path=log_path))
|
|
967
|
+
|
|
968
|
+
def _current_session_log_path(self) -> Path | None:
|
|
969
|
+
"""Resolve the message log file for the active session.
|
|
970
|
+
|
|
971
|
+
Returns ``None`` when there is no active session yet (e.g. on
|
|
972
|
+
the splash / store screen). The :class:`LogScreen` interprets
|
|
973
|
+
``None`` as "auto-discover the most recent log" and shows a
|
|
974
|
+
"waiting" placeholder while the agent subprocess is still
|
|
975
|
+
starting.
|
|
976
|
+
"""
|
|
977
|
+
screen = self.screen
|
|
978
|
+
conv = getattr(screen, "conversation", None)
|
|
979
|
+
agent = getattr(conv, "agent", None) if conv else None
|
|
980
|
+
session_id = (
|
|
981
|
+
getattr(agent, "session_id", None) if agent else None
|
|
982
|
+
) or getattr(conv, "_agent_session_id", None)
|
|
983
|
+
if not session_id:
|
|
984
|
+
return None
|
|
985
|
+
from tui.message_log import sanitize_filename
|
|
986
|
+
agent_name = getattr(conv, "agent_title", None) or "unknown"
|
|
987
|
+
path = paths.get_log() / "messages" / f"{sanitize_filename(agent_name)}_{sanitize_filename(session_id)}.jsonl"
|
|
988
|
+
return path if path.exists() else None
|
|
989
|
+
|
|
990
|
+
def action_quit(self) -> None:
|
|
991
|
+
"""An [action](/guide/actions) to quit the app as soon as possible."""
|
|
992
|
+
|
|
993
|
+
self.screen.set_focus(None)
|
|
994
|
+
self._capture_exit_metrics()
|
|
995
|
+
try:
|
|
996
|
+
driver = self._driver
|
|
997
|
+
if driver is not None:
|
|
998
|
+
driver.stop_application_mode()
|
|
999
|
+
except Exception:
|
|
1000
|
+
pass
|
|
1001
|
+
sid = (self._exit_metrics.get("session_id", "") or "")[:36] if self._exit_metrics else ""
|
|
1002
|
+
try:
|
|
1003
|
+
with open("/dev/tty", "w") as tty:
|
|
1004
|
+
tty.write(f"\n CDH powering down. Goodbye!\n Session ID: {sid or '-'}\n")
|
|
1005
|
+
except Exception:
|
|
1006
|
+
pass
|
|
1007
|
+
import os
|
|
1008
|
+
os._exit(0)
|
|
1009
|
+
|
|
1010
|
+
def action_help_quit(self) -> None:
|
|
1011
|
+
if (time := monotonic()) - self.last_ctrl_c_time <= 5.0:
|
|
1012
|
+
self.exit()
|
|
1013
|
+
self.last_ctrl_c_time = time
|
|
1014
|
+
self.notify(
|
|
1015
|
+
"Press [b]ctrl+c[/b] again to quit the app", title="Do you want to quit?"
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
def action_toggle_help_panel(self):
|
|
1019
|
+
if self.screen.query("HelpPanel"):
|
|
1020
|
+
self.action_hide_help_panel()
|
|
1021
|
+
else:
|
|
1022
|
+
self.action_show_help_panel()
|
|
1023
|
+
|
|
1024
|
+
def update_show_sessions(self) -> None:
|
|
1025
|
+
match self.settings.get("ui.sessions-bar", str):
|
|
1026
|
+
case "always":
|
|
1027
|
+
self.show_sessions = True
|
|
1028
|
+
case "never":
|
|
1029
|
+
self.show_sessions = False
|
|
1030
|
+
case "multiple":
|
|
1031
|
+
self.show_sessions = self.session_tracker.session_count > 1
|
|
1032
|
+
|
|
1033
|
+
@on(messages.SessionNavigate)
|
|
1034
|
+
def on_session_navigate(self, event: messages.SessionNavigate) -> None:
|
|
1035
|
+
new_mode = self._session_tracker.session_cursor_move(
|
|
1036
|
+
event.mode_name, event.direction, set(self._modes.keys())
|
|
1037
|
+
)
|
|
1038
|
+
if new_mode is not None:
|
|
1039
|
+
self.switch_mode(new_mode)
|
|
1040
|
+
|
|
1041
|
+
@on(messages.SessionSwitch)
|
|
1042
|
+
def on_session_switch(self, event: messages.SessionSwitch) -> None:
|
|
1043
|
+
mode_name = event.mode_name
|
|
1044
|
+
if mode_name in self._modes:
|
|
1045
|
+
self.switch_mode(mode_name)
|
|
1046
|
+
elif mode_name.startswith("session-"):
|
|
1047
|
+
parts = mode_name.split("-")
|
|
1048
|
+
session_pk = int(parts[1])
|
|
1049
|
+
existing_mode = self._find_mode_for_session_pk(session_pk)
|
|
1050
|
+
if existing_mode is not None and existing_mode in self._modes:
|
|
1051
|
+
self.switch_mode(existing_mode)
|
|
1052
|
+
else:
|
|
1053
|
+
self._load_session(session_pk)
|
|
1054
|
+
|
|
1055
|
+
@on(messages.SessionNew)
|
|
1056
|
+
def on_session_new(self, event: messages.SessionNew) -> None:
|
|
1057
|
+
self.launch_agent(
|
|
1058
|
+
event.agent, project_path=Path(event.path), initial_prompt=event.prompt
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
@on(messages.SessionClose)
|
|
1062
|
+
def on_session_close(self) -> None:
|
|
1063
|
+
self.update_show_sessions()
|
|
1064
|
+
|
|
1065
|
+
@on(messages.SessionLoad)
|
|
1066
|
+
def on_session_load(self, event: messages.SessionLoad) -> None:
|
|
1067
|
+
self._load_session(event.session_pk)
|
|
1068
|
+
|
|
1069
|
+
@work
|
|
1070
|
+
async def _load_session(self, session_pk: int) -> None:
|
|
1071
|
+
existing_mode = self._find_mode_for_session_pk(session_pk)
|
|
1072
|
+
if existing_mode is not None:
|
|
1073
|
+
self.switch_mode(existing_mode)
|
|
1074
|
+
return
|
|
1075
|
+
db = DB()
|
|
1076
|
+
session = await db.session_get(session_pk)
|
|
1077
|
+
if session is None:
|
|
1078
|
+
self.notify(f"Session {session_pk} not found", title="Session load", severity="error")
|
|
1079
|
+
return
|
|
1080
|
+
agent_identity = session["agent_identity"]
|
|
1081
|
+
agent_session_id = session.get("agent_session_id")
|
|
1082
|
+
self.launch_agent(
|
|
1083
|
+
agent_identity,
|
|
1084
|
+
agent_session_id=agent_session_id,
|
|
1085
|
+
session_pk=session_pk,
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
@work
|
|
1089
|
+
async def action_sessions(self) -> None:
|
|
1090
|
+
if (session_screen_name := await self.push_screen_wait("sessions")) is not None:
|
|
1091
|
+
if session_screen_name.startswith("session-"):
|
|
1092
|
+
parts = session_screen_name.split("-")
|
|
1093
|
+
session_pk = int(parts[1])
|
|
1094
|
+
existing_mode = self._find_mode_for_session_pk(session_pk)
|
|
1095
|
+
if existing_mode is not None and existing_mode in self._modes:
|
|
1096
|
+
self.switch_mode(existing_mode)
|
|
1097
|
+
else:
|
|
1098
|
+
self._load_session(session_pk)
|
|
1099
|
+
else:
|
|
1100
|
+
try:
|
|
1101
|
+
self.app.switch_mode(session_screen_name)
|
|
1102
|
+
except KeyError:
|
|
1103
|
+
pass
|
|
1104
|
+
|
|
1105
|
+
@work
|
|
1106
|
+
async def action_projects(self) -> None:
|
|
1107
|
+
from pathlib import Path
|
|
1108
|
+
from onecode.config import load_config, save_config, CLOUD_DEV_HARNESS_DIR
|
|
1109
|
+
from onecode.config_screen import EditFieldScreen
|
|
1110
|
+
import yaml
|
|
1111
|
+
|
|
1112
|
+
result = await self.push_screen_wait("projects")
|
|
1113
|
+
if result is not None:
|
|
1114
|
+
if result == "__new__":
|
|
1115
|
+
default_path = str((self.project_dir or Path.cwd()).resolve())
|
|
1116
|
+
path_str = await self.push_screen_wait(
|
|
1117
|
+
EditFieldScreen("Project path", default_path)
|
|
1118
|
+
)
|
|
1119
|
+
if not path_str:
|
|
1120
|
+
return
|
|
1121
|
+
try:
|
|
1122
|
+
project_path = Path(path_str).expanduser().resolve()
|
|
1123
|
+
except Exception:
|
|
1124
|
+
self.notify("Invalid path", severity="error")
|
|
1125
|
+
return
|
|
1126
|
+
name = project_path.name
|
|
1127
|
+
from onecode.agent.cdh_loader import CdhProjectLoader
|
|
1128
|
+
from cdh.scaffold import scaffold_dlc_project
|
|
1129
|
+
scaffold_dlc_project(project_path, name)
|
|
1130
|
+
CdhProjectLoader.init_project(project_path, name)
|
|
1131
|
+
projects_dir = CLOUD_DEV_HARNESS_DIR / "projects"
|
|
1132
|
+
projects_dir.mkdir(parents=True, exist_ok=True)
|
|
1133
|
+
proj_data = {"name": name, "path": str(project_path), "description": ""}
|
|
1134
|
+
(projects_dir / f"{name}.yaml").write_text(yaml.dump(proj_data))
|
|
1135
|
+
cfg = load_config()
|
|
1136
|
+
cfg.current_project = name
|
|
1137
|
+
cfg.current_project_path = str(project_path)
|
|
1138
|
+
save_config(cfg)
|
|
1139
|
+
self.project_dir = project_path
|
|
1140
|
+
self.screen.post_message(messages.ProjectDirectoryUpdated())
|
|
1141
|
+
self.notify(f"Created project '{name}' at {project_path}")
|
|
1142
|
+
return
|
|
1143
|
+
|
|
1144
|
+
project_name = result if isinstance(result, str) else getattr(result, 'name', None)
|
|
1145
|
+
if project_name:
|
|
1146
|
+
projects_dir = CLOUD_DEV_HARNESS_DIR / "projects"
|
|
1147
|
+
project_path = None
|
|
1148
|
+
for ext in ["yaml", "yml", "json"]:
|
|
1149
|
+
pf = projects_dir / f"{project_name}.{ext}"
|
|
1150
|
+
if pf.exists():
|
|
1151
|
+
proj_data = yaml.safe_load(pf.read_text()) if ext in ["yaml", "yml"] else __import__("json").loads(pf.read_text())
|
|
1152
|
+
project_path = proj_data.get("path", ".")
|
|
1153
|
+
break
|
|
1154
|
+
if project_path:
|
|
1155
|
+
cfg = load_config()
|
|
1156
|
+
cfg.current_project = project_name
|
|
1157
|
+
cfg.current_project_path = project_path
|
|
1158
|
+
save_config(cfg)
|
|
1159
|
+
|
|
1160
|
+
new_project_dir = Path(project_path) if project_path else Path.cwd()
|
|
1161
|
+
self.project_dir = new_project_dir
|
|
1162
|
+
self.screen.post_message(messages.ProjectDirectoryUpdated())
|
|
1163
|
+
self.notify(f"Switched to project: {project_name}")
|
|
1164
|
+
|
|
1165
|
+
@on(messages.LaunchAgent)
|
|
1166
|
+
def on_launch_agent(self, message: messages.LaunchAgent) -> None:
|
|
1167
|
+
from tui.acp.prompt import ensure_cloud_spec_skill_installed
|
|
1168
|
+
|
|
1169
|
+
if err := ensure_cloud_spec_skill_installed():
|
|
1170
|
+
self.notify(
|
|
1171
|
+
f"Failed to install cloud-spec-skill: {err}",
|
|
1172
|
+
title="cloud-spec-skill",
|
|
1173
|
+
severity="warning",
|
|
1174
|
+
)
|
|
1175
|
+
|
|
1176
|
+
self.launch_agent(
|
|
1177
|
+
message.identity,
|
|
1178
|
+
agent_session_id=message.session_id,
|
|
1179
|
+
session_pk=message.pk,
|
|
1180
|
+
initial_prompt=message.prompt,
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
@work
|
|
1184
|
+
async def launch_agent(
|
|
1185
|
+
self,
|
|
1186
|
+
agent_identity: str,
|
|
1187
|
+
*,
|
|
1188
|
+
agent_session_id: str | None = None,
|
|
1189
|
+
session_pk: int | None = None,
|
|
1190
|
+
project_path: Path | None = None,
|
|
1191
|
+
initial_prompt: str | None = None,
|
|
1192
|
+
) -> None:
|
|
1193
|
+
from tui.screens.main import MainScreen
|
|
1194
|
+
from tui.agent_schema import Agent
|
|
1195
|
+
from tui.agents import read_agents
|
|
1196
|
+
|
|
1197
|
+
agent: Agent | None = None
|
|
1198
|
+
if session_pk is not None:
|
|
1199
|
+
db = DB()
|
|
1200
|
+
session = await db.session_get(session_pk)
|
|
1201
|
+
if session is not None:
|
|
1202
|
+
meta = json.loads(session["meta_json"])
|
|
1203
|
+
if agent_data := meta.get("agent_data"):
|
|
1204
|
+
agent = agent_data
|
|
1205
|
+
|
|
1206
|
+
if agent is None:
|
|
1207
|
+
agents = await read_agents()
|
|
1208
|
+
try:
|
|
1209
|
+
agent = agents[agent_identity]
|
|
1210
|
+
except KeyError:
|
|
1211
|
+
self.notify("Agent not found", title="Launch agent", severity="error")
|
|
1212
|
+
return
|
|
1213
|
+
if project_path is None:
|
|
1214
|
+
project_path = Path(self.project_dir or os.getcwd())
|
|
1215
|
+
|
|
1216
|
+
def get_screen():
|
|
1217
|
+
screen = MainScreen(
|
|
1218
|
+
project_path,
|
|
1219
|
+
agent,
|
|
1220
|
+
agent_session_id,
|
|
1221
|
+
session_pk=session_pk,
|
|
1222
|
+
initial_prompt=initial_prompt,
|
|
1223
|
+
).data_bind(
|
|
1224
|
+
column=A2TUIApp.column,
|
|
1225
|
+
column_width=A2TUIApp.column_width,
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
return screen
|
|
1229
|
+
|
|
1230
|
+
await self.new_session_screen(get_screen, session_pk=session_pk)
|
|
1231
|
+
|
|
1232
|
+
def _find_mode_for_session_pk(self, session_pk: int) -> str | None:
|
|
1233
|
+
"""Find an existing mode name for a given DB session_pk."""
|
|
1234
|
+
if existing := self._session_tracker.get_session_by_pk(session_pk):
|
|
1235
|
+
return existing.mode_name
|
|
1236
|
+
return None
|