limbo-code 0.1.0__tar.gz → 0.1__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 (168) hide show
  1. {limbo_code-0.1.0 → limbo_code-0.1}/.github/workflows/test.yml +9 -3
  2. {limbo_code-0.1.0 → limbo_code-0.1}/.gitignore +1 -0
  3. {limbo_code-0.1.0 → limbo_code-0.1}/AGENTS.md +33 -9
  4. limbo_code-0.1/PKG-INFO +267 -0
  5. {limbo_code-0.1.0 → limbo_code-0.1}/README.md +93 -2
  6. limbo_code-0.1/docs/assets/walkthrough/limbo-dark/1_idle.svg +165 -0
  7. limbo_code-0.1/docs/assets/walkthrough/limbo-dark/2_thinking_tool_running.svg +166 -0
  8. limbo_code-0.1/docs/assets/walkthrough/limbo-dark/3_tool_success.svg +167 -0
  9. limbo_code-0.1/docs/assets/walkthrough/limbo-dark/4_tool_error.svg +168 -0
  10. limbo_code-0.1/docs/assets/walkthrough/limbo-dark/5_llm_error.svg +166 -0
  11. limbo_code-0.1/docs/assets/walkthrough/limbo-dark/6_edit_diff.svg +168 -0
  12. limbo_code-0.1/docs/assets/walkthrough/limbo-light/1_idle.svg +166 -0
  13. limbo_code-0.1/docs/assets/walkthrough/limbo-light/2_thinking_tool_running.svg +167 -0
  14. limbo_code-0.1/docs/assets/walkthrough/limbo-light/3_tool_success.svg +168 -0
  15. limbo_code-0.1/docs/assets/walkthrough/limbo-light/4_tool_error.svg +169 -0
  16. limbo_code-0.1/docs/assets/walkthrough/limbo-light/5_llm_error.svg +167 -0
  17. limbo_code-0.1/docs/assets/walkthrough/limbo-light/6_edit_diff.svg +169 -0
  18. {limbo_code-0.1.0 → limbo_code-0.1}/docs/skills.md +14 -2
  19. {limbo_code-0.1.0 → limbo_code-0.1}/pyproject.toml +16 -3
  20. limbo_code-0.1/scripts/check_contrast.py +28 -0
  21. limbo_code-0.1/scripts/gen_banner.py +103 -0
  22. limbo_code-0.1/scripts/ui_walkthrough.py +130 -0
  23. limbo_code-0.1/src/limbo/__init__.py +8 -0
  24. limbo_code-0.1/src/limbo/agent.py +1113 -0
  25. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/app.py +12 -8
  26. limbo_code-0.1/src/limbo/compaction.py +189 -0
  27. limbo_code-0.1/src/limbo/config.py +256 -0
  28. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/history.py +0 -19
  29. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/llm/anthropic_client.py +89 -36
  30. limbo_code-0.1/src/limbo/llm/catalog.py +396 -0
  31. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/llm/client.py +6 -0
  32. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/llm/factory.py +3 -0
  33. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/llm/openai_client.py +98 -22
  34. limbo_code-0.1/src/limbo/llm/responses_client.py +409 -0
  35. limbo_code-0.1/src/limbo/llm/retry.py +203 -0
  36. limbo_code-0.1/src/limbo/llm/sse.py +31 -0
  37. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/models.py +36 -2
  38. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/sessions.py +96 -6
  39. limbo_code-0.1/src/limbo/skills.py +190 -0
  40. limbo_code-0.1/src/limbo/tools/base.py +294 -0
  41. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/tools/bash.py +39 -7
  42. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/tools/edit.py +23 -19
  43. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/tools/find.py +12 -6
  44. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/tools/grep.py +18 -10
  45. limbo_code-0.1/src/limbo/tools/mutation_queue.py +36 -0
  46. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/tools/read.py +56 -18
  47. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/tools/registry.py +35 -2
  48. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/tools/write.py +7 -5
  49. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/ui/app.py +17 -6
  50. limbo_code-0.1/src/limbo/ui/app.tcss +316 -0
  51. limbo_code-0.1/src/limbo/ui/banner.py +117 -0
  52. limbo_code-0.1/src/limbo/ui/clipboard.py +314 -0
  53. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/ui/commands.py +4 -0
  54. limbo_code-0.1/src/limbo/ui/contrast.py +139 -0
  55. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/ui/screens/main.py +296 -16
  56. limbo_code-0.1/src/limbo/ui/screens/model_picker.py +122 -0
  57. limbo_code-0.1/src/limbo/ui/syntax.py +84 -0
  58. limbo_code-0.1/src/limbo/ui/theme.py +101 -0
  59. limbo_code-0.1/src/limbo/ui/widgets/chat.py +320 -0
  60. limbo_code-0.1/src/limbo/ui/widgets/input.py +409 -0
  61. limbo_code-0.1/src/limbo/ui/widgets/status_bar.py +116 -0
  62. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/ui/widgets/tool_card.py +18 -8
  63. limbo_code-0.1/src/limbo/user_paths.py +82 -0
  64. limbo_code-0.1/tests/test_agent.py +1965 -0
  65. {limbo_code-0.1.0 → limbo_code-0.1}/tests/test_anthropic_client.py +277 -2
  66. limbo_code-0.1/tests/test_catalog.py +293 -0
  67. {limbo_code-0.1.0 → limbo_code-0.1}/tests/test_cli.py +12 -0
  68. limbo_code-0.1/tests/test_clipboard.py +205 -0
  69. limbo_code-0.1/tests/test_compaction.py +181 -0
  70. limbo_code-0.1/tests/test_config.py +220 -0
  71. {limbo_code-0.1.0 → limbo_code-0.1}/tests/test_history.py +0 -13
  72. {limbo_code-0.1.0 → limbo_code-0.1}/tests/test_llm_client.py +276 -2
  73. limbo_code-0.1/tests/test_responses_client.py +582 -0
  74. limbo_code-0.1/tests/test_retry.py +306 -0
  75. {limbo_code-0.1.0 → limbo_code-0.1}/tests/test_sessions.py +181 -4
  76. limbo_code-0.1/tests/test_skills.py +187 -0
  77. limbo_code-0.1/tests/test_user_paths.py +87 -0
  78. limbo_code-0.1/tests/tools/test_base.py +244 -0
  79. {limbo_code-0.1.0 → limbo_code-0.1}/tests/tools/test_bash.py +66 -6
  80. {limbo_code-0.1.0 → limbo_code-0.1}/tests/tools/test_find.py +20 -0
  81. limbo_code-0.1/tests/tools/test_mutation_queue.py +47 -0
  82. {limbo_code-0.1.0 → limbo_code-0.1}/tests/tools/test_read.py +89 -2
  83. {limbo_code-0.1.0 → limbo_code-0.1}/tests/tools/test_registry.py +32 -0
  84. limbo_code-0.1/tests/ui/__snapshots__/test_snapshots/test_snapshot_idle.raw +172 -0
  85. limbo_code-0.1/tests/ui/__snapshots__/test_snapshots/test_snapshot_session_picker.raw +168 -0
  86. limbo_code-0.1/tests/ui/__snapshots__/test_snapshots/test_snapshot_thinking_and_running_tool.raw +174 -0
  87. limbo_code-0.1/tests/ui/__snapshots__/test_snapshots/test_snapshot_tool_error_and_error_message.raw +175 -0
  88. limbo_code-0.1/tests/ui/test_compact_ui.py +188 -0
  89. limbo_code-0.1/tests/ui/test_contrast.py +41 -0
  90. limbo_code-0.1/tests/ui/test_input_attachments.py +164 -0
  91. limbo_code-0.1/tests/ui/test_input_paste.py +314 -0
  92. {limbo_code-0.1.0 → limbo_code-0.1}/tests/ui/test_main_screen.py +74 -2
  93. limbo_code-0.1/tests/ui/test_model_picker.py +393 -0
  94. limbo_code-0.1/tests/ui/test_scroll_follow.py +85 -0
  95. limbo_code-0.1/tests/ui/test_snapshots.py +136 -0
  96. limbo_code-0.1/tests/ui/test_startup_art.py +137 -0
  97. limbo_code-0.1/tests/ui/test_steer_ui.py +532 -0
  98. limbo_code-0.1/tests/ui/test_theme.py +104 -0
  99. limbo_code-0.1/uv.lock +936 -0
  100. limbo_code-0.1.0/PKG-INFO +0 -16
  101. limbo_code-0.1.0/src/limbo/__init__.py +0 -3
  102. limbo_code-0.1.0/src/limbo/agent.py +0 -456
  103. limbo_code-0.1.0/src/limbo/config.py +0 -99
  104. limbo_code-0.1.0/src/limbo/llm/catalog.py +0 -234
  105. limbo_code-0.1.0/src/limbo/skills.py +0 -89
  106. limbo_code-0.1.0/src/limbo/tools/base.py +0 -102
  107. limbo_code-0.1.0/src/limbo/ui/app.tcss +0 -174
  108. limbo_code-0.1.0/src/limbo/ui/banner.py +0 -83
  109. limbo_code-0.1.0/src/limbo/ui/widgets/chat.py +0 -149
  110. limbo_code-0.1.0/src/limbo/ui/widgets/input.py +0 -199
  111. limbo_code-0.1.0/src/limbo/ui/widgets/status_bar.py +0 -32
  112. limbo_code-0.1.0/tests/test_agent.py +0 -729
  113. limbo_code-0.1.0/tests/test_catalog.py +0 -160
  114. limbo_code-0.1.0/tests/test_config.py +0 -83
  115. limbo_code-0.1.0/tests/test_skills.py +0 -89
  116. limbo_code-0.1.0/tests/tools/test_base.py +0 -85
  117. limbo_code-0.1.0/tests/ui/test_startup_art.py +0 -94
  118. {limbo_code-0.1.0 → limbo_code-0.1}/.agents/skills/grill-with-docs/SKILL.md +0 -0
  119. {limbo_code-0.1.0 → limbo_code-0.1}/.agents/skills/grill-with-docs/agents/openai.yaml +0 -0
  120. {limbo_code-0.1.0 → limbo_code-0.1}/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +0 -0
  121. {limbo_code-0.1.0 → limbo_code-0.1}/.agents/skills/improve-codebase-architecture/SKILL.md +0 -0
  122. {limbo_code-0.1.0 → limbo_code-0.1}/.agents/skills/improve-codebase-architecture/agents/openai.yaml +0 -0
  123. {limbo_code-0.1.0 → limbo_code-0.1}/.agents/skills/tdd/SKILL.md +0 -0
  124. {limbo_code-0.1.0 → limbo_code-0.1}/.agents/skills/tdd/agents/openai.yaml +0 -0
  125. {limbo_code-0.1.0 → limbo_code-0.1}/.agents/skills/tdd/mocking.md +0 -0
  126. {limbo_code-0.1.0 → limbo_code-0.1}/.agents/skills/tdd/tests.md +0 -0
  127. {limbo_code-0.1.0 → limbo_code-0.1}/.github/workflows/publish.yml +0 -0
  128. {limbo_code-0.1.0 → limbo_code-0.1}/design/confirm_view.html +0 -0
  129. {limbo_code-0.1.0 → limbo_code-0.1}/design/limbo-ui-minimal.md +0 -0
  130. {limbo_code-0.1.0 → limbo_code-0.1}/design/limbo-ui-redesign.md +0 -0
  131. {limbo_code-0.1.0 → limbo_code-0.1}/design/prototype-minimal-confirm.png +0 -0
  132. {limbo_code-0.1.0 → limbo_code-0.1}/design/prototype-minimal.html +0 -0
  133. {limbo_code-0.1.0 → limbo_code-0.1}/design/prototype-minimal.png +0 -0
  134. {limbo_code-0.1.0 → limbo_code-0.1}/design/prototype.html +0 -0
  135. {limbo_code-0.1.0 → limbo_code-0.1}/design/prototype.png +0 -0
  136. {limbo_code-0.1.0 → limbo_code-0.1}/docs/assets/limbo-current-ui.png +0 -0
  137. {limbo_code-0.1.0 → limbo_code-0.1}/docs/assets/limbo-new-ui.png +0 -0
  138. {limbo_code-0.1.0 → limbo_code-0.1}/docs/session-management.md +0 -0
  139. {limbo_code-0.1.0 → limbo_code-0.1}/docs/ui-redesign-proposal.md +0 -0
  140. {limbo_code-0.1.0 → limbo_code-0.1}/skills-lock.json +0 -0
  141. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/__main__.py +0 -0
  142. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/llm/__init__.py +0 -0
  143. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/tools/__init__.py +0 -0
  144. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/tools/ignore.py +0 -0
  145. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/tools/ls.py +0 -0
  146. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/trace.py +0 -0
  147. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/ui/__init__.py +0 -0
  148. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/ui/screens/__init__.py +0 -0
  149. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/ui/screens/game2048.py +0 -0
  150. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/ui/screens/session_picker.py +0 -0
  151. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/ui/widgets/__init__.py +0 -0
  152. {limbo_code-0.1.0 → limbo_code-0.1}/src/limbo/ui/widgets/command_menu.py +0 -0
  153. {limbo_code-0.1.0 → limbo_code-0.1}/tests/test_integration.py +0 -0
  154. {limbo_code-0.1.0 → limbo_code-0.1}/tests/test_models.py +0 -0
  155. {limbo_code-0.1.0 → limbo_code-0.1}/tests/test_trace.py +0 -0
  156. {limbo_code-0.1.0 → limbo_code-0.1}/tests/tools/test_edit.py +0 -0
  157. {limbo_code-0.1.0 → limbo_code-0.1}/tests/tools/test_grep.py +0 -0
  158. {limbo_code-0.1.0 → limbo_code-0.1}/tests/tools/test_ignore.py +0 -0
  159. {limbo_code-0.1.0 → limbo_code-0.1}/tests/tools/test_ls.py +0 -0
  160. {limbo_code-0.1.0 → limbo_code-0.1}/tests/tools/test_write.py +0 -0
  161. {limbo_code-0.1.0 → limbo_code-0.1}/tests/ui/test_app_smoke.py +0 -0
  162. {limbo_code-0.1.0 → limbo_code-0.1}/tests/ui/test_command_menu.py +0 -0
  163. {limbo_code-0.1.0 → limbo_code-0.1}/tests/ui/test_commands.py +0 -0
  164. {limbo_code-0.1.0 → limbo_code-0.1}/tests/ui/test_game2048.py +0 -0
  165. {limbo_code-0.1.0 → limbo_code-0.1}/tests/ui/test_input_history.py +0 -0
  166. {limbo_code-0.1.0 → limbo_code-0.1}/tests/ui/test_sessions_ui.py +0 -0
  167. {limbo_code-0.1.0 → limbo_code-0.1}/tests/ui/test_skills_ui.py +0 -0
  168. {limbo_code-0.1.0 → limbo_code-0.1}/tests/ui/test_widgets.py +0 -0
