limbo-code 0.1.12__tar.gz → 0.1.14__tar.gz
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.
- {limbo_code-0.1.12 → limbo_code-0.1.14}/AGENTS.md +2 -0
- limbo_code-0.1.12/README.md → limbo_code-0.1.14/PKG-INFO +31 -0
- limbo_code-0.1.12/PKG-INFO → limbo_code-0.1.14/README.md +17 -14
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/agent.py +5 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/app.py +18 -1
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/config.py +34 -0
- limbo_code-0.1.14/src/limbo/integrations/__init__.py +43 -0
- limbo_code-0.1.14/src/limbo/integrations/base.py +87 -0
- limbo_code-0.1.14/src/limbo/integrations/herdr.py +143 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/screens/main.py +33 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_cli.py +77 -8
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_config.py +31 -1
- limbo_code-0.1.14/tests/test_herdr.py +145 -0
- limbo_code-0.1.14/tests/test_integrations.py +60 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/.agents/skills/grill-with-docs/SKILL.md +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/.agents/skills/grill-with-docs/agents/openai.yaml +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/.agents/skills/improve-codebase-architecture/SKILL.md +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/.agents/skills/improve-codebase-architecture/agents/openai.yaml +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/.agents/skills/tdd/SKILL.md +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/.agents/skills/tdd/agents/openai.yaml +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/.agents/skills/tdd/mocking.md +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/.agents/skills/tdd/tests.md +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/.github/workflows/publish.yml +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/.github/workflows/test.yml +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/.gitignore +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/CONTEXT.md +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/Makefile +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/design/confirm_view.html +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/design/limbo-ui-minimal.md +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/design/limbo-ui-redesign.md +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/design/prototype-minimal-confirm.png +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/design/prototype-minimal.html +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/design/prototype-minimal.png +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/design/prototype.html +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/design/prototype.png +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/assets/limbo-current-ui.png +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/assets/limbo-new-ui.png +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-dark/1_idle.svg +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-dark/2_thinking_tool_running.svg +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-dark/3_tool_success.svg +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-dark/4_tool_error.svg +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-dark/5_llm_error.svg +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-dark/6_edit_diff.svg +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-light/1_idle.svg +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-light/2_thinking_tool_running.svg +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-light/3_tool_success.svg +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-light/4_tool_error.svg +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-light/5_llm_error.svg +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-light/6_edit_diff.svg +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/session-management.md +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/skills.md +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/docs/ui-redesign-proposal.md +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/pyproject.toml +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/scripts/check_contrast.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/scripts/gen_banner.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/scripts/ui_walkthrough.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/skills-lock.json +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/__init__.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/__main__.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/attachments.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/btw.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/compaction.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/goal.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/goal_driver.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/history.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/llm/__init__.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/llm/anthropic_client.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/llm/catalog.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/llm/client.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/llm/factory.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/llm/openai_client.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/llm/responses_client.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/llm/retry.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/llm/scaffold.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/llm/sse.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/llm/usage.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/model_switch.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/models.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/prompt.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/sessions.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/skills.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/steer.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/tools/__init__.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/tools/base.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/tools/bash.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/tools/edit.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/tools/find.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/tools/grep.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/tools/ignore.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/tools/ls.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/tools/mutation_queue.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/tools/read.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/tools/registry.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/tools/write.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/trace.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/__init__.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/app.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/app.tcss +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/banner.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/clipboard.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/commands.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/contrast.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/screens/__init__.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/screens/btw.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/screens/game2048.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/screens/model_picker.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/screens/session_picker.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/screens/verify_picker.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/syntax.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/theme.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/widgets/__init__.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/widgets/chat.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/widgets/command_menu.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/widgets/input.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/widgets/status_bar.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/ui/widgets/tool_card.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/src/limbo/user_paths.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/conftest.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_agent.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_anthropic_client.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_attachments.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_btw.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_catalog.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_clipboard.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_compaction.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_goal.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_goal_driver.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_history.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_integration.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_interrupt.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_llm_client.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_llm_scaffold.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_model_switch.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_models.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_prompt.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_responses_client.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_retry.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_sessions.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_skills.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_sse.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_steer.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_trace.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_usage.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/test_user_paths.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/tools/test_base.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/tools/test_bash.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/tools/test_edit.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/tools/test_find.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/tools/test_grep.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/tools/test_ignore.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/tools/test_ls.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/tools/test_mutation_queue.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/tools/test_read.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/tools/test_registry.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/tools/test_write.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/__snapshots__/test_snapshots/test_snapshot_idle.raw +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/__snapshots__/test_snapshots/test_snapshot_session_picker.raw +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/__snapshots__/test_snapshots/test_snapshot_thinking_and_running_tool.raw +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/__snapshots__/test_snapshots/test_snapshot_tool_error_and_error_message.raw +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_app_smoke.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_btw_ui.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_command_menu.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_commands.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_compact_ui.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_contrast.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_copy.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_game2048.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_goal_ui.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_input_attachments.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_input_history.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_input_paste.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_interrupt_ui.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_main_screen.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_model_picker.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_scroll_follow.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_sessions_ui.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_skills_ui.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_snapshots.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_startup_art.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_steer_ui.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_theme.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/tests/ui/test_widgets.py +0 -0
- {limbo_code-0.1.12 → limbo_code-0.1.14}/uv.lock +0 -0
|
@@ -33,6 +33,8 @@ src/limbo/
|
|
|
33
33
|
├── goal_driver.py # /goal orchestrator (frontend-agnostic, non-UI): turn → verify → next round
|
|
34
34
|
├── sessions.py # Session JSONL save/load/list/export
|
|
35
35
|
├── trace.py # Append-only JSONL run log (sessions/traces/)
|
|
36
|
+
├── integrations/ # External mux/orchestrator lifecycle reporters: base.py (protocol
|
|
37
|
+
│ # + CompositeReporter + AgentState), herdr.py; create_reporters() fans out
|
|
36
38
|
├── skills.py # SKILL.md discovery (user + project dirs)
|
|
37
39
|
├── user_paths.py # Fence grants from paths in user messages
|
|
38
40
|
├── llm/ # catalog.py (provider/model specs), factory.py (dialect→client),
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: limbo-code
|
|
3
|
+
Version: 0.1.14
|
|
4
|
+
Summary: A minimal terminal AI coding agent
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: httpx>=0.27
|
|
7
|
+
Requires-Dist: openai>=1.30
|
|
8
|
+
Requires-Dist: pydantic>=2.0
|
|
9
|
+
Requires-Dist: pygments>=2.17
|
|
10
|
+
Requires-Dist: textual>=8.0
|
|
11
|
+
Requires-Dist: toml>=0.10
|
|
12
|
+
Requires-Dist: tomlkit>=0.13
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
1
15
|
# Limbo
|
|
2
16
|
|
|
3
17
|
A minimal terminal AI coding agent.
|
|
@@ -169,6 +183,23 @@ Optional tool settings:
|
|
|
169
183
|
bash_enabled = true
|
|
170
184
|
```
|
|
171
185
|
|
|
186
|
+
Optional UI settings:
|
|
187
|
+
|
|
188
|
+
```toml
|
|
189
|
+
[ui]
|
|
190
|
+
kitty_keyboard = "auto" # auto (default) | enabled | disabled
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`kitty_keyboard` controls Textual's kitty keyboard protocol. The default
|
|
194
|
+
`auto` disables it only when Limbo runs inside the **herdr** multiplexer:
|
|
195
|
+
herdr mirrors a pane's `REPORT_ALL_KEYS` request to the host terminal
|
|
196
|
+
(e.g. Ghostty), and Ghostty then encodes IME commits as the physical commit
|
|
197
|
+
key (space) while dropping the composed text — typing Chinese turns into
|
|
198
|
+
spaces (pasting is unaffected). With the protocol disabled, herdr keeps
|
|
199
|
+
IME-compatible host flags and committed IME text arrives as raw UTF-8. Use
|
|
200
|
+
`enabled` to always keep the protocol (e.g. in a direct Ghostty/kitty
|
|
201
|
+
terminal), or `disabled` to force raw/legacy input everywhere.
|
|
202
|
+
|
|
172
203
|
### Session storage
|
|
173
204
|
|
|
174
205
|
Conversations are saved as JSONL files in `~/.limbo/sessions/` so you can
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: limbo-code
|
|
3
|
-
Version: 0.1.12
|
|
4
|
-
Summary: A minimal terminal AI coding agent
|
|
5
|
-
Requires-Python: >=3.11
|
|
6
|
-
Requires-Dist: httpx>=0.27
|
|
7
|
-
Requires-Dist: openai>=1.30
|
|
8
|
-
Requires-Dist: pydantic>=2.0
|
|
9
|
-
Requires-Dist: pygments>=2.17
|
|
10
|
-
Requires-Dist: textual>=8.0
|
|
11
|
-
Requires-Dist: toml>=0.10
|
|
12
|
-
Requires-Dist: tomlkit>=0.13
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
|
|
15
1
|
# Limbo
|
|
16
2
|
|
|
17
3
|
A minimal terminal AI coding agent.
|
|
@@ -183,6 +169,23 @@ Optional tool settings:
|
|
|
183
169
|
bash_enabled = true
|
|
184
170
|
```
|
|
185
171
|
|
|
172
|
+
Optional UI settings:
|
|
173
|
+
|
|
174
|
+
```toml
|
|
175
|
+
[ui]
|
|
176
|
+
kitty_keyboard = "auto" # auto (default) | enabled | disabled
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`kitty_keyboard` controls Textual's kitty keyboard protocol. The default
|
|
180
|
+
`auto` disables it only when Limbo runs inside the **herdr** multiplexer:
|
|
181
|
+
herdr mirrors a pane's `REPORT_ALL_KEYS` request to the host terminal
|
|
182
|
+
(e.g. Ghostty), and Ghostty then encodes IME commits as the physical commit
|
|
183
|
+
key (space) while dropping the composed text — typing Chinese turns into
|
|
184
|
+
spaces (pasting is unaffected). With the protocol disabled, herdr keeps
|
|
185
|
+
IME-compatible host flags and committed IME text arrives as raw UTF-8. Use
|
|
186
|
+
`enabled` to always keep the protocol (e.g. in a direct Ghostty/kitty
|
|
187
|
+
terminal), or `disabled` to force raw/legacy input everywhere.
|
|
188
|
+
|
|
186
189
|
### Session storage
|
|
187
190
|
|
|
188
191
|
Conversations are saved as JSONL files in `~/.limbo/sessions/` so you can
|
|
@@ -344,6 +344,11 @@ class Agent:
|
|
|
344
344
|
"""Stable id of the current session (the session file stem)."""
|
|
345
345
|
return self._meta.id
|
|
346
346
|
|
|
347
|
+
@property
|
|
348
|
+
def session_path(self) -> Path:
|
|
349
|
+
"""Path of the current session JSONL file."""
|
|
350
|
+
return self._session_file
|
|
351
|
+
|
|
347
352
|
@property
|
|
348
353
|
def session_meta(self) -> SessionMeta:
|
|
349
354
|
"""Metadata of the current session (title filled in on first save)."""
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
@@ -15,11 +16,24 @@ from limbo.sessions import (
|
|
|
15
16
|
latest_session,
|
|
16
17
|
load_session,
|
|
17
18
|
)
|
|
18
|
-
from limbo.ui.app import LimboApp
|
|
19
19
|
|
|
20
20
|
DEFAULT_SESSION_DIR = Path.home() / ".limbo" / "sessions"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def _apply_kitty_keyboard_workaround(config) -> None:
|
|
24
|
+
"""Apply the `ui.kitty_keyboard` setting before Textual initializes.
|
|
25
|
+
|
|
26
|
+
Textual reads TEXTUAL_DISABLE_KITTY_KEY at import time, so this must run
|
|
27
|
+
before the first `textual` import. See KittyKeyboardMode for why typing
|
|
28
|
+
Chinese inside the herdr multiplexer turns into spaces when the kitty
|
|
29
|
+
keyboard protocol is enabled.
|
|
30
|
+
"""
|
|
31
|
+
if config.ui.kitty_keyboard.disable_textual_kitty_key(
|
|
32
|
+
in_herdr=bool(os.environ.get("HERDR_ENV"))
|
|
33
|
+
):
|
|
34
|
+
os.environ["TEXTUAL_DISABLE_KITTY_KEY"] = "1"
|
|
35
|
+
|
|
36
|
+
|
|
23
37
|
def main() -> int:
|
|
24
38
|
parser = argparse.ArgumentParser(description="Limbo TUI coding agent")
|
|
25
39
|
parser.add_argument(
|
|
@@ -55,6 +69,7 @@ def main() -> int:
|
|
|
55
69
|
args = parser.parse_args()
|
|
56
70
|
|
|
57
71
|
config = load_config()
|
|
72
|
+
_apply_kitty_keyboard_workaround(config)
|
|
58
73
|
if args.model:
|
|
59
74
|
config.llm.model = args.model
|
|
60
75
|
spec = resolve_model(config.llm.model)
|
|
@@ -97,6 +112,8 @@ def main() -> int:
|
|
|
97
112
|
if meta.workdir and Path(meta.workdir).is_dir():
|
|
98
113
|
workdir = Path(meta.workdir)
|
|
99
114
|
|
|
115
|
+
from limbo.ui.app import LimboApp # noqa: I001, PLC0415 - deferred so the kitty keyboard workaround runs first
|
|
116
|
+
|
|
100
117
|
app = LimboApp(
|
|
101
118
|
workdir=workdir,
|
|
102
119
|
config=config,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import warnings
|
|
6
|
+
from enum import Enum
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
@@ -132,12 +133,45 @@ class ProviderOverride(BaseModel):
|
|
|
132
133
|
headers: dict[str, str] = Field(default_factory=dict)
|
|
133
134
|
|
|
134
135
|
|
|
136
|
+
class KittyKeyboardMode(str, Enum):
|
|
137
|
+
"""How Limbo negotiates Textual's kitty keyboard protocol.
|
|
138
|
+
|
|
139
|
+
Textual 8.2.8 requests the kitty keyboard protocol with ``REPORT_ALL_KEYS``.
|
|
140
|
+
Inside the herdr multiplexer, herdr mirrors that request to the host
|
|
141
|
+
terminal (e.g. Ghostty), and Ghostty then encodes IME commits as the
|
|
142
|
+
physical commit key (space) while dropping the composed text — typing
|
|
143
|
+
Chinese turns into spaces (pasting is unaffected, it bypasses the key
|
|
144
|
+
encoder). Disabling the protocol makes herdr negotiate IME-compatible host
|
|
145
|
+
flags so committed IME text arrives as raw UTF-8.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
# Disable only inside the herdr multiplexer (detected via HERDR_ENV);
|
|
149
|
+
# keep the protocol everywhere else. Safe default for CJK users.
|
|
150
|
+
AUTO = "auto"
|
|
151
|
+
# Always request the kitty keyboard protocol (Textual's default behavior).
|
|
152
|
+
ENABLED = "enabled"
|
|
153
|
+
# Never request it — forces raw/legacy terminal input everywhere.
|
|
154
|
+
DISABLED = "disabled"
|
|
155
|
+
|
|
156
|
+
def disable_textual_kitty_key(self, *, in_herdr: bool) -> bool:
|
|
157
|
+
"""Whether Textual's kitty keyboard protocol should be disabled."""
|
|
158
|
+
return self is KittyKeyboardMode.DISABLED or (
|
|
159
|
+
self is KittyKeyboardMode.AUTO and in_herdr
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
135
163
|
class UIConfig(BaseModel):
|
|
136
164
|
# Theme name: built-in "limbo-dark" (default) / "limbo-light", or any
|
|
137
165
|
# Textual built-in theme (e.g. "textual-dark", "dracula", "nord").
|
|
138
166
|
theme: str | None = None
|
|
139
167
|
# Show the startup ASCII-art banner on fresh sessions.
|
|
140
168
|
show_banner: bool = True
|
|
169
|
+
# Kitty keyboard protocol handling (see KittyKeyboardMode):
|
|
170
|
+
# auto - enable normally, except inside the herdr multiplexer where
|
|
171
|
+
# it breaks IME/Chinese input (default)
|
|
172
|
+
# enabled - always request the kitty keyboard protocol
|
|
173
|
+
# disabled - never request it (forces raw/legacy terminal input)
|
|
174
|
+
kitty_keyboard: KittyKeyboardMode = KittyKeyboardMode.AUTO
|
|
141
175
|
|
|
142
176
|
|
|
143
177
|
class SafetyConfig(BaseModel):
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Integrations with external terminal multiplexers / agent orchestrators.
|
|
2
|
+
|
|
3
|
+
Each integration lives in its own module (e.g. ``herdr``) and implements
|
|
4
|
+
the ``IntegrationReporter`` protocol from ``base``. Its ``from_env()``
|
|
5
|
+
returns ``None`` when Limbo is not running inside that tool, so the
|
|
6
|
+
integration is a strict no-op elsewhere. ``create_reporters()`` collects
|
|
7
|
+
every active integration into one ``CompositeReporter`` — the only object
|
|
8
|
+
call sites ever touch.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
|
|
16
|
+
from limbo.integrations.base import (
|
|
17
|
+
AgentState,
|
|
18
|
+
CompositeReporter,
|
|
19
|
+
IntegrationReporter,
|
|
20
|
+
)
|
|
21
|
+
from limbo.integrations.herdr import HerdrReporter
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"AgentState",
|
|
25
|
+
"CompositeReporter",
|
|
26
|
+
"HerdrReporter",
|
|
27
|
+
"IntegrationReporter",
|
|
28
|
+
"create_reporters",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_reporters(env: Mapping[str, str] | None = None) -> CompositeReporter:
|
|
33
|
+
"""Detect active integrations from the environment.
|
|
34
|
+
|
|
35
|
+
Register new integrations here: append ``XReporter.from_env(env)`` when
|
|
36
|
+
it returns a reporter. Order is irrelevant; each reporter is
|
|
37
|
+
independent and fire-and-forget.
|
|
38
|
+
"""
|
|
39
|
+
env = os.environ if env is None else env
|
|
40
|
+
reporters: list[IntegrationReporter] = []
|
|
41
|
+
if (herdr := HerdrReporter.from_env(env)) is not None:
|
|
42
|
+
reporters.append(herdr)
|
|
43
|
+
return CompositeReporter(reporters)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Shared contract for terminal multiplexer / agent orchestrator integrations.
|
|
2
|
+
|
|
3
|
+
Every integration (Herdr, future *mux tools, ...) implements
|
|
4
|
+
``IntegrationReporter``: it receives semantic lifecycle state
|
|
5
|
+
(``working``/``idle``/``blocked``) plus session identity, and maps that onto
|
|
6
|
+
whatever the specific tool understands. Call sites (UI, agent loop) only
|
|
7
|
+
ever talk to ``CompositeReporter`` — adding a new integration means writing
|
|
8
|
+
one module here and registering it in ``create_reporters()``, with zero
|
|
9
|
+
changes at call sites.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Iterable
|
|
15
|
+
from typing import Literal, Protocol
|
|
16
|
+
|
|
17
|
+
# Semantic lifecycle states every integration must be able to represent.
|
|
18
|
+
# ``blocked`` means the agent is waiting on a user decision.
|
|
19
|
+
AgentState = Literal["working", "idle", "blocked"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class IntegrationReporter(Protocol):
|
|
23
|
+
"""One external tool interested in Limbo's lifecycle.
|
|
24
|
+
|
|
25
|
+
Implementations must be fire-and-forget: never block, never raise.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def report(
|
|
29
|
+
self,
|
|
30
|
+
state: AgentState,
|
|
31
|
+
*,
|
|
32
|
+
message: str | None = None,
|
|
33
|
+
session_id: str | None = None,
|
|
34
|
+
session_path: str | None = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Report lifecycle state (optionally carrying session identity)."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def report_session(
|
|
40
|
+
self, session_id: str, session_path: str | None = None
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Report session identity when it changes independently of state."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def release(self) -> None:
|
|
46
|
+
"""Release any held authority/resources (on agent exit)."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CompositeReporter:
|
|
51
|
+
"""Fans lifecycle reports out to every active integration.
|
|
52
|
+
|
|
53
|
+
Always safe to call, even with zero reporters — call sites never need
|
|
54
|
+
to know which (if any) integrations are active.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, reporters: Iterable[IntegrationReporter] = ()): # noqa: D107
|
|
58
|
+
self._reporters = list(reporters)
|
|
59
|
+
|
|
60
|
+
def __bool__(self) -> bool:
|
|
61
|
+
return bool(self._reporters)
|
|
62
|
+
|
|
63
|
+
def report(
|
|
64
|
+
self,
|
|
65
|
+
state: AgentState,
|
|
66
|
+
*,
|
|
67
|
+
message: str | None = None,
|
|
68
|
+
session_id: str | None = None,
|
|
69
|
+
session_path: str | None = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
for reporter in self._reporters:
|
|
72
|
+
reporter.report(
|
|
73
|
+
state,
|
|
74
|
+
message=message,
|
|
75
|
+
session_id=session_id,
|
|
76
|
+
session_path=session_path,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def report_session(
|
|
80
|
+
self, session_id: str, session_path: str | None = None
|
|
81
|
+
) -> None:
|
|
82
|
+
for reporter in self._reporters:
|
|
83
|
+
reporter.report_session(session_id, session_path)
|
|
84
|
+
|
|
85
|
+
def release(self) -> None:
|
|
86
|
+
for reporter in self._reporters:
|
|
87
|
+
reporter.release()
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Herdr terminal multiplexer integration.
|
|
2
|
+
|
|
3
|
+
When Limbo runs inside a Herdr pane, it inherits ``HERDR_ENV``,
|
|
4
|
+
``HERDR_PANE_ID``, ``HERDR_BIN_PATH``, and ``HERDR_SOCKET_PATH``. This
|
|
5
|
+
reporter uses Herdr's CLI to report semantic lifecycle state
|
|
6
|
+
(``working``/``idle``/``blocked``) and native session identity, per
|
|
7
|
+
https://herdr.dev/docs/integrations/#integrate-your-own-agent
|
|
8
|
+
|
|
9
|
+
The integration is a strict no-op outside Herdr: ``from_env()`` returns
|
|
10
|
+
``None`` unless ``HERDR_ENV=1`` and both the pane id and a usable ``herdr``
|
|
11
|
+
binary (``HERDR_BIN_PATH``, else PATH) are present.
|
|
12
|
+
Reports are fire-and-forget CLI invocations — they must never block or
|
|
13
|
+
break the UI, and a missing/broken ``herdr`` binary is silently ignored.
|
|
14
|
+
|
|
15
|
+
``--source`` must stay stable and unique to this integration; ``--seq`` is
|
|
16
|
+
strictly increasing so Herdr can drop reports that arrive out of order
|
|
17
|
+
(turn-end and steer follow-up hand-offs race between workers).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
from collections.abc import Mapping
|
|
26
|
+
|
|
27
|
+
from limbo.integrations.base import AgentState
|
|
28
|
+
|
|
29
|
+
SOURCE = "custom:limbo"
|
|
30
|
+
AGENT = "limbo"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HerdrReporter:
|
|
34
|
+
"""Fire-and-forget lifecycle reporter bound to one Herdr pane."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, pane_id: str, bin_path: str):
|
|
37
|
+
self._pane_id = pane_id
|
|
38
|
+
self._bin_path = bin_path
|
|
39
|
+
self._seq = 0
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_env(cls, env: Mapping[str, str] | None = None) -> HerdrReporter | None:
|
|
43
|
+
"""Create a reporter when running inside a Herdr pane, else None.
|
|
44
|
+
|
|
45
|
+
``HERDR_BIN_PATH`` is preferred (portable across PATH differences)
|
|
46
|
+
but is not guaranteed to be set — fall back to a PATH lookup.
|
|
47
|
+
"""
|
|
48
|
+
env = os.environ if env is None else env
|
|
49
|
+
if env.get("HERDR_ENV") != "1":
|
|
50
|
+
return None
|
|
51
|
+
pane_id = env.get("HERDR_PANE_ID")
|
|
52
|
+
bin_path = env.get("HERDR_BIN_PATH") or shutil.which("herdr")
|
|
53
|
+
if not pane_id or not bin_path:
|
|
54
|
+
return None
|
|
55
|
+
return cls(pane_id, bin_path)
|
|
56
|
+
|
|
57
|
+
def report(
|
|
58
|
+
self,
|
|
59
|
+
state: AgentState,
|
|
60
|
+
*,
|
|
61
|
+
message: str | None = None,
|
|
62
|
+
session_id: str | None = None,
|
|
63
|
+
session_path: str | None = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Report lifecycle state (optionally with session identity)."""
|
|
66
|
+
cmd = [
|
|
67
|
+
self._bin_path,
|
|
68
|
+
"pane",
|
|
69
|
+
"report-agent",
|
|
70
|
+
self._pane_id,
|
|
71
|
+
"--source",
|
|
72
|
+
SOURCE,
|
|
73
|
+
"--agent",
|
|
74
|
+
AGENT,
|
|
75
|
+
"--state",
|
|
76
|
+
state,
|
|
77
|
+
"--seq",
|
|
78
|
+
str(self._next_seq()),
|
|
79
|
+
]
|
|
80
|
+
if message:
|
|
81
|
+
cmd += ["--message", message]
|
|
82
|
+
if session_id:
|
|
83
|
+
cmd += ["--agent-session-id", session_id]
|
|
84
|
+
if session_path:
|
|
85
|
+
cmd += ["--agent-session-path", session_path]
|
|
86
|
+
self._run(cmd)
|
|
87
|
+
|
|
88
|
+
def report_session(
|
|
89
|
+
self, session_id: str, session_path: str | None = None
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Report session identity when it changes independently of state
|
|
92
|
+
(``/new``, ``/sessions`` switch)."""
|
|
93
|
+
cmd = [
|
|
94
|
+
self._bin_path,
|
|
95
|
+
"pane",
|
|
96
|
+
"report-agent-session",
|
|
97
|
+
self._pane_id,
|
|
98
|
+
"--source",
|
|
99
|
+
SOURCE,
|
|
100
|
+
"--agent",
|
|
101
|
+
AGENT,
|
|
102
|
+
"--seq",
|
|
103
|
+
str(self._next_seq()),
|
|
104
|
+
"--agent-session-id",
|
|
105
|
+
session_id,
|
|
106
|
+
]
|
|
107
|
+
if session_path:
|
|
108
|
+
cmd += ["--agent-session-path", session_path]
|
|
109
|
+
self._run(cmd)
|
|
110
|
+
|
|
111
|
+
def release(self) -> None:
|
|
112
|
+
"""Release this source's lifecycle authority (on agent exit)."""
|
|
113
|
+
self._run(
|
|
114
|
+
[
|
|
115
|
+
self._bin_path,
|
|
116
|
+
"pane",
|
|
117
|
+
"release-agent",
|
|
118
|
+
self._pane_id,
|
|
119
|
+
"--source",
|
|
120
|
+
SOURCE,
|
|
121
|
+
"--agent",
|
|
122
|
+
AGENT,
|
|
123
|
+
]
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def _next_seq(self) -> int:
|
|
127
|
+
# Assigned synchronously before spawning, so sequence order matches
|
|
128
|
+
# call order even when deliveries race.
|
|
129
|
+
self._seq += 1
|
|
130
|
+
return self._seq
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def _run(cmd: list[str]) -> None:
|
|
134
|
+
try:
|
|
135
|
+
subprocess.Popen(
|
|
136
|
+
cmd,
|
|
137
|
+
stdin=subprocess.DEVNULL,
|
|
138
|
+
stdout=subprocess.DEVNULL,
|
|
139
|
+
stderr=subprocess.DEVNULL,
|
|
140
|
+
)
|
|
141
|
+
except OSError:
|
|
142
|
+
# A missing/broken herdr binary must never break the session.
|
|
143
|
+
pass
|
|
@@ -41,6 +41,7 @@ from limbo.goal_driver import (
|
|
|
41
41
|
GoalVerifyResultEvent,
|
|
42
42
|
GoalVerifyStarted,
|
|
43
43
|
)
|
|
44
|
+
from limbo.integrations import AgentState, create_reporters
|
|
44
45
|
from limbo.llm.client import LLMClient
|
|
45
46
|
from limbo.llm.factory import create_llm_client
|
|
46
47
|
from limbo.model_switch import (
|
|
@@ -111,6 +112,9 @@ class MainScreen(Screen[None]):
|
|
|
111
112
|
self._compact_busy = False
|
|
112
113
|
self._commands = SlashCommandRegistry()
|
|
113
114
|
self._register_builtin_commands()
|
|
115
|
+
# External tool integrations (Herdr, ...): an empty composite
|
|
116
|
+
# outside any integrated environment, so all calls are no-ops.
|
|
117
|
+
self._integrations = create_reporters()
|
|
114
118
|
|
|
115
119
|
@property
|
|
116
120
|
def _agent_busy(self) -> bool:
|
|
@@ -140,6 +144,20 @@ class MainScreen(Screen[None]):
|
|
|
140
144
|
resume=resume,
|
|
141
145
|
)
|
|
142
146
|
|
|
147
|
+
# -- External tool integrations (no-op outside integrated environments) --
|
|
148
|
+
|
|
149
|
+
def _report_state(
|
|
150
|
+
self, state: AgentState, *, message: str | None = None
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Report semantic lifecycle state to active integrations."""
|
|
153
|
+
self._integrations.report(state, message=message)
|
|
154
|
+
|
|
155
|
+
def _report_session(self) -> None:
|
|
156
|
+
"""Report the current session identity (startup, /new, /sessions)."""
|
|
157
|
+
self._integrations.report_session(
|
|
158
|
+
self.agent.session_id, str(self.agent.session_path)
|
|
159
|
+
)
|
|
160
|
+
|
|
143
161
|
def compose(self) -> ComposeResult:
|
|
144
162
|
yield StatusBar(
|
|
145
163
|
model=self.config.llm.model,
|
|
@@ -169,6 +187,8 @@ class MainScreen(Screen[None]):
|
|
|
169
187
|
)
|
|
170
188
|
self._refresh_goal_indicator() # D6: silent resume, badge only
|
|
171
189
|
self.query_one("#input", InputWidget).focus()
|
|
190
|
+
self._report_session()
|
|
191
|
+
self._report_state("idle")
|
|
172
192
|
|
|
173
193
|
# -- slash command menu ---------------------------------------------------
|
|
174
194
|
|
|
@@ -332,6 +352,7 @@ class MainScreen(Screen[None]):
|
|
|
332
352
|
self._pending_proposals = parsed or []
|
|
333
353
|
if not self._pending_proposals:
|
|
334
354
|
chat.add_info("未能解析模型的验收提议,请手动输入或跳过")
|
|
355
|
+
self._report_state("blocked", message="等待确认 /goal 验收命令")
|
|
335
356
|
self.app.push_screen(
|
|
336
357
|
VerifyPicker(self._pending_proposals), self._on_verify_picked
|
|
337
358
|
)
|
|
@@ -345,6 +366,7 @@ class MainScreen(Screen[None]):
|
|
|
345
366
|
if choice is None:
|
|
346
367
|
chat.add_info("已跳过自动验收(单轮模式);/goal clear 可退出 goal")
|
|
347
368
|
self._refresh_goal_indicator()
|
|
369
|
+
self._report_state("idle")
|
|
348
370
|
return
|
|
349
371
|
if choice == VERIFY_EDIT:
|
|
350
372
|
# Prefill the best proposal for editing; the next submission is
|
|
@@ -356,6 +378,7 @@ class MainScreen(Screen[None]):
|
|
|
356
378
|
input_widget.focus()
|
|
357
379
|
self._editing_verify = True
|
|
358
380
|
chat.add_info("编辑验收命令后回车确认(清空回车则跳过)")
|
|
381
|
+
self._report_state("blocked", message="等待编辑 /goal 验收命令")
|
|
359
382
|
return
|
|
360
383
|
self._accept_verify(choice)
|
|
361
384
|
|
|
@@ -364,6 +387,7 @@ class MainScreen(Screen[None]):
|
|
|
364
387
|
if is_dangerous(command, self.config.safety.dangerous_commands):
|
|
365
388
|
chat.add_error(f"验收命令命中危险命令过滤,已拒绝:{command}")
|
|
366
389
|
chat.add_info("goal 保持单轮模式;/goal clear 退出")
|
|
390
|
+
self._report_state("idle")
|
|
367
391
|
return
|
|
368
392
|
state = self.driver.set_verify(command)
|
|
369
393
|
if state is None:
|
|
@@ -484,6 +508,7 @@ class MainScreen(Screen[None]):
|
|
|
484
508
|
input_widget.disabled = True
|
|
485
509
|
self._compact_busy = True
|
|
486
510
|
statusbar.set_state("compacting…", "thinking")
|
|
511
|
+
self._report_state("working")
|
|
487
512
|
try:
|
|
488
513
|
async for event in self.agent.compact(trigger="manual"):
|
|
489
514
|
await self._process_agent_event(event)
|
|
@@ -492,6 +517,7 @@ class MainScreen(Screen[None]):
|
|
|
492
517
|
input_widget.disabled = False
|
|
493
518
|
input_widget.focus()
|
|
494
519
|
statusbar.set_state("idle")
|
|
520
|
+
self._report_state("idle")
|
|
495
521
|
|
|
496
522
|
def _show_help(self) -> None:
|
|
497
523
|
self.query_one("#chat", ChatWidget).add_info(self._commands.help_text())
|
|
@@ -549,6 +575,7 @@ class MainScreen(Screen[None]):
|
|
|
549
575
|
self.agent = self._new_agent()
|
|
550
576
|
chat.clear()
|
|
551
577
|
chat.add_info(f"已开始新会话 {self.agent.session_id}")
|
|
578
|
+
self._report_session()
|
|
552
579
|
|
|
553
580
|
def _find_skill(self, name: str) -> Skill | None:
|
|
554
581
|
if self._commands.get(f"/{name}") is not None:
|
|
@@ -602,6 +629,7 @@ class MainScreen(Screen[None]):
|
|
|
602
629
|
self._render_history()
|
|
603
630
|
meta = self.agent.session_meta
|
|
604
631
|
chat.add_info(f"已切换到会话 {meta.id} · {meta.title or '(无标题)'}")
|
|
632
|
+
self._report_session()
|
|
605
633
|
|
|
606
634
|
def _render_history(self) -> None:
|
|
607
635
|
"""Render the agent's restored history into the chat flow.
|
|
@@ -684,6 +712,7 @@ class MainScreen(Screen[None]):
|
|
|
684
712
|
async def on_unmount(self) -> None:
|
|
685
713
|
"""Close owned resources on shutdown: the LLM client's HTTP pool and
|
|
686
714
|
the agent's trace file handle."""
|
|
715
|
+
self._integrations.release()
|
|
687
716
|
self.agent.close()
|
|
688
717
|
close = getattr(self.llm_client, "close", None)
|
|
689
718
|
if close is not None:
|
|
@@ -707,6 +736,7 @@ class MainScreen(Screen[None]):
|
|
|
707
736
|
self._editing_verify = False
|
|
708
737
|
chat = self.query_one("#chat", ChatWidget)
|
|
709
738
|
if text.startswith("/"):
|
|
739
|
+
self._report_state("idle")
|
|
710
740
|
self._handle_command(text)
|
|
711
741
|
return
|
|
712
742
|
command = text.strip()
|
|
@@ -715,6 +745,7 @@ class MainScreen(Screen[None]):
|
|
|
715
745
|
self._accept_verify(command)
|
|
716
746
|
else:
|
|
717
747
|
chat.add_info("已跳过自动验收(单轮模式);/goal clear 可退出 goal")
|
|
748
|
+
self._report_state("idle")
|
|
718
749
|
return
|
|
719
750
|
if text.startswith("/"):
|
|
720
751
|
self._handle_command(text)
|
|
@@ -825,6 +856,7 @@ class MainScreen(Screen[None]):
|
|
|
825
856
|
# go to the steer queue instead of being blocked. Single-flight is
|
|
826
857
|
# owned by the goal driver (``running`` is set eagerly on call).
|
|
827
858
|
statusbar.set_state("thinking…", "thinking")
|
|
859
|
+
self._report_state("working")
|
|
828
860
|
# driver.run() sets ``running`` eagerly at call time, so the badge
|
|
829
861
|
# refresh below already sees the loop as executing (rainbow on).
|
|
830
862
|
events = self.driver.run(user_input, attachments)
|
|
@@ -858,6 +890,7 @@ class MainScreen(Screen[None]):
|
|
|
858
890
|
# the user can keep typing without clicking.
|
|
859
891
|
input_widget.focus()
|
|
860
892
|
statusbar.set_state("idle")
|
|
893
|
+
self._report_state("idle")
|
|
861
894
|
self._refresh_goal_indicator()
|
|
862
895
|
# M2: a proposal round that ends without leftover steers pops
|
|
863
896
|
# the verify-command confirmation picker.
|