lcode-cli 0.32.0__tar.gz → 0.33.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 (171) hide show
  1. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/CHANGELOG.md +34 -1
  2. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/PKG-INFO +11 -10
  3. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/README.md +10 -9
  4. lcode_cli-0.33.1/docs/assets/extra.css +60 -0
  5. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/index.md +3 -2
  6. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/quickstart.md +1 -1
  7. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/usage.md +42 -3
  8. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/mkdocs.yml +2 -2
  9. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/pyproject.toml +3 -0
  10. lcode_cli-0.33.1/scripts/make_logos.py +62 -0
  11. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/scripts/social-preview.html +7 -19
  12. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/__init__.py +1 -1
  13. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/agent.py +50 -14
  14. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/permissions.py +3 -1
  15. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/repl.py +30 -4
  16. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/sessions.py +3 -1
  17. lcode_cli-0.33.1/src/lcode/steer.py +287 -0
  18. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/tools.py +4 -0
  19. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_sessions.py +2 -0
  20. lcode_cli-0.33.1/tests/test_steer.py +372 -0
  21. lcode_cli-0.32.0/docs/assets/extra.css +0 -29
  22. lcode_cli-0.32.0/docs/assets/logo.svg +0 -15
  23. lcode_cli-0.32.0/docs/assets/social-preview.png +0 -0
  24. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.editorconfig +0 -0
  25. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/CODEOWNERS +0 -0
  26. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  27. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  28. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  29. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
  30. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  31. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/dependabot.yml +0 -0
  32. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/workflows/ci.yml +0 -0
  33. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/workflows/docs.yml +0 -0
  34. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/workflows/release.yml +0 -0
  35. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.gitignore +0 -0
  36. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.pre-commit-config.yaml +0 -0
  37. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/AGENTS.md +0 -0
  38. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/CODE_OF_CONDUCT.md +0 -0
  39. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/CONTRIBUTING.md +0 -0
  40. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/LICENSE +0 -0
  41. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/SECURITY.md +0 -0
  42. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/action.yml +0 -0
  43. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/agents.md +0 -0
  44. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/assets/demo.svg +0 -0
  45. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/automation.md +0 -0
  46. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/bench.md +0 -0
  47. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/changelog.md +0 -0
  48. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/commands.md +0 -0
  49. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/configuration.md +0 -0
  50. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/contributing.md +0 -0
  51. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/editors.md +0 -0
  52. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/git.md +0 -0
  53. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/hooks.md +0 -0
  54. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/how-it-works.md +0 -0
  55. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/installation.md +0 -0
  56. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/lsp.md +0 -0
  57. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/mcp.md +0 -0
  58. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/memory.md +0 -0
  59. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/models.md +0 -0
  60. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/sandbox.md +0 -0
  61. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/search.md +0 -0
  62. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/servers.md +0 -0
  63. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/troubleshooting.md +0 -0
  64. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/examples/README.md +0 -0
  65. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/examples/api/fix_tests.py +0 -0
  66. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/examples/commands/review.md +0 -0
  67. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/examples/skills/commit-message/SKILL.md +0 -0
  68. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/examples/skills/commit-message/scripts/staged.sh +0 -0
  69. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/install.sh +0 -0
  70. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/overrides/main.html +0 -0
  71. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/scripts/record_demo.py +0 -0
  72. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/__main__.py +0 -0
  73. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/acp.py +0 -0
  74. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/action.py +0 -0
  75. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/api.py +0 -0
  76. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/backends.py +0 -0
  77. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/bench.py +0 -0
  78. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/bench_backend.py +0 -0
  79. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/bench_runtime.py +0 -0
  80. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/catalog.py +0 -0
  81. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/checkpoints.py +0 -0
  82. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/checks.py +0 -0
  83. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/cli.py +0 -0
  84. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/clipboard.py +0 -0
  85. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/codesearch.py +0 -0
  86. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/config.py +0 -0
  87. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/context.py +0 -0
  88. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/extensions.py +0 -0
  89. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/frontmatter.py +0 -0
  90. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/gitflow.py +0 -0
  91. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/hardware.py +0 -0
  92. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/hooks.py +0 -0
  93. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/jobs.py +0 -0
  94. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/limits.py +0 -0
  95. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/lsp.py +0 -0
  96. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/matching.py +0 -0
  97. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/__init__.py +0 -0
  98. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/auth.py +0 -0
  99. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/catalog.py +0 -0
  100. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/catalog.toml +0 -0
  101. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/client.py +0 -0
  102. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/commands.py +0 -0
  103. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/config.py +0 -0
  104. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/http.py +0 -0
  105. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/manager.py +0 -0
  106. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/protocol.py +0 -0
  107. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/server.py +0 -0
  108. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/stdio.py +0 -0
  109. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/memory.py +0 -0
  110. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/models.toml +0 -0
  111. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/notify.py +0 -0
  112. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/ollama.py +0 -0
  113. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/planning.py +0 -0
  114. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/render.py +0 -0
  115. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/repeats.py +0 -0
  116. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/repomap.py +0 -0
  117. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/residency.py +0 -0
  118. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/review.py +0 -0
  119. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/sandbox.py +0 -0
  120. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/secretscan.py +0 -0
  121. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/servers/__init__.py +0 -0
  122. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/servers/encord.py +0 -0
  123. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/servers/valohai.py +0 -0
  124. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/servers/xai.py +0 -0
  125. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/subagents.py +0 -0
  126. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/update.py +0 -0
  127. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/vision.py +0 -0
  128. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/web.py +0 -0
  129. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/conftest.py +0 -0
  130. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/fake_lsp_server.py +0 -0
  131. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/fake_mcp_server.py +0 -0
  132. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_acp.py +0 -0
  133. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_action.py +0 -0
  134. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_agent.py +0 -0
  135. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_api.py +0 -0
  136. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_backends.py +0 -0
  137. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_bench.py +0 -0
  138. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_catalog.py +0 -0
  139. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_checkpoints.py +0 -0
  140. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_checks.py +0 -0
  141. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_cli.py +0 -0
  142. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_clipboard.py +0 -0
  143. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_codesearch.py +0 -0
  144. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_config.py +0 -0
  145. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_context.py +0 -0
  146. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_context_picker.py +0 -0
  147. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_extensions.py +0 -0
  148. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_gitflow.py +0 -0
  149. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_hooks.py +0 -0
  150. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_jobs.py +0 -0
  151. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_lsp.py +0 -0
  152. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_matching.py +0 -0
  153. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_mcp.py +0 -0
  154. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_memory.py +0 -0
  155. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_permissions.py +0 -0
  156. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_plan_mode.py +0 -0
  157. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_repeats.py +0 -0
  158. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_repomap.py +0 -0
  159. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_residency.py +0 -0
  160. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_review.py +0 -0
  161. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_robustness.py +0 -0
  162. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_sandbox.py +0 -0
  163. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_secretscan.py +0 -0
  164. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_servers.py +0 -0
  165. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_subagents.py +0 -0
  166. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_tools.py +0 -0
  167. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_update.py +0 -0
  168. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_vision.py +0 -0
  169. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_web.py +0 -0
  170. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_xai.py +0 -0
  171. {lcode_cli-0.32.0 → lcode_cli-0.33.1}/uv.lock +0 -0