@@ -17,14 +17,20 @@ jobs:
17
17
  steps:
18
18
  - uses: actions/checkout@v4
19
19
 
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v5
22
+
20
23
  - name: Set up Python
21
24
  uses: actions/setup-python@v5
22
25
  with:
23
26
  python-version: ${{ matrix.python-version }}
24
- cache: pip
25
27
 
28
+ # Install from uv.lock so CI renders snapshots with the exact same
29
+ # textual/rich/syrupy versions the baselines were generated with.
26
30
  - name: Install dependencies
27
- run: pip install -e ".[dev]"
31
+ run: uv sync --frozen --extra dev
32
+ env:
33
+ UV_PYTHON: ${{ matrix.python-version }}
28
34
 
29
35
  - name: Run tests
30
- run: python -m pytest -v
36
+ run: uv run python -m pytest -v
@@ -11,3 +11,4 @@ venv/
11
11
  dist/
12
12
  build/
13
13
  .pi-subagents/
14
+ snapshot_report.html
@@ -44,17 +44,25 @@ src/limbo/
44
44
  └── ui/
45
45
  ├── app.py # Textual App subclass (CSS_PATH = app.tcss)
46
46
  ├── app.tcss # Centralized stylesheet for the whole TUI
47
+ ├── theme.py # limbo-dark / limbo-light Theme definitions (RFC LIM-16 palette)
48
+ ├── syntax.py # Pygments styles matching the theme palette (tool-card highlighting)
49
+ ├── contrast.py # WCAG contrast checker over the theme palettes
47
50
  ├── commands.py # SlashCommandRegistry: menu metadata + dispatch in one place
