limbo-code 0.1.12__tar.gz → 0.1.13__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.
Files changed (179) hide show
  1. {limbo_code-0.1.12 → limbo_code-0.1.13}/PKG-INFO +18 -1
  2. {limbo_code-0.1.12 → limbo_code-0.1.13}/README.md +17 -0
  3. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/app.py +18 -1
  4. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/config.py +34 -0
  5. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_cli.py +77 -8
  6. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_config.py +31 -1
  7. {limbo_code-0.1.12 → limbo_code-0.1.13}/.agents/skills/grill-with-docs/SKILL.md +0 -0
  8. {limbo_code-0.1.12 → limbo_code-0.1.13}/.agents/skills/grill-with-docs/agents/openai.yaml +0 -0
  9. {limbo_code-0.1.12 → limbo_code-0.1.13}/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +0 -0
  10. {limbo_code-0.1.12 → limbo_code-0.1.13}/.agents/skills/improve-codebase-architecture/SKILL.md +0 -0
  11. {limbo_code-0.1.12 → limbo_code-0.1.13}/.agents/skills/improve-codebase-architecture/agents/openai.yaml +0 -0
  12. {limbo_code-0.1.12 → limbo_code-0.1.13}/.agents/skills/tdd/SKILL.md +0 -0
  13. {limbo_code-0.1.12 → limbo_code-0.1.13}/.agents/skills/tdd/agents/openai.yaml +0 -0
  14. {limbo_code-0.1.12 → limbo_code-0.1.13}/.agents/skills/tdd/mocking.md +0 -0
  15. {limbo_code-0.1.12 → limbo_code-0.1.13}/.agents/skills/tdd/tests.md +0 -0
  16. {limbo_code-0.1.12 → limbo_code-0.1.13}/.github/workflows/publish.yml +0 -0
  17. {limbo_code-0.1.12 → limbo_code-0.1.13}/.github/workflows/test.yml +0 -0
  18. {limbo_code-0.1.12 → limbo_code-0.1.13}/.gitignore +0 -0
  19. {limbo_code-0.1.12 → limbo_code-0.1.13}/AGENTS.md +0 -0
  20. {limbo_code-0.1.12 → limbo_code-0.1.13}/CONTEXT.md +0 -0
  21. {limbo_code-0.1.12 → limbo_code-0.1.13}/Makefile +0 -0
  22. {limbo_code-0.1.12 → limbo_code-0.1.13}/design/confirm_view.html +0 -0
  23. {limbo_code-0.1.12 → limbo_code-0.1.13}/design/limbo-ui-minimal.md +0 -0
  24. {limbo_code-0.1.12 → limbo_code-0.1.13}/design/limbo-ui-redesign.md +0 -0
  25. {limbo_code-0.1.12 → limbo_code-0.1.13}/design/prototype-minimal-confirm.png +0 -0
  26. {limbo_code-0.1.12 → limbo_code-0.1.13}/design/prototype-minimal.html +0 -0
  27. {limbo_code-0.1.12 → limbo_code-0.1.13}/design/prototype-minimal.png +0 -0
  28. {limbo_code-0.1.12 → limbo_code-0.1.13}/design/prototype.html +0 -0
  29. {limbo_code-0.1.12 → limbo_code-0.1.13}/design/prototype.png +0 -0
  30. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/assets/limbo-current-ui.png +0 -0
  31. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/assets/limbo-new-ui.png +0 -0
  32. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/assets/walkthrough/limbo-dark/1_idle.svg +0 -0
  33. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/assets/walkthrough/limbo-dark/2_thinking_tool_running.svg +0 -0
  34. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/assets/walkthrough/limbo-dark/3_tool_success.svg +0 -0
  35. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/assets/walkthrough/limbo-dark/4_tool_error.svg +0 -0
  36. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/assets/walkthrough/limbo-dark/5_llm_error.svg +0 -0
  37. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/assets/walkthrough/limbo-dark/6_edit_diff.svg +0 -0
  38. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/assets/walkthrough/limbo-light/1_idle.svg +0 -0
  39. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/assets/walkthrough/limbo-light/2_thinking_tool_running.svg +0 -0
  40. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/assets/walkthrough/limbo-light/3_tool_success.svg +0 -0
  41. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/assets/walkthrough/limbo-light/4_tool_error.svg +0 -0
  42. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/assets/walkthrough/limbo-light/5_llm_error.svg +0 -0
  43. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/assets/walkthrough/limbo-light/6_edit_diff.svg +0 -0
  44. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/session-management.md +0 -0
  45. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/skills.md +0 -0
  46. {limbo_code-0.1.12 → limbo_code-0.1.13}/docs/ui-redesign-proposal.md +0 -0
  47. {limbo_code-0.1.12 → limbo_code-0.1.13}/pyproject.toml +0 -0
  48. {limbo_code-0.1.12 → limbo_code-0.1.13}/scripts/check_contrast.py +0 -0
  49. {limbo_code-0.1.12 → limbo_code-0.1.13}/scripts/gen_banner.py +0 -0
  50. {limbo_code-0.1.12 → limbo_code-0.1.13}/scripts/ui_walkthrough.py +0 -0
  51. {limbo_code-0.1.12 → limbo_code-0.1.13}/skills-lock.json +0 -0
  52. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/__init__.py +0 -0
  53. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/__main__.py +0 -0
  54. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/agent.py +0 -0
  55. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/attachments.py +0 -0
  56. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/btw.py +0 -0
  57. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/compaction.py +0 -0
  58. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/goal.py +0 -0
  59. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/goal_driver.py +0 -0
  60. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/history.py +0 -0
  61. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/llm/__init__.py +0 -0
  62. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/llm/anthropic_client.py +0 -0
  63. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/llm/catalog.py +0 -0
  64. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/llm/client.py +0 -0
  65. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/llm/factory.py +0 -0
  66. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/llm/openai_client.py +0 -0
  67. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/llm/responses_client.py +0 -0
  68. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/llm/retry.py +0 -0
  69. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/llm/scaffold.py +0 -0
  70. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/llm/sse.py +0 -0
  71. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/llm/usage.py +0 -0
  72. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/model_switch.py +0 -0
  73. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/models.py +0 -0
  74. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/prompt.py +0 -0
  75. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/sessions.py +0 -0
  76. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/skills.py +0 -0
  77. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/steer.py +0 -0
  78. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/tools/__init__.py +0 -0
  79. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/tools/base.py +0 -0
  80. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/tools/bash.py +0 -0
  81. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/tools/edit.py +0 -0
  82. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/tools/find.py +0 -0
  83. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/tools/grep.py +0 -0
  84. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/tools/ignore.py +0 -0
  85. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/tools/ls.py +0 -0
  86. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/tools/mutation_queue.py +0 -0
  87. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/tools/read.py +0 -0
  88. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/tools/registry.py +0 -0
  89. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/tools/write.py +0 -0
  90. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/trace.py +0 -0
  91. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/__init__.py +0 -0
  92. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/app.py +0 -0
  93. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/app.tcss +0 -0
  94. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/banner.py +0 -0
  95. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/clipboard.py +0 -0
  96. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/commands.py +0 -0
  97. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/contrast.py +0 -0
  98. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/screens/__init__.py +0 -0
  99. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/screens/btw.py +0 -0
  100. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/screens/game2048.py +0 -0
  101. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/screens/main.py +0 -0
  102. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/screens/model_picker.py +0 -0
  103. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/screens/session_picker.py +0 -0
  104. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/screens/verify_picker.py +0 -0
  105. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/syntax.py +0 -0
  106. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/theme.py +0 -0
  107. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/widgets/__init__.py +0 -0
  108. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/widgets/chat.py +0 -0
  109. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/widgets/command_menu.py +0 -0
  110. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/widgets/input.py +0 -0
  111. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/widgets/status_bar.py +0 -0
  112. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/ui/widgets/tool_card.py +0 -0
  113. {limbo_code-0.1.12 → limbo_code-0.1.13}/src/limbo/user_paths.py +0 -0
  114. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/conftest.py +0 -0
  115. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_agent.py +0 -0
  116. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_anthropic_client.py +0 -0
  117. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_attachments.py +0 -0
  118. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_btw.py +0 -0
  119. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_catalog.py +0 -0
  120. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_clipboard.py +0 -0
  121. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_compaction.py +0 -0
  122. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_goal.py +0 -0
  123. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_goal_driver.py +0 -0
  124. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_history.py +0 -0
  125. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_integration.py +0 -0
  126. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_interrupt.py +0 -0
  127. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_llm_client.py +0 -0
  128. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_llm_scaffold.py +0 -0
  129. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_model_switch.py +0 -0
  130. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_models.py +0 -0
  131. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_prompt.py +0 -0
  132. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_responses_client.py +0 -0
  133. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_retry.py +0 -0
  134. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_sessions.py +0 -0
  135. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_skills.py +0 -0
  136. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_sse.py +0 -0
  137. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_steer.py +0 -0
  138. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_trace.py +0 -0
  139. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_usage.py +0 -0
  140. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/test_user_paths.py +0 -0
  141. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/tools/test_base.py +0 -0
  142. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/tools/test_bash.py +0 -0
  143. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/tools/test_edit.py +0 -0
  144. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/tools/test_find.py +0 -0
  145. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/tools/test_grep.py +0 -0
  146. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/tools/test_ignore.py +0 -0
  147. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/tools/test_ls.py +0 -0
  148. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/tools/test_mutation_queue.py +0 -0
  149. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/tools/test_read.py +0 -0
  150. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/tools/test_registry.py +0 -0
  151. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/tools/test_write.py +0 -0
  152. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/__snapshots__/test_snapshots/test_snapshot_idle.raw +0 -0
  153. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/__snapshots__/test_snapshots/test_snapshot_session_picker.raw +0 -0
  154. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/__snapshots__/test_snapshots/test_snapshot_thinking_and_running_tool.raw +0 -0
  155. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/__snapshots__/test_snapshots/test_snapshot_tool_error_and_error_message.raw +0 -0
  156. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_app_smoke.py +0 -0
  157. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_btw_ui.py +0 -0
  158. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_command_menu.py +0 -0
  159. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_commands.py +0 -0
  160. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_compact_ui.py +0 -0
  161. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_contrast.py +0 -0
  162. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_copy.py +0 -0
  163. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_game2048.py +0 -0
  164. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_goal_ui.py +0 -0
  165. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_input_attachments.py +0 -0
  166. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_input_history.py +0 -0
  167. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_input_paste.py +0 -0
  168. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_interrupt_ui.py +0 -0
  169. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_main_screen.py +0 -0
  170. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_model_picker.py +0 -0
  171. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_scroll_follow.py +0 -0
  172. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_sessions_ui.py +0 -0
  173. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_skills_ui.py +0 -0
  174. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_snapshots.py +0 -0
  175. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_startup_art.py +0 -0
  176. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_steer_ui.py +0 -0
  177. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_theme.py +0 -0
  178. {limbo_code-0.1.12 → limbo_code-0.1.13}/tests/ui/test_widgets.py +0 -0
  179. {limbo_code-0.1.12 → limbo_code-0.1.13}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: limbo-code