@@ -6,6 +6,37 @@ All notable changes to lcode are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.33.1] - 2026-10-14
10
+
11
+ ### Changed
12
+
13
+ - A new logo, on GitHub, PyPI and the docs site (header, favicon, home page and link previews),
14
+ with a light version for dark backgrounds. `scripts/make_logos.py` makes every size from the
15
+ original.
16
+
17
+ ### Fixed
18
+
19
+ - The README's images and links work on PyPI: the logo, the demo and the links to the license
20
+ and contributing guide were relative, so PyPI showed them broken.
21
+ - The source package is half the size (0.6 MB): it no longer carries the docs' images.
22
+
23
+ ## [0.33.0] - 2026-10-14
24
+
25
+ ### Added
26
+
27
+ - Steer while it works. Type while a request runs and press Enter: the line goes to the model at
28
+ its next step, as a note from you, without interrupting anything. The spinner shows what you're
29
+ typing and the notes waiting. Esc pauses after the current step, so you can look, then go on with
30
+ or without a note. Notes count as your requests for summaries and the review, and anything that
31
+ never reached the model goes back into the prompt. A command that runs for a while now shows a
32
+ spinner with its elapsed time. See
33
+ [Steer while it works](https://nasser1941.github.io/lcode/usage/#steer-while-it-works).
34
+
35
+ ### Fixed
36
+
37
+ - "Where you left off" (`lcode -c`) and session titles showed lcode's own messages to the model,
38
+ such as a failed check, as if you had written them. They show your last request now.
39
+
9
40
  ## [0.32.0] - 2026-10-14
10
41
 
11
42
  ### Added
@@ -691,7 +722,9 @@ First public release.
691
722
  - `AGENTS.md` project instructions and `/init` to generate them.
692
723
  - One-line installer for Linux and macOS.
693
724
 
694
- [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.32.0...HEAD
725
+ [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.33.1...HEAD
726
+ [0.33.1]: https://github.com/nasser1941/lcode/compare/v0.33.0...v0.33.1
727
+ [0.33.0]: https://github.com/nasser1941/lcode/compare/v0.32.0...v0.33.0
695
728
  [0.32.0]: https://github.com/nasser1941/lcode/compare/v0.31.0...v0.32.0
696
729
  [0.31.0]: https://github.com/nasser1941/lcode/compare/v0.30.0...v0.31.0
697
730
  [0.30.0]: https://github.com/nasser1941/lcode/compare/v0.29.0...v0.30.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lcode-cli
3
- Version: 0.32.0
3
+ Version: 0.33.1
4
4
  Summary: A local-first terminal coding agent powered by open-weight models on your own GPU or Mac, via Ollama.
5
5
  Project-URL: Homepage, https://nasser1941.github.io/lcode/
6
6
  Project-URL: Documentation, https://nasser1941.github.io/lcode/
@@ -35,11 +35,12 @@ Requires-Dist: encord>=0.1.200; extra == 'encord'
35
35
  Description-Content-Type: text/markdown
36
36
 
37
37
  <p align="center">
38
- <img src="docs/assets/logo.svg" width="96" alt="lcode logo">
38
+ <picture>
39
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/nasser1941/lcode/main/docs/assets/logo-dark.png">
40
+ <img src="https://raw.githubusercontent.com/nasser1941/lcode/main/docs/assets/logo.png" width="360" alt="lcode">
41
+ </picture>
39
42
  </p>
40
43
 
41
- <h1 align="center">lcode</h1>
42
-
43
44
  <p align="center">
44
45
  <b>A coding agent that runs entirely on your own machine.</b><br>
45
46
  Open-weight models · up to 256K tokens of context · your NVIDIA GPU or Apple Silicon Mac · your code stays local
@@ -49,7 +50,7 @@ Description-Content-Type: text/markdown
49
50
  <a href="https://github.com/nasser1941/lcode/actions/workflows/ci.yml"><img src="https://github.com/nasser1941/lcode/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
50
51
  <a href="https://nasser1941.github.io/lcode/"><img src="https://github.com/nasser1941/lcode/actions/workflows/docs.yml/badge.svg" alt="Docs"></a>
51
52
  <a href="https://pypi.org/project/lcode-cli/"><img src="https://img.shields.io/pypi/v/lcode-cli.svg" alt="PyPI"></a>
52
- <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License"></a>
53
+ <a href="https://github.com/nasser1941/lcode/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License"></a>
53
54
  <img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+">
54
55
  <img src="https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20(Apple%20Silicon)-lightgrey.svg" alt="Linux and macOS">
55
56
  </p>
@@ -58,11 +59,11 @@ Description-Content-Type: text/markdown
58
59
  <a href="https://nasser1941.github.io/lcode/"><b>Documentation</b></a> ·
59
60
  <a href="https://nasser1941.github.io/lcode/installation/">Install</a> ·
60
61
  <a href="https://nasser1941.github.io/lcode/models/">Models</a> ·
61
- <a href="CONTRIBUTING.md">Contributing</a>
62
+ <a href="https://github.com/nasser1941/lcode/blob/main/CONTRIBUTING.md">Contributing</a>
62
63
  </p>
63
64
 
64
65
  <p align="center">
65
- <img src="docs/assets/demo.svg" alt="lcode finding and fixing a bug, then running the tests" width="820">
66
+ <img src="https://raw.githubusercontent.com/nasser1941/lcode/main/docs/assets/demo.svg" alt="lcode finding and fixing a bug, then running the tests" width="820">
66
67
  </p>
67
68
 
68
69
  Point lcode at a repository and talk to it in your terminal. It explores the code, answers
@@ -222,8 +223,8 @@ See the pinned [Roadmap issue](https://github.com/nasser1941/lcode/issues/10). N
222
223
  ## Contributing
223
224
 
224
225
  Contributions are welcome: bug reports, model test results, docs and code. See
225
- [CONTRIBUTING.md](CONTRIBUTING.md). Please follow the [code of conduct](CODE_OF_CONDUCT.md), and
226
- report security issues privately as described in [SECURITY.md](SECURITY.md).
226
+ [CONTRIBUTING.md](https://github.com/nasser1941/lcode/blob/main/CONTRIBUTING.md). Please follow the [code of conduct](https://github.com/nasser1941/lcode/blob/main/CODE_OF_CONDUCT.md), and
227
+ report security issues privately as described in [SECURITY.md](https://github.com/nasser1941/lcode/blob/main/SECURITY.md).
227
228
 
228
229
  ## Acknowledgements
229
230
 
@@ -234,4 +235,4 @@ the open-weight models from Qwen, Poolside, NVIDIA and OpenAI,
234
235
 
235
236
  ## License
236
237
 
237
- [MIT](LICENSE) © 2026 Naser Derakhshan
238
+ [MIT](https://github.com/nasser1941/lcode/blob/main/LICENSE) © 2026 Naser Derakhshan
@@ -1,9 +1,10 @@
1
1
  <p align="center">
2
- <img src="docs/assets/logo.svg" width="96" alt="lcode logo">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/nasser1941/lcode/main/docs/assets/logo-dark.png">
4
+ <img src="https://raw.githubusercontent.com/nasser1941/lcode/main/docs/assets/logo.png" width="360" alt="lcode">
5
+ </picture>
3
6
  </p>
4
7
 
5
- <h1 align="center">lcode</h1>
6
-
7
8
  <p align="center">
8
9
  <b>A coding agent that runs entirely on your own machine.</b><br>
9
10
  Open-weight models · up to 256K tokens of context · your NVIDIA GPU or Apple Silicon Mac · your code stays local
@@ -13,7 +14,7 @@
13
14
  <a href="https://github.com/nasser1941/lcode/actions/workflows/ci.yml"><img src="https://github.com/nasser1941/lcode/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
14
15
  <a href="https://nasser1941.github.io/lcode/"><img src="https://github.com/nasser1941/lcode/actions/workflows/docs.yml/badge.svg" alt="Docs"></a>
15
16
  <a href="https://pypi.org/project/lcode-cli/"><img src="https://img.shields.io/pypi/v/lcode-cli.svg" alt="PyPI"></a>
16
- <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License"></a>
17
+ <a href="https://github.com/nasser1941/lcode/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License"></a>
17
18
  <img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+">
18
19
  <img src="https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20(Apple%20Silicon)-lightgrey.svg" alt="Linux and macOS">
19
20
  </p>
@@ -22,11 +23,11 @@
22
23
  <a href="https://nasser1941.github.io/lcode/"><b>Documentation</b></a> ·
23
24
  <a href="https://nasser1941.github.io/lcode/installation/">Install</a> ·
24
25
  <a href="https://nasser1941.github.io/lcode/models/">Models</a> ·
25
- <a href="CONTRIBUTING.md">Contributing</a>
26
+ <a href="https://github.com/nasser1941/lcode/blob/main/CONTRIBUTING.md">Contributing</a>
26
27
  </p>
27
28
 
28
29
  <p align="center">
29
- <img src="docs/assets/demo.svg" alt="lcode finding and fixing a bug, then running the tests" width="820">
30
+ <img src="https://raw.githubusercontent.com/nasser1941/lcode/main/docs/assets/demo.svg" alt="lcode finding and fixing a bug, then running the tests" width="820">
30
31
  </p>
31
32
 
32
33
  Point lcode at a repository and talk to it in your terminal. It explores the code, answers
@@ -186,8 +187,8 @@ See the pinned [Roadmap issue](https://github.com/nasser1941/lcode/issues/10). N
186
187
  ## Contributing
187
188
 
188
189
  Contributions are welcome: bug reports, model test results, docs and code. See
189
- [CONTRIBUTING.md](CONTRIBUTING.md). Please follow the [code of conduct](CODE_OF_CONDUCT.md), and
190
- report security issues privately as described in [SECURITY.md](SECURITY.md).
190
+ [CONTRIBUTING.md](https://github.com/nasser1941/lcode/blob/main/CONTRIBUTING.md). Please follow the [code of conduct](https://github.com/nasser1941/lcode/blob/main/CODE_OF_CONDUCT.md), and
191
+ report security issues privately as described in [SECURITY.md](https://github.com/nasser1941/lcode/blob/main/SECURITY.md).
191
192
 
192
193
  ## Acknowledgements
193
194
 
@@ -198,4 +199,4 @@ the open-weight models from Qwen, Poolside, NVIDIA and OpenAI,
198
199
 
199
200
  ## License
200
201
 
201
- [MIT](LICENSE) © 2026 Naser Derakhshan
202
+ [MIT](https://github.com/nasser1941/lcode/blob/main/LICENSE) © 2026 Naser Derakhshan
@@ -0,0 +1,60 @@
1
+ .hero {
2
+ text-align: center;
3
+ padding: 1.5rem 0 0.5rem;
4
+ }
5
+
6
+ .hero h1 {
7
+ font-size: 3rem;
8
+ font-weight: 800;
9
+ margin: 0.5rem 0 0.25rem;
10
+ letter-spacing: -0.02em;
11
+ }
12
+
13
+ .hero-logo {
14
+ width: 360px;
15
+ max-width: 80%;
16
+ height: auto;
17
+ margin: 0.5rem 0 0.75rem;
18
+ }
19
+
20
+ .hero p {
21
+ font-size: 1.1rem;
22
+ max-width: 44rem;
23
+ margin: 0.5rem auto 1.25rem;
24
+ }
25
+
26
+ .demo {
27
+ display: block;
28
+ margin: 1.5rem auto;
29
+ border-radius: 10px;
30
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
31
+ }
32
+
33
+ /* In the page for screen readers and search, but the logo shows the name. */
34
+ .sr-only {
35
+ position: absolute;
36
+ width: 1px;
37
+ height: 1px;
38
+ overflow: hidden;
39
+ clip: rect(0, 0, 0, 0);
40
+ white-space: nowrap;
41
+ }
42
+
43
+ /* The buttons in the logo's colours. */
44
+ .hero .md-button {
45
+ border-color: #00b8cc;
46
+ color: inherit;
47
+ }
48
+
49
+ .hero .md-button--primary {
50
+ background-color: #00e3fc;
51
+ border-color: #00e3fc;
52
+ color: #0a0e10;
53
+ }
54
+
55
+ .hero .md-button:hover,
56
+ .hero .md-button:focus {
57
+ background-color: #00b8cc;
58
+ border-color: #00b8cc;
59
+ color: #0a0e10;
60
+ }
@@ -7,9 +7,10 @@ hide:
7
7
 
8
8
  <div class="hero" markdown>
9
9
 
10
- ![lcode logo](assets/logo.svg){ .hero-logo }
10
+ ![lcode](assets/logo.png#only-light){ .hero-logo }
11
+ ![lcode](assets/logo-dark.png#only-dark){ .hero-logo }
11
12
 
12
- # lcode
13
+ # lcode { .sr-only }
13
14
 
14
15
  **A coding agent that runs entirely on your own machine.**
15
16
  Open-weight models · up to 256K tokens of context · your NVIDIA GPU or Apple Silicon Mac · your code stays local.
@@ -12,7 +12,7 @@ lcode
12
12
 
13
13
  ```text
14
14
  ╭──────────────────────────────────────────────╮
15
- │ lcode v0.32.0 — local coding agent │
15
+ │ lcode v0.33.1 — local coding agent │
16
16
  │ │
17
17
  │ model lcode-qwen3.6-35b │
18
18
  │ context 256K tokens │
@@ -44,6 +44,8 @@ lcode update [--check] # update lcode, and Ollama when it runs in Docker (
44
44
  | ++enter++ | Send |
45
45
  | ++esc++ ++enter++ or a trailing `\` | New line |
46
46
  | ++ctrl+c++ | Interrupt the model or a running command |
47
+ | Typing while it works | A note for the model, read at its next step ([Steer while it works](#steer-while-it-works)) |
48
+ | ++esc++ while it works | Pause after the current step |
47
49
  | ++ctrl+d++ | Quit |
48
50
  | ++shift+tab++ | Cycle permission mode: ask → plan → auto-edit → yolo |
49
51
  | ++tab++ | Complete slash commands and `@` paths |
@@ -231,9 +233,10 @@ Much of the waiting with a local model isn't the model thinking: it's loading th
231
233
  a long conversation before the first word. lcode says which, in the status line under a request:
232
234
 
233
235
  ```text
234
- ⠋ Loading qwen3.6-35b into memory… (0 tokens, 9s · Ctrl+C to stop)
235
- ⠋ Reading the conversation (18K tokens)… (0 tokens, 21s · Ctrl+C to stop)
236
- ⠋ Thinking… (312 tokens, 27s · Ctrl+C to stop)
236
+ ⠋ Loading qwen3.6-35b into memory… (0 tokens, 9s · type a note · Esc pauses · Ctrl+C stops)
237
+ ⠋ Reading the conversation (18K tokens)… (0 tokens, 21s · type a note · Esc pauses · Ctrl+C stops)
238
+ ⠋ Thinking… (312 tokens, 27s · type a note · Esc pauses · Ctrl+C stops)
239
+ ⠋ Running the command… (48s · type a note · Esc pauses · Ctrl+C stops)
237
240
  ```
238
241
 
239
242
  It prints a line only when something worth knowing happens, and nothing while the model simply
@@ -252,6 +255,42 @@ While the model loads in the background at startup, the status bar says
252
255
  part of them in system RAM. Dense ones slow down a lot, to a few tokens a second, which is why they
253
256
  get the warning.
254
257
 
258
+ ## Steer while it works
259
+
260
+ A request can take minutes with a local model. You don't have to stop it to change course: type
261
+ while it works, and press ++enter++. The line goes to the model at its next step, after the step it's
262
+ on finishes, as a note from you. Nothing is interrupted or thrown away.
263
+
264
+ ```text
265
+ ● edit_file(tally.py)
266
+ ✓ Edited tally.py
267
+ ✎ Note for the model: also add a --min-count N option that skips rare words
268
+ ```
269
+
270
+ What you type shows under the spinner, since the terminal doesn't echo it while lcode works:
271
+
272
+ ```text
273
+ ⠋ Thinking… (212 tokens, 14s · type a note · Esc pauses · Ctrl+C stops)
274
+ ✎ also add a --min-count N opt▏ Enter adds it as a note
275
+ ```
276
+
277
+ - A note typed while the model writes its final answer still counts: the model reads it before the
278
+ request ends.
279
+ - Press ++esc++ to pause after the current step, to look at what it did. Then press ++enter++ to go
280
+ on, type a note to go on with it, or press ++ctrl+c++ to stop the request. Press ++esc++ again
281
+ before the step ends if you change your mind.
282
+ - Notes count as your requests: they're kept when old messages are
283
+ [summarized](how-it-works.md#context-management), and the [review](#a-review-before-the-answer)
284
+ checks the change against them too.
285
+ - When lcode asks you something, such as a permission, the keyboard is yours again for the answer.
286
+ Keys you typed just before the question appeared are dropped, so a note you were finishing can't
287
+ answer it by accident.
288
+ - What never reached the model, such as a note typed as the request ended or after ++ctrl+c++, is
289
+ put back in the prompt.
290
+
291
+ Steering works in the interactive session on macOS and Linux. `lcode -p` and the editor
292
+ integration don't read the keyboard during a request.
293
+
255
294
  ## Notifications
256
295
 
257
296
  Local models take a while, so you'll often switch to something else during a request. lcode shows
@@ -9,8 +9,8 @@ copyright: Copyright &copy; 2026 Naser Derakhshan · <a href="https://github.com
9
9
  theme:
10
10
  name: material
11
11
  custom_dir: overrides
12
- logo: assets/logo.svg
13
- favicon: assets/logo.svg
12
+ logo: assets/icon.png
13
+ favicon: assets/favicon.png
14
14
  icon:
15
15
  repo: fontawesome/brands/github
16
16
  font:
@@ -61,6 +61,9 @@ path = "src/lcode/__init__.py"
61
61
  [tool.hatch.build.targets.wheel]
62
62
  packages = ["src/lcode"]
63
63
 
64
+ [tool.hatch.build.targets.sdist]
65
+ exclude = ["docs/assets/*.png"] # images for the README and the docs site: not needed to build or install
66
+
64
67
  [tool.ruff]
65
68
  line-length = 120
66
69
  target-version = "py310"
@@ -0,0 +1,62 @@
1
+ """Make the logo files the README, the docs and PyPI use, from docs/assets/logo-original.png.
2
+
3
+ uv run --with pillow python scripts/make_logos.py
4
+
5
+ - logo.png: the mark and the name, for light backgrounds
6
+ - logo-dark.png: the same with a light name, for dark backgrounds (GitHub's dark mode, the docs' dark scheme)
7
+ - icon.png: the mark alone, square (the docs' header)
8
+ - favicon.png: the mark, small (browser tabs)
9
+ """
10
+
11
+ from pathlib import Path
12
+
13
+ from PIL import Image
14
+
15
+ ASSETS = Path(__file__).resolve().parent.parent / "docs" / "assets"
16
+ WIDTH = 1200 # sharp at the README's 360 pixels on a 3x screen
17
+ LIGHT_TEXT = (230, 237, 243) # GitHub's dark-mode text colour
18
+ PAD = 0.03 # around the logo, as a share of its width
19
+
20
+
21
+ def is_mark(r: int, g: int, b: int) -> bool:
22
+ return b > 120 and g > 100 and r < 120 # the cyan of the mark (anti-aliased edges included)
23
+
24
+
25
+ def main() -> None:
26
+ original = Image.open(ASSETS / "logo-original.png").convert("RGBA")
27
+ box = original.getchannel("A").point(lambda a: 255 if a > 8 else 0).getbbox()
28
+ logo = original.crop(box)
29
+ pad = int(logo.width * PAD)
30
+ framed = Image.new("RGBA", (logo.width + 2 * pad, logo.height + 2 * pad))
31
+ framed.paste(logo, (pad, pad))
32
+ height = round(framed.height * WIDTH / framed.width)
33
+ light = framed.resize((WIDTH, height), Image.LANCZOS)
34
+ light.save(ASSETS / "logo.png", optimize=True)
35
+
36
+ dark = light.copy()
37
+ pixels = dark.load()
38
+ for y in range(dark.height):
39
+ for x in range(dark.width):
40
+ r, g, b, a = pixels[x, y]
41
+ if a and not is_mark(r, g, b):
42
+ pixels[x, y] = (*LIGHT_TEXT, a)
43
+ dark.save(ASSETS / "logo-dark.png", optimize=True)
44
+
45
+ # The mark: the cyan part on the left.
46
+ cyan = Image.new("L", logo.size)
47
+ lp, cp = logo.load(), cyan.load()
48
+ for y in range(logo.height):
49
+ for x in range(logo.width):
50
+ r, g, b, a = lp[x, y]
51
+ if a > 8 and is_mark(r, g, b):
52
+ cp[x, y] = 255
53
+ mark = logo.crop(cyan.getbbox())
54
+ side = round(max(mark.size) * 1.12)
55
+ square = Image.new("RGBA", (side, side))
56
+ square.paste(mark, ((side - mark.width) // 2, (side - mark.height) // 2))
57
+ square.resize((512, 512), Image.LANCZOS).save(ASSETS / "icon.png", optimize=True)
58
+ square.resize((96, 96), Image.LANCZOS).save(ASSETS / "favicon.png", optimize=True)
59
+
60
+
61
+ if __name__ == "__main__":
62
+ main()
@@ -8,14 +8,14 @@
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
9
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&family=JetBrains+Mono:wght@400;700&display=block" rel="stylesheet">
10
10
  <style>
11
- :root { --cyan: #22d3ee; --violet: #a78bfa; --text: #e2e8f0; --muted: #94a3b8; }
11
+ :root { --cyan: #00e3fc; --cyan-light: #8ef4ff; --text: #e6edf3; --muted: #94a3b8; }
12
12
  * { box-sizing: border-box; margin: 0; padding: 0; }
13
13
  html, body { width: 1280px; height: 640px; overflow: hidden; }
14
14
  body {
15
15
  font-family: Inter, system-ui, sans-serif; color: var(--text);
16
16
  background:
17
- radial-gradient(900px 500px at 85% 20%, rgba(167, 139, 250, 0.22), transparent 60%),
18
- radial-gradient(700px 500px at 10% 90%, rgba(34, 211, 238, 0.18), transparent 60%),
17
+ radial-gradient(900px 500px at 85% 20%, rgba(0, 227, 252, 0.14), transparent 60%),
18
+ radial-gradient(700px 500px at 10% 90%, rgba(0, 227, 252, 0.16), transparent 60%),
19
19
  linear-gradient(135deg, #0b1120 0%, #0f172a 55%, #111827 100%);
20
20
  display: flex; align-items: center; padding: 64px 72px; gap: 56px;
21
21
  }
@@ -25,12 +25,11 @@
25
25
  background-size: 40px 40px;
26
26
  }
27
27
  .left { flex: 1 1 0; position: relative; }
28
- .brand { display: flex; align-items: center; gap: 22px; margin-bottom: 26px; }
29
- .brand svg { width: 92px; height: 92px; filter: drop-shadow(0 10px 30px rgba(34, 211, 238, 0.25)); }
30
- h1 { font-size: 104px; font-weight: 800; letter-spacing: -0.04em; line-height: 1; }
28
+ .brand { margin: 0 0 30px -10px; }
29
+ .brand img { height: 118px; display: block; filter: drop-shadow(0 10px 30px rgba(0, 227, 252, 0.22)); }
31
30
  .tagline { font-size: 34px; font-weight: 600; line-height: 1.25; margin-bottom: 30px; max-width: 560px; }
32
31
  .tagline span {
33
- background: linear-gradient(90deg, var(--cyan), var(--violet)); -webkit-background-clip: text; color: transparent;
32
+ background: linear-gradient(90deg, var(--cyan), var(--cyan-light)); -webkit-background-clip: text; color: transparent;
34
33
  }
35
34
  .chips { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 34px; max-width: 580px; }
36
35
  .chip {
@@ -56,18 +55,7 @@
56
55
  </head>
57
56
  <body>
58
57
  <div class="left">
59
- <div class="brand">
60
- <svg viewBox="0 0 128 128" aria-hidden="true">
61
- <defs>
62
- <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#1e293b"/><stop offset="1" stop-color="#0f172a"/></linearGradient>
63
- <linearGradient id="fg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#22d3ee"/><stop offset="1" stop-color="#a78bfa"/></linearGradient>
64
- </defs>
65
- <rect width="128" height="128" rx="28" fill="url(#bg)" stroke="rgba(148,163,184,.35)" stroke-width="2"/>
66
- <path d="M34 40 L62 64 L34 88" fill="none" stroke="url(#fg)" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
67
- <rect x="70" y="80" width="30" height="10" rx="5" fill="url(#fg)"/>
68
- </svg>
69
- <h1>lcode</h1>
70
- </div>
58
+ <div class="brand"><img src="../docs/assets/logo-dark.png" alt="lcode"></div>
71
59
  <p class="tagline">A coding agent that runs <span>entirely on your own machine.</span></p>
72
60
  <div class="chips">
73
61
  <div class="chip">Open-weight models</div>
@@ -3,7 +3,7 @@
3
3
  From a program: `from lcode import Session` (see lcode.api).
4
4
  """
5
5
 
6
- __version__ = "0.32.0"
6
+ __version__ = "0.33.1"
7
7
 
8
8
  API = ("Session", "Result", "Options", "SetupError")
9
9
 
@@ -52,6 +52,7 @@ from lcode.permissions import Permissions
52
52
  from lcode.render import MarkdownStreamer
53
53
  from lcode.repeats import Repeats
54
54
  from lcode.sandbox import Sandbox, SandboxError, project_root
55
+ from lcode.steer import NOTE_INTRO, Steering
55
56
  from lcode.tools import (
56
57
  LSP_SCHEMA,
57
58
  MEMORY_SCHEMA,
@@ -224,10 +225,12 @@ class Progress:
224
225
  arrives, for example while the model writes a long file into a tool call.
225
226
  """
226
227
 
227
- def __init__(self, phase: str = "Thinking") -> None:
228
+ def __init__(self, phase: str = "Thinking", steering: Steering | None = None, tokens: bool = True) -> None:
228
229
  self.started = self.last_chunk = time.time()
229
230
  self.phase = phase
230
231
  self.chars = 0
232
+ self.steering = steering # the note being typed and the notes waiting, shown on a second line
233
+ self.tokens = tokens # False for a command running: no tokens to count
231
234
 
232
235
  def update(self, phase: str, text: str) -> None:
233
236
  self.phase, self.chars, self.last_chunk = phase, self.chars + len(text), time.time()
@@ -240,9 +243,11 @@ class Progress:
240
243
  now = time.time()
241
244
  streaming = self.phase in ("Thinking", "Writing") # loading and reading say what they are throughout
242
245
  phase = self.phase if now - self.last_chunk < 3 or not streaming else "Working"
243
- return Text.from_markup(
244
- f"[cyan]{phase}…[/] [dim]({self.chars // 4} tokens, {now - self.started:.0f}s · Ctrl+C to stop)[/]"
245
- )
246
+ tokens = f"{self.chars // 4} tokens, " if self.tokens else ""
247
+ keys = "Ctrl+C to stop" if self.steering is None else "type a note · Esc pauses · Ctrl+C stops"
248
+ line = Text.from_markup(f"[cyan]{phase}…[/] [dim]({tokens}{now - self.started:.0f}s · {keys})[/]")
249
+ below = self.steering.status() if self.steering is not None else None
250
+ return line if below is None else Text("\n").join([line, below])
246
251
 
247
252
 
248
253
  @dataclass
@@ -321,6 +326,9 @@ class Agent:
321
326
  self.read_only = False # only read-only shell commands
322
327
  self.no_changes = "" # set during a review: why nothing may change (read-only tools only)
323
328
  self.cancel: threading.Event | None = None # set from another thread to stop
329
+ self.steering: Steering | None = None # notes typed and pauses asked for during a request (lcode.repl)
330
+ self.turn_requests: list[str] = [] # the request, then the notes added to it while it ran
331
+ self.unsent = "" # typed during the last request but never sent to the model: back in the prompt (lcode.repl)
324
332
  self.on_tool = None # called with (name, arguments) before each tool runs
325
333
  self.jobs = Jobs() # background commands (shared with subagents)
326
334
  self.turn_started = 0.0 # when the running request started (time.monotonic)
@@ -749,7 +757,7 @@ class Agent:
749
757
  """Stream one model response, rendering thinking/content live. Returns the assistant message."""
750
758
  content, thinking, tool_calls, final = "", "", [], {}
751
759
  md = MarkdownStreamer(self.console)
752
- progress = Progress()
760
+ progress = Progress(steering=self.steering)
753
761
  watch = self.before_request(progress)
754
762
  status = None
755
763
  printed_thinking = False
@@ -845,15 +853,10 @@ class Agent:
845
853
  diff, changed = review.diff_of(self.tools.before, self.current_text, budget)
846
854
  if changed < review.MIN_CHANGED_LINES:
847
855
  return []
848
- request = next(
849
- (
850
- context_tools.request_text(str(m.get("content", "")))
851
- for m in reversed(self.messages)
852
- if m.get("role") == "user"
853
- and not str(m.get("content", "")).startswith(("[lcode]", sessions.SUMMARY_PREFIX))
854
- ),
855
- "",
856
- )
856
+ request = self.turn_requests[0] if self.turn_requests else ""
857
+ if len(self.turn_requests) > 1:
858
+ added = "\n".join(f"- {note}" for note in self.turn_requests[1:])
859
+ request += f"\n\nWhat the user added while you worked:\n{added}"
857
860
  try:
858
861
  with self.console.status("Reviewing the change against your request…", spinner="dots"):
859
862
  problems = review.run(self, request, answer, diff)
@@ -1069,6 +1072,7 @@ class Agent:
1069
1072
  self.turn_status = "running"
1070
1073
  note = planning.NOTE if self.planning() and self.allowed_tools is None else ""
1071
1074
  self.messages.append({"role": "user", "content": self.expand_mentions(user_text) + note})
1075
+ self.turn_requests = [user_text.strip()]
1072
1076
  self.repeats.reset()
1073
1077
  self.check_results = []
1074
1078
  self.review_results = None
@@ -1081,6 +1085,8 @@ class Agent:
1081
1085
  for step in range(self.max_steps):
1082
1086
  if self.cancel is not None and self.cancel.is_set():
1083
1087
  raise KeyboardInterrupt
1088
+ if step and self.steering is not None:
1089
+ self.steer()
1084
1090
  if step and self.autosave:
1085
1091
  self.save() # a crash or a closed terminal loses at most this step
1086
1092
  self.maybe_compact()
@@ -1126,6 +1132,8 @@ class Agent:
1126
1132
  )
1127
1133
  continue
1128
1134
  if not calls:
1135
+ if self.steering is not None and self.steering.waiting():
1136
+ continue # the user added a note while the model wrote its answer: it reads the note first
1129
1137
  check = self.project_check() if changed and not verified else None
1130
1138
  if check is not None and check_rounds < checks.MAX_ROUNDS:
1131
1139
  check_rounds += 1
@@ -1241,6 +1249,34 @@ class Agent:
1241
1249
  self.turn_status = "success"
1242
1250
  self.print_stats()
1243
1251
 
1252
+ def steer(self) -> None:
1253
+ """Between two steps: pause if the user pressed Esc, then hand the model the notes typed meanwhile."""
1254
+ steering = self.steering
1255
+ if steering is None:
1256
+ return
1257
+ if steering.take_pause():
1258
+ self.console.print(
1259
+ Text(
1260
+ "⏸ Paused. Press Enter to go on, type a note to go on with it, or press Ctrl+C to stop the request.",
1261
+ style="yellow",
1262
+ )
1263
+ )
1264
+ note = steering.ask(" ⏸ ").strip()
1265
+ if note:
1266
+ steering.add(note)
1267
+ notes = steering.take()
1268
+ if not notes:
1269
+ return
1270
+ self.turn_requests += notes
1271
+ self.messages.append({"role": "user", "content": "\n\n".join(notes) + "\n\n" + NOTE_INTRO})
1272
+ for note in notes:
1273
+ shown = note if len(note) <= 200 else note[:200] + "…"
1274
+ self.console.print(Text(f"✎ Note for the model: {shown}", style="cyan"))
1275
+
1276
+ def activity(self, phase: str):
1277
+ """A spinner with the elapsed time while something runs (a command), with the note being typed under it."""
1278
+ return self.console.status(Progress(phase, self.steering, tokens=False), spinner="dots")
1279
+
1244
1280
  def print_stats(self) -> None:
1245
1281
  pct = 100 * self.ctx_used / self.settings.context
1246
1282
  self.console.print(
@@ -198,6 +198,7 @@ class Permissions:
198
198
  self.always: set[str] = set()
199
199
  self.rules = Rules() # allow and deny lists from the settings (lcode.hooks)
200
200
  self.on_prompt = None # called with the title before lcode asks the user (notification hooks)
201
+ self.read_line: Callable[[str], str] | None = None # asks instead of input() (lcode.steer, during a request)
201
202
  # Decides instead of asking (lcode.api, lcode acp): True, False, or a message saying why not.
202
203
  self.approve: Callable[[dict], bool | str] | None = None
203
204
 
@@ -243,7 +244,8 @@ class Permissions:
243
244
  if kind == "mcp":
244
245
  scope = scope.replace(":", " › ", 1)
245
246
  try:
246
- answer = input(f" Allow? [y]es / [a]lways for '{scope}' this session / [n]o (+ optional reason): ")
247
+ question = f" Allow? [y]es / [a]lways for '{scope}' this session / [n]o (+ optional reason): "
248
+ answer = (self.read_line or input)(question)
247
249
  except EOFError:
248
250
  return False, "The user denied this action (no input available)."
249
251
  answer = answer.strip()