48
51
  ├── screens/
49
52
  │ └── main.py # Single-column chat screen + event handling + slash commands
50
53
  │ └── session_picker.py # Modal session switcher (/sessions)
54
+ │ └── model_picker.py # Modal model switcher (/model), grouped by provider
51
55
  └── widgets/
52
- ├── chat.py # Chat flow: user/assistant(Markdown)/tool cards/errors
56
+ ├── chat.py # Chat flow: user/assistant(Markdown)/tool cards/errors + scroll-follow
53
57
  ├── input.py # Multi-line user input
54
- ├── status_bar.py # Top status bar: agent state + model/workdir
58
+ ├── status_bar.py # Top status bar: spinner state + elapsed + tokens + model/workdir
55
59
  ├── tool_card.py # Inline tool-call card (one-line summary, expandable)
56
60
  └── confirm.py # Confirmation modal (ConfirmDialog, Confirmed/Rejected events)
57
61
 
62
+ scripts/
63
+ ├── check_contrast.py # Palette contrast report/CI gate (P2-3)
64
+ └── ui_walkthrough.py # Render 5 key UI states to SVG for visual review (§7.4)
65
+
58
66
  tests/
59
67
  ├── test_models.py
60
68
  ├── test_config.py
@@ -85,8 +93,8 @@ tests/
85
93
 
86
94
  ## AgentLoop Details
87
95
 
88
- - `Agent.run()` yields `AgentEvent` types: `TextDelta`, `ToolCallRequest`, `ToolResultEvent`, `ErrorEvent`
89
- - The loop respects `config.llm.max_iterations` (default 10)
96
+ - `Agent.run()` yields `AgentEvent` types: `TextDelta`, `ThinkingDelta`, `ToolCallRequest`, `ToolResultEvent`, `ErrorEvent`, `UsageUpdate` (cumulative session token count, emitted after each LLM call)
97
+ - The loop respects `config.llm.max_iterations` (default 50)
90
98
  - Multi-tool calls in a single assistant turn are executed sequentially
91
99
  - When a tool requires confirmation, placeholder `role="tool"` messages are inserted for all remaining calls in that turn so the message history stays valid for the OpenAI API
92
100
  - After a confirmed tool is applied, `Agent.continue_after_confirmation()` resumes the loop, first executing remaining placeholders, then returning to the LLM
@@ -123,36 +131,48 @@ api_key = "..." # Required
123
131
  model = "deepseek-chat" # Default
124
132
  base_url = "https://api.deepseek.com/v1"
125
133
  temperature = 0.2
126
- max_iterations = 10
134
+ max_iterations = 50
127
135
 
128
136
  [tools]
129
137
  bash_enabled = true
130
138
 
131
139
  [ui]
132
- theme = "textual-dark" # optional Textual built-in theme name
140
+ theme = "limbo-dark" # optional: limbo-dark (default) / limbo-light / any Textual built-in
141
+ show_banner = true # startup ASCII art on fresh sessions
133
142
 
134
143
  [safety]
135
144
  dangerous_commands = ["rm", "git reset --hard"]
136
145
  sensitive_files = [".env", "id_rsa", "id_ed25519", ".ssh"]
146
+
147
+ [providers.glm] # optional per-provider override (base_url / api_key /
148
+ base_url = "..." # api_key_env / headers); beats the global [llm] values
137
149
  ```
138
150
 
139
151
  ## UI Layout
140
152
 
141
153
  Pi-style single-column layout (top to bottom):
142
- - **Status bar (1 line)**: Agent state on the left (`● idle / thinking… / running <tool>…`), model + workdir on the right
154
+ - **Status bar (1 line)**: Agent state on the left (`● idle`, or animated `⠋ thinking… / running <tool>…` with elapsed seconds), model + cumulative tokens + workdir on the right
143
155
  - **Chat flow**: The conversation as a single scrolling stream — user messages (`❯` prefix), assistant replies rendered as streaming Markdown, inline tool-call cards (`✓/⏸/✗` one-line summaries, click or `ctrl+o` to expand full output), error lines
144
156
  - **Input box**: The only persistent rounded border on screen; Enter submits, Shift+Enter inserts a newline
145
157
  - **Hint line (1 line)**: Key hints in muted color
146
158
 
147
159
  Confirmation modal: `ConfirmDialog` shows tool output with `y`/`n`/`Esc` shortcuts and "Apply"/"Reject" buttons.
148
160
 
149
- All styles live in `ui/app.tcss`; widgets do not define `DEFAULT_CSS`. The theme is configurable via `[ui] theme` in `config.toml` (Textual built-in theme names).
161
+ All styles live in `ui/app.tcss`; widgets do not define `DEFAULT_CSS`. Colors must
162
+ reference theme semantic variables only (no bare hex; 2048 tiles and the mascot
163
+ banner art are exempt). The palette and per-state color rules are defined in
164
+ `ui/theme.py` (RFC LIM-16): `limbo-dark` is the default, `limbo-light` is also
165
+ built in, and `[ui] theme` can select any Textual built-in theme — custom CSS
166
+ variables fall back to the limbo-dark values via `App.get_theme_variable_defaults()`.
167
+ Run `python scripts/check_contrast.py` after touching the palette: all *used*
168
+ text/background pairs must stay ≥ 4.5:1.
150
169
 
151
170
  ## Key Design Decisions
152
171
 
153
172
  - **No provider field**: Provider is inferred from `base_url`, `model`, `api_key`
154
173
  - **Async Agent, sync tools**: Tools run via `asyncio.to_thread()` — they are synchronous by default but executed in a thread pool
155
174
  - **Session persistence**: Full conversation history rewritten on each save (safe for MVP-scale conversations). Sessions are resumable — see [docs/session-management.md](./docs/session-management.md): `limbo --continue` / `--resume <id>`, and in-TUI `/sessions`, `/new`, `/export` commands
175
+ - **Runtime model switching**: `/model` (picker or direct arg) rebuilds the LLM client mid-session — `MainScreen` sets `config.llm.model` first, then `agent.update_llm()` re-resolves the spec (context window, vision gate, compaction budget); the choice is written back to `config.toml` via tomlkit (comment-preserving, `save_model_to_config`). Busy turns refuse the switch
156
176
  - **System message**: Built into `Agent._init_system_message()` — tool descriptions and guidelines are hardcoded
157
177
  - **Bash safety filter**: Heuristic only — tokenizes the command, checks against pattern list. Known bypass vectors (subshells, command substitution, variable indirection) are documented.
158
178
 
@@ -162,9 +182,13 @@ All styles live in `ui/app.tcss`; widgets do not define `DEFAULT_CSS`. The theme
162
182
  pytest tests/ -v # all tests
163
183
  ruff check src tests # lint
164
184
  mypy src # type check (strict=false)
185
+ python scripts/check_contrast.py # palette WCAG contrast gate
186
+ python scripts/ui_walkthrough.py # export key UI states as SVGs to docs/assets/walkthrough/
165
187
  ```