3
- Version: 0.1.12
3
+ Version: 0.1.13
4
4
  Summary: A minimal terminal AI coding agent
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: httpx>=0.27
@@ -183,6 +183,23 @@ Optional tool settings:
183
183
  bash_enabled = true
184
184
  ```
185
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
+
186
203
  ### Session storage
187
204
 
188
205
  Conversations are saved as JSONL files in `~/.limbo/sessions/` so you can
@@ -169,6 +169,23 @@ Optional tool settings:
169
169
  bash_enabled = true
170
170
  ```
171
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
+
172
189
  ### Session storage
173
190
 
174
191
  Conversations are saved as JSONL files in `~/.limbo/sessions/` so you can
@@ -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):
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  import sys
6
7
  from pathlib import Path
7
8
  from unittest.mock import MagicMock
@@ -9,6 +10,7 @@ from unittest.mock import MagicMock
9
10
  import pytest
10
11
 
11
12
  import limbo.app as app_module
13
+ import limbo.ui.app as ui_app_module
12
14
  from limbo.app import main
13
15
  from limbo.config import Config
14
16
 
@@ -23,7 +25,7 @@ def fake_config(monkeypatch):
23
25
 
24
26
  def test_cli_passes_session_dir(fake_config, monkeypatch):
25
27
  mock_app_class = MagicMock()