166
188
 
167
- Tests use `pytest-asyncio` for async tests, `respx` for HTTP mocking (LLM client), and temp directories for tool tests.
189
+ Tests use `pytest-asyncio` for async tests, `respx` for HTTP mocking (LLM client),
190
+ temp directories for tool tests, and `pytest-textual-snapshot` for UI snapshots
191
+ (`pytest --snapshot-update` after intentional visual changes; review the SVG diff).
168
192
 
169
193
  ## Common Patterns
170
194
 
@@ -0,0 +1,267 @@
1
+ Metadata-Version: 2.4
2
+ Name: limbo-code
3
+ Version: 0.1
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
+ Provides-Extra: dev
14
+ Requires-Dist: mypy>=1.10; extra == 'dev'
15
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
16
+ Requires-Dist: pytest-textual-snapshot>=1.0; extra == 'dev'
17
+ Requires-Dist: pytest>=8.0; extra == 'dev'
18
+ Requires-Dist: respx>=0.21; extra == 'dev'
19
+ Requires-Dist: ruff>=0.4; extra == 'dev'
20
+ Requires-Dist: types-pygments>=2.17; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Limbo
24
+
25
+ A minimal terminal AI coding agent.
26
+
27
+ ## Install
28
+
29
+ Requires Python 3.11 or later.
30
+
31
+ ```bash
32
+ pip install -e .
33
+ ```
34
+
35
+ ## Configure
36
+
37
+ Create `~/.limbo/config.toml`:
38
+
39
+ ```toml
40
+ [llm]
41
+ api_key = "your-api-key"
42
+ model = "deepseek-chat"
43
+ base_url = "https://api.deepseek.com/v1"
44
+ ```
45
+
46
+ Limbo speaks to LLMs through a **provider/model catalog**
47
+ (`src/limbo/llm/catalog.py`). Each provider declares its API dialect,
48
+ endpoint, and credential env var; each model carries its own context window,
49
+ max output tokens, and thinking/reasoning behavior. A client factory
50
+ (`src/limbo/llm/factory.py`) picks the client implementation from the
51
+ provider's API dialect, so non-OpenAI dialects can be added without touching
52
+ call sites. Models not in the catalog fall back to generic
53
+ OpenAI-compatible defaults driven by `base_url` + `model` + `api_key`.
54
+
55
+ Built-in providers:
56
+
57
+ | Provider | API dialect | Endpoint | Key env var |
58
+ |----------|-------------|----------|-------------|
59
+ | `deepseek` | openai-completions | `https://api.deepseek.com/v1` | `DEEPSEEK_API_KEY` |
60
+ | `moonshotai` (Kimi) | openai-completions | `https://api.moonshot.ai/v1` | `MOONSHOT_API_KEY` |
61
+ | `kimi-coding` (Kimi For Coding) | anthropic-messages | `https://api.kimi.com/coding` | `KIMI_API_KEY` |
62
+ | `glm` (GLM Coding Plan) | openai-completions | `https://open.bigmodel.cn/api/coding/paas/v4` | `ZHIPUAI_API_KEY` |
63
+ | `codex` (OpenAI Codex) | openai-responses | `https://api.openai.com/v1` (override with your relay) | `CODEX_API_KEY` |
64
+
65
+ Built-in Codex models include `gpt-5.5`, `gpt-5.6-sol`/`terra`/`luna`,
66
+ `gpt-5.4`, `gpt-5.4-mini`, and `gpt-5.3-codex-spark`. **`gpt-5.5` is
67
+ verified by live test against the target relay** (its 1M context is
68
+ relay-reported); the other entries mirror pi's built-in catalog and are
69
+ unverified — availability depends on the relay in use, and unknown or
70
+ unsupported models fall back to the generic OpenAI-compatible defaults.
71
+ Codex speaks the
72
+ OpenAI **Responses API** (`POST {base_url}/responses`), served by a
73
+ dedicated client (`src/limbo/llm/responses_client.py`, plain httpx SSE):
74
+ system messages become `instructions`, tool definitions are flattened, and
75
+ reasoning models stream `reasoning` summaries and replay encrypted
76
+ reasoning items across turns. Limbo targets **API-key relays**, not the
77
+ ChatGPT subscription backend (OAuth) — point the provider at your relay
78
+ and make sure its model IDs match the catalog:
79
+
80
+ ```toml
81
+ [llm]
82
+ model = "gpt-5.5"
83
+
84
+ [providers.codex]
85
+ base_url = "https://my-relay.example.com/v1"
86
+ api_key_env = "CODEX_API_KEY"
87
+ ```
88
+
89
+ Built-in GLM Coding Plan models: `glm-4.7`, `glm-5.1`, `glm-5.2` (1M
90
+ context), `glm-5-turbo`, `glm-5v-turbo` (vision), and `glm-4.5-air`. The
91
+ Coding Plan is a subscription with its own endpoint and keys — a coding-plan
92
+ key only works on the `/api/coding/paas/v4` endpoints (not the pay-per-token
93
+ `/api/paas/v4` ones) and vice versa. For the international endpoint set
94
+ `[providers.glm] base_url = "https://api.z.ai/api/coding/paas/v4"` (see
95
+ below).
96
+
97
+ Built-in Kimi models include `kimi-k3` (1M context, pay-per-token), and for
98
+ Kimi For Coding subscriptions: `k3` (1M context), `kimi-for-coding`, and
99
+ `kimi-for-coding-highspeed` (256K context). The two use different endpoints
100
+ and keys — a `sk-kimi-*` Kimi For Coding key only works with the
101
+ `kimi-coding` models (`k3`, ...) and vice versa.
102
+
103
+ Switching to a catalog model only requires changing `model` — the provider's
104
+ endpoint and key env var are picked up automatically:
105
+
106
+ ```toml
107
+ [llm]
108
+ model = "k3" # Kimi For Coding; base_url/api_key resolve from the catalog
109
+ ```
110
+
111
+ The `anthropic-messages` dialect is served by a dedicated client
112
+ (`src/limbo/llm/anthropic_client.py`, plain httpx SSE) selected by the
113
+ client factory. It converts OpenAI-style tool definitions to Anthropic's
114
+ shape, merges consecutive tool results into a single user turn, and replays
115
+ assistant thinking blocks with their signature.
116
+
117
+ For the mainland-China Moonshot endpoint, set `base_url =
118
+ "https://api.moonshot.cn/v1"` explicitly (a configured `base_url` always
119
+ wins over the catalog — unless a `[providers.<id>]` override says otherwise,
120
+ see below).
121
+
122
+ ### Per-provider overrides
123
+
124
+ An optional `[providers.<id>]` section overrides a single catalog provider
125
+ without touching the global `[llm]` settings — e.g. pointing a provider at a
126
+ relay, renaming its credential env var, or adding extra headers:
127
+
128
+ ```toml
129
+ [providers.glm]
130
+ base_url = "https://api.z.ai/api/coding/paas/v4" # international endpoint
131
+
132
+ [providers.codex]
133
+ base_url = "https://my-relay.example.com/v1"
134
+ api_key_env = "CODEX_API_KEY" # rename the env var read for the key
135
+ headers = { x-relay = "on" } # extra headers on every request
136
+ ```
137
+
138
+ All fields are optional; unset fields fall back to the catalog. Resolution
139
+ order for `base_url` (first hit wins):
140
+
141
+ 1. `[providers.<id>] base_url`
142
+ 2. `[llm] base_url` (when changed from the DeepSeek default)
143
+ 3. the catalog provider's built-in endpoint
144
+
145
+ and for the API key: `[providers.<id>] api_key` → `[llm] api_key` → the
146
+ environment variable (`[providers.<id>] api_key_env` rename, else the
147
+ catalog's default env var).
148
+
149
+ > **Note:** rule 1 is the single exception to the long-standing "an explicit
150
+ > `[llm] base_url` always wins" behavior — a per-provider override is more
151
+ > specific than the global setting. If you configure both, the
152
+ > `[providers.<id>]` value is used for that provider's models.
153
+
154
+ ### Optional LLM settings
155
+
156
+ ```toml
157
+ [llm]
158
+ temperature = 0.2 # 0.0 - 2.0, default 0.2
159
+ max_iterations = 50 # safety limit on tool-turn loops, default 50
160
+ max_tokens = 8192 # output cap; default = the model's catalog value
161
+ thinking_effort = "high" # reasoning control; default = provider behavior
162
+ ```
163
+
164
+ `thinking_effort` is interpreted per model dialect:
165
+
166
+ - `k3`, `kimi-for-coding*` (Anthropic adaptive thinking): `low` | `high` |
167
+ `max` → `thinking: {type: adaptive}` + `output_config.effort`. Thinking
168
+ cannot be disabled; temperature is omitted while thinking is enabled.
169
+ - `kimi-k3` (moonshotai, OpenAI-style): `low` | `high` | `max` → sent as
170
+ `reasoning_effort`. Thinking cannot be disabled on K3.
171
+ - `kimi-k2-thinking`, `kimi-k2.5`+ (DeepSeek-style): any non-`off` value →
172
+ `thinking: {type: enabled}`; `"off"` → `thinking: {type: disabled}`
173
+ (except `kimi-k2.7-code*`, where thinking is always on).
174
+ - `glm-*` (z.ai-style): like DeepSeek-style but with `clear_thinking: false`
175
+ so thinking is preserved across turns; `glm-5.2` additionally maps
176
+ `low`/`high`/`max` to `reasoning_effort` (`low` clamps to `high`).
177
+ - `gpt-5.*` codex models (Responses API): `low` | `medium` | `high` |
178
+ `xhigh` → `reasoning: {effort, summary: auto}`; 5.6-generation models
179
+ (`gpt-5.6-*`) also accept `max`. Temperature is omitted for reasoning
180
+ models.
181
+ - Non-reasoning models: ignored.
182
+
183
+ Reasoning output streams into the chat as muted thinking blocks and is
184
+ stored on the assistant message so it can be replayed to APIs that require
185
+ it (Kimi K3 rejects tool-call replays without `reasoning_content`).
186
+
187
+ Optional tool settings:
188
+
189
+ ```toml
190
+ [tools]
191
+ bash_enabled = true
192
+ ```
193
+
194
+ ### Session storage
195
+
196
+ Conversations are saved as JSONL files in `~/.limbo/sessions/` so you can
197
+ review or debug them later. Use the `--session-dir` argument to redirect them
198
+ to another location.
199
+
200
+ Old session files are not automatically cleaned up; remove them manually when
201
+ you no longer need them.
202
+
203
+ ## Run
204
+
205
+ ```bash
206
+ limbo --workdir /path/to/project
207
+ limbo --model glm-4.7 # override the configured model for this run
208
+ ```
209
+
210
+ ### Switching models at runtime
211
+
212
+ Use `/model` in the TUI: without an argument it opens a picker that lists
213
+ catalog models grouped by provider (context window, reasoning capability,
214
+ and the current model are annotated; providers without a resolvable API key
215
+ are dimmed with a hint). `/model <name>` switches directly — unknown names
216
+ fall back to generic OpenAI-compatible defaults. The picker re-reads
217
+ `config.toml` every time it opens, so edits (e.g. a newly added
218
+ `[providers.<id>]`) apply without a restart.
219
+
220
+ A switch takes effect immediately (no restart): the LLM client is rebuilt,
221
+ and the new model is written back to `config.toml` (comments preserved via
222
+ tomlkit) so the next launch keeps it. If the write fails, the switch still
223
+ applies for the current session. Switching is refused while a turn is in
224
+ flight.
225
+
226
+ ## Safety
227
+
228
+ Limbo executes every tool call immediately, without asking for confirmation.
229
+ File tools (`read`, `edit`, `write`, `grep`, `find`, `ls`) are bounded to the
230
+ current working directory and reject paths that escape it, including via
231
+ symlinks. The boundary check resolves the path before each operation, so a
232
+ symlink swapped between the check and the operation (a time-of-check-to-time-of-use
233
+ race) could escape the workdir. **This is a known limitation for the MVP.**
234
+
235
+ Bash is an exception: it is started in the working directory but is **not**
236
+ sandboxed. Commands can `cd ..`, use absolute paths, and read or write outside
237
+ the workdir. In addition, commands that match dangerous patterns such as `rm`
238
+ or `git reset --hard` are **rejected outright**.
239
+ The pattern list is configurable but cannot be disabled from the UI. Bash
240
+ commands are filtered with a simple heuristic, but that filter can be bypassed
241
+ by subshells (`bash -c 'rm -rf /'`), command substitution (`$(rm -rf /)`),
242
+ variable indirection, options before the command name
243
+ (`git -C /foo reset --hard`), variable assignments before the command name
244
+ (`VAR=1 rm -rf /`), and similar shell constructs. Only run Limbo with
245
+ trusted commands and in repositories you can afford to modify or lose.
246
+
247
+ If you need to work with untrusted projects, disable the bash tool entirely:
248
+
249
+ ```toml
250
+ [tools]
251
+ bash_enabled = false
252
+ ```
253
+
254
+ ## Development
255
+
256
+ Run tests:
257
+
258
+ ```bash
259
+ pytest tests/ -v
260
+ ```
261
+
262
+ Run linting and type checks:
263
+
264
+ ```bash
265
+ ruff check src tests
266
+ mypy src
267
+ ```
@@ -37,6 +37,40 @@ Built-in providers:
37
37
  | `deepseek` | openai-completions | `https://api.deepseek.com/v1` | `DEEPSEEK_API_KEY` |
38
38
  | `moonshotai` (Kimi) | openai-completions | `https://api.moonshot.ai/v1` | `MOONSHOT_API_KEY` |
39
39
  | `kimi-coding` (Kimi For Coding) | anthropic-messages | `https://api.kimi.com/coding` | `KIMI_API_KEY` |
40
+ | `glm` (GLM Coding Plan) | openai-completions | `https://open.bigmodel.cn/api/coding/paas/v4` | `ZHIPUAI_API_KEY` |
41
+ | `codex` (OpenAI Codex) | openai-responses | `https://api.openai.com/v1` (override with your relay) | `CODEX_API_KEY` |
42
+
43
+ Built-in Codex models include `gpt-5.5`, `gpt-5.6-sol`/`terra`/`luna`,
44
+ `gpt-5.4`, `gpt-5.4-mini`, and `gpt-5.3-codex-spark`. **`gpt-5.5` is
45
+ verified by live test against the target relay** (its 1M context is
46
+ relay-reported); the other entries mirror pi's built-in catalog and are
47
+ unverified — availability depends on the relay in use, and unknown or
48
+ unsupported models fall back to the generic OpenAI-compatible defaults.
49
+ Codex speaks the
50
+ OpenAI **Responses API** (`POST {base_url}/responses`), served by a
51
+ dedicated client (`src/limbo/llm/responses_client.py`, plain httpx SSE):
52
+ system messages become `instructions`, tool definitions are flattened, and
53
+ reasoning models stream `reasoning` summaries and replay encrypted
54
+ reasoning items across turns. Limbo targets **API-key relays**, not the
55
+ ChatGPT subscription backend (OAuth) — point the provider at your relay
56
+ and make sure its model IDs match the catalog:
57
+
58
+ ```toml
59
+ [llm]
60
+ model = "gpt-5.5"
61
+
62
+ [providers.codex]
63
+ base_url = "https://my-relay.example.com/v1"
64
+ api_key_env = "CODEX_API_KEY"
65
+ ```
66
+
67
+ Built-in GLM Coding Plan models: `glm-4.7`, `glm-5.1`, `glm-5.2` (1M
68
+ context), `glm-5-turbo`, `glm-5v-turbo` (vision), and `glm-4.5-air`. The
69
+ Coding Plan is a subscription with its own endpoint and keys — a coding-plan
70
+ key only works on the `/api/coding/paas/v4` endpoints (not the pay-per-token
71
+ `/api/paas/v4` ones) and vice versa. For the international endpoint set
72
+ `[providers.glm] base_url = "https://api.z.ai/api/coding/paas/v4"` (see
73
+ below).
40
74
 
41
75
  Built-in Kimi models include `kimi-k3` (1M context, pay-per-token), and for
42
76
  Kimi For Coding subscriptions: `k3` (1M context), `kimi-for-coding`, and
@@ -60,14 +94,47 @@ assistant thinking blocks with their signature.
60
94
 
61
95
  For the mainland-China Moonshot endpoint, set `base_url =
62
96
  "https://api.moonshot.cn/v1"` explicitly (a configured `base_url` always
63
- wins over the catalog).
97
+ wins over the catalog — unless a `[providers.<id>]` override says otherwise,
98
+ see below).
99
+
100
+ ### Per-provider overrides
101
+
102
+ An optional `[providers.<id>]` section overrides a single catalog provider
103
+ without touching the global `[llm]` settings — e.g. pointing a provider at a
104
+ relay, renaming its credential env var, or adding extra headers:
105
+
106
+ ```toml
107
+ [providers.glm]
108
+ base_url = "https://api.z.ai/api/coding/paas/v4" # international endpoint
109
+
110
+ [providers.codex]
111
+ base_url = "https://my-relay.example.com/v1"
112
+ api_key_env = "CODEX_API_KEY" # rename the env var read for the key
113
+ headers = { x-relay = "on" } # extra headers on every request
114
+ ```
115
+
116
+ All fields are optional; unset fields fall back to the catalog. Resolution
117
+ order for `base_url` (first hit wins):
118
+
119
+ 1. `[providers.<id>] base_url`
120
+ 2. `[llm] base_url` (when changed from the DeepSeek default)
121
+ 3. the catalog provider's built-in endpoint
122
+
123
+ and for the API key: `[providers.<id>] api_key` → `[llm] api_key` → the
124
+ environment variable (`[providers.<id>] api_key_env` rename, else the
125
+ catalog's default env var).
126
+
127
+ > **Note:** rule 1 is the single exception to the long-standing "an explicit
128
+ > `[llm] base_url` always wins" behavior — a per-provider override is more
129
+ > specific than the global setting. If you configure both, the
130
+ > `[providers.<id>]` value is used for that provider's models.
64
131
 
65
132
  ### Optional LLM settings
66
133
 
67
134
  ```toml
68
135
  [llm]
69
136
  temperature = 0.2 # 0.0 - 2.0, default 0.2
70
- max_iterations = 10 # safety limit on tool-turn loops, default 10
137
+ max_iterations = 50 # safety limit on tool-turn loops, default 50
71
138
  max_tokens = 8192 # output cap; default = the model's catalog value
72
139
  thinking_effort = "high" # reasoning control; default = provider behavior
73
140
  ```
@@ -82,6 +149,13 @@ thinking_effort = "high" # reasoning control; default = provider behavior
82
149
  - `kimi-k2-thinking`, `kimi-k2.5`+ (DeepSeek-style): any non-`off` value →
83
150
  `thinking: {type: enabled}`; `"off"` → `thinking: {type: disabled}`
84
151
  (except `kimi-k2.7-code*`, where thinking is always on).
152
+ - `glm-*` (z.ai-style): like DeepSeek-style but with `clear_thinking: false`
153
+ so thinking is preserved across turns; `glm-5.2` additionally maps
154
+ `low`/`high`/`max` to `reasoning_effort` (`low` clamps to `high`).
155
+ - `gpt-5.*` codex models (Responses API): `low` | `medium` | `high` |
156
+ `xhigh` → `reasoning: {effort, summary: auto}`; 5.6-generation models
157
+ (`gpt-5.6-*`) also accept `max`. Temperature is omitted for reasoning
158
+ models.
85
159
  - Non-reasoning models: ignored.
86
160
 
87
161
  Reasoning output streams into the chat as muted thinking blocks and is
@@ -108,8 +182,25 @@ you no longer need them.
108
182
 
109
183
  ```bash
110
184
  limbo --workdir /path/to/project
185
+ limbo --model glm-4.7 # override the configured model for this run
111
186
  ```
112
187
 
188
+ ### Switching models at runtime
189
+
190
+ Use `/model` in the TUI: without an argument it opens a picker that lists
191
+ catalog models grouped by provider (context window, reasoning capability,
192
+ and the current model are annotated; providers without a resolvable API key
193
+ are dimmed with a hint). `/model <name>` switches directly — unknown names
194
+ fall back to generic OpenAI-compatible defaults. The picker re-reads
195
+ `config.toml` every time it opens, so edits (e.g. a newly added
196
+ `[providers.<id>]`) apply without a restart.
197
+
198
+ A switch takes effect immediately (no restart): the LLM client is rebuilt,
199
+ and the new model is written back to `config.toml` (comments preserved via
200
+ tomlkit) so the next launch keeps it. If the write fails, the switch still
201
+ applies for the current session. Switching is refused while a turn is in
202
+ flight.
203
+
113
204
  ## Safety
114
205
 
115
206
  Limbo executes every tool call immediately, without asking for confirmation.