26
- monkeypatch.setattr(app_module, "LimboApp", mock_app_class)
28
+ monkeypatch.setattr(ui_app_module, "LimboApp", mock_app_class)
27
29
  monkeypatch.setattr(
28
30
  sys,
29
31
  "argv",
@@ -40,7 +42,7 @@ def test_cli_passes_session_dir(fake_config, monkeypatch):
40
42
 
41
43
  def test_cli_defaults_session_dir_to_none(fake_config, monkeypatch):
42
44
  mock_app_class = MagicMock()
43
- monkeypatch.setattr(app_module, "LimboApp", mock_app_class)
45
+ monkeypatch.setattr(ui_app_module, "LimboApp", mock_app_class)
44
46
  monkeypatch.setattr(sys, "argv", ["limbo"])
45
47
 
46
48
  assert main() == 0
@@ -70,7 +72,7 @@ def test_cli_continue_resumes_latest_for_workdir(fake_config, monkeypatch, tmp_p
70
72
  path = _write_session(session_dir, "abc123", project)
71
73
 
72
74
  mock_app_class = MagicMock()
73
- monkeypatch.setattr(app_module, "LimboApp", mock_app_class)
75
+ monkeypatch.setattr(ui_app_module, "LimboApp", mock_app_class)
74
76
  monkeypatch.setattr(
75
77
  sys,
76
78
  "argv",
@@ -91,7 +93,7 @@ def test_cli_continue_resumes_latest_for_workdir(fake_config, monkeypatch, tmp_p
91
93
 
92
94
 
93
95
  def test_cli_continue_without_sessions_fails(fake_config, monkeypatch, tmp_path, capsys):
94
- monkeypatch.setattr(app_module, "LimboApp", MagicMock())
96
+ monkeypatch.setattr(ui_app_module, "LimboApp", MagicMock())
95
97
  monkeypatch.setattr(
96
98
  sys,
97
99
  "argv",
@@ -118,7 +120,7 @@ def test_cli_resume_by_id_prefix_switches_workdir(
118
120
  path = _write_session(session_dir, "def456", project)
119
121
 
120
122
  mock_app_class = MagicMock()
121
- monkeypatch.setattr(app_module, "LimboApp", mock_app_class)
123
+ monkeypatch.setattr(ui_app_module, "LimboApp", mock_app_class)
122
124
  monkeypatch.setattr(
123
125
  sys,
124
126
  "argv",
@@ -133,7 +135,7 @@ def test_cli_resume_by_id_prefix_switches_workdir(
133
135
 
134
136
 
135
137
  def test_cli_resume_unknown_id_fails(fake_config, monkeypatch, tmp_path, capsys):
136
- monkeypatch.setattr(app_module, "LimboApp", MagicMock())
138
+ monkeypatch.setattr(ui_app_module, "LimboApp", MagicMock())
137
139
  monkeypatch.setattr(
138
140
  sys,
139
141
  "argv",
@@ -151,7 +153,7 @@ def test_cli_resume_ambiguous_id_lists_candidates(
151
153
  _write_session(session_dir, "abc1", tmp_path)
152
154
  _write_session(session_dir, "abc2", tmp_path)
153
155
 
154
- monkeypatch.setattr(app_module, "LimboApp", MagicMock())
156
+ monkeypatch.setattr(ui_app_module, "LimboApp", MagicMock())
155
157
  monkeypatch.setattr(
156
158
  sys,
157
159
  "argv",
@@ -165,7 +167,7 @@ def test_cli_resume_ambiguous_id_lists_candidates(
165
167
 
166
168
  def test_cli_model_flag_overrides_config(fake_config, monkeypatch):
167
169
  mock_app_class = MagicMock()
168
- monkeypatch.setattr(app_module, "LimboApp", mock_app_class)
170
+ monkeypatch.setattr(ui_app_module, "LimboApp", mock_app_class)
169
171
  monkeypatch.setattr(sys, "argv", ["limbo", "--model", "glm-4.7"])
170
172
 
171
173
  assert main() == 0
@@ -173,3 +175,70 @@ def test_cli_model_flag_overrides_config(fake_config, monkeypatch):
173
175
  assert fake_config.llm.model == "glm-4.7"
174
176
  kwargs = mock_app_class.call_args.kwargs
175
177
  assert kwargs["config"].llm.model == "glm-4.7"
178
+
179
+
180
+ def _probe_kitty_env(home: Path, herdr: bool) -> str:
181
+ """Import limbo.app in a subprocess with a controlled HOME + HERDR_ENV and
182
+ report whether TEXTUAL_DISABLE_KITTY_KEY ended up set."""
183
+ import subprocess
184
+ import sys
185
+
186
+ probe = (
187
+ "import os, sys; "
188
+ "sys.argv = ['limbo']; "
189
+ "import limbo.app; "
190
+ "limbo.app.main(); "
191
+ "print(os.environ.get('TEXTUAL_DISABLE_KITTY_KEY', ''))"
192
+ )
193
+ env = os.environ.copy()
194
+ env["HOME"] = str(home)
195
+ env.pop("TEXTUAL_DISABLE_KITTY_KEY", None)
196
+ if herdr:
197
+ env["HERDR_ENV"] = "1"
198
+ else:
199
+ env.pop("HERDR_ENV", None)
200
+ run = subprocess.run(
201
+ [sys.executable, "-c", probe],
202
+ capture_output=True,
203
+ text=True,
204
+ env=env,
205
+ cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
206
+ )
207
+ assert run.returncode == 0, run.stderr
208
+ return run.stdout.strip()
209
+
210
+
211
+ def test_kitty_keyboard_config_drives_textual_env(tmp_path):
212
+ """The `ui.kitty_keyboard` config option decides Textual's
213
+ TEXTUAL_DISABLE_KITTY_KEY, not raw environment sniffing.
214
+
215
+ Without the workaround, Textual's kitty keyboard REPORT_ALL_KEYS request
216
+ makes herdr mirror report-all to the host terminal (Ghostty), and Ghostty
217
+ then encodes IME commits as the physical commit key (space) while dropping
218
+ the composed text — typing Chinese turns into spaces. Pasting is
219
+ unaffected because it bypasses the key encoder.
220
+ """
221
+ home = tmp_path / "home"
222
+ (home / ".limbo").mkdir(parents=True)
223
+ config = home / ".limbo" / "config.toml"
224
+
225
+ # auto (default) + inside herdr -> disable (the IME fix)
226
+ config.write_text('[ui]\nkitty_keyboard = "auto"\n')
227
+ assert _probe_kitty_env(home, herdr=True) == "1"
228
+ # auto + direct terminal -> keep the kitty keyboard protocol
229
+ assert _probe_kitty_env(home, herdr=False) == ""
230
+ # enabled + herdr -> user opted out of the workaround
231
+ config.write_text('[ui]\nkitty_keyboard = "enabled"\n')
232
+ assert _probe_kitty_env(home, herdr=True) == ""
233
+ # disabled everywhere
234
+ config.write_text('[ui]\nkitty_keyboard = "disabled"\n')
235
+ assert _probe_kitty_env(home, herdr=False) == "1"
236
+
237
+
238
+ def test_kitty_keyboard_config_defaults_to_auto(tmp_path):
239
+ """Without a config file the default is `auto`: inside herdr the
240
+ workaround is on, elsewhere the kitty keyboard protocol stays enabled."""
241
+ home = tmp_path / "home"
242
+ (home / ".limbo").mkdir(parents=True)
243
+ assert _probe_kitty_env(home, herdr=True) == "1"
244
+ assert _probe_kitty_env(home, herdr=False) == ""
@@ -4,7 +4,12 @@ from pathlib import Path
4
4
 
5
5
  import pytest
6
6
 
7
- from limbo.config import Config, load_config, save_model_to_config
7
+ from limbo.config import (
8
+ Config,
9
+ KittyKeyboardMode,
10
+ load_config,
11
+ save_model_to_config,
12
+ )
8
13
 
9
14
 
10
15
  def test_default_config():
@@ -218,3 +223,28 @@ def test_save_model_unwritable_path_returns_false(tmp_path):
218
223
  assert save_model_to_config("glm-4.7", path) is False
219
224
  finally:
220
225
  os.chmod(tmp_path, 0o700)
226
+
227
+
228
+ def test_kitty_keyboard_default_is_auto():
229
+ cfg = Config()
230
+ assert cfg.ui.kitty_keyboard == KittyKeyboardMode.AUTO
231
+
232
+
233
+ def test_kitty_keyboard_mode_from_toml(tmp_path):
234
+ path = tmp_path / "kitty.toml"
235
+ path.write_text('[ui]\nkitty_keyboard = "disabled"\n')
236
+ assert load_config(path).ui.kitty_keyboard is KittyKeyboardMode.DISABLED
237
+
238
+ path.write_text('[ui]\nkitty_keyboard = "enabled"\n')
239
+ assert load_config(path).ui.kitty_keyboard is KittyKeyboardMode.ENABLED
240
+
241
+
242
+ def test_kitty_keyboard_mode_disable_decision():
243
+ # auto: disable only inside the herdr multiplexer (IME fix), keep the
244
+ # kitty keyboard protocol everywhere else.
245
+ assert KittyKeyboardMode.AUTO.disable_textual_kitty_key(in_herdr=True)
246
+ assert not KittyKeyboardMode.AUTO.disable_textual_kitty_key(in_herdr=False)
247
+ # enabled: never disable (opt out of the workaround).
248
+ assert not KittyKeyboardMode.ENABLED.disable_textual_kitty_key(in_herdr=True)
249
+ # disabled: always force raw/legacy input.
250
+ assert KittyKeyboardMode.DISABLED.disable_textual_kitty_key(in_herdr=False)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes