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.
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/CHANGELOG.md +34 -1
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/PKG-INFO +11 -10
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/README.md +10 -9
- lcode_cli-0.33.1/docs/assets/extra.css +60 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/index.md +3 -2
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/quickstart.md +1 -1
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/usage.md +42 -3
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/mkdocs.yml +2 -2
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/pyproject.toml +3 -0
- lcode_cli-0.33.1/scripts/make_logos.py +62 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/scripts/social-preview.html +7 -19
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/__init__.py +1 -1
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/agent.py +50 -14
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/permissions.py +3 -1
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/repl.py +30 -4
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/sessions.py +3 -1
- lcode_cli-0.33.1/src/lcode/steer.py +287 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/tools.py +4 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_sessions.py +2 -0
- lcode_cli-0.33.1/tests/test_steer.py +372 -0
- lcode_cli-0.32.0/docs/assets/extra.css +0 -29
- lcode_cli-0.32.0/docs/assets/logo.svg +0 -15
- lcode_cli-0.32.0/docs/assets/social-preview.png +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.editorconfig +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/CODEOWNERS +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/dependabot.yml +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/workflows/ci.yml +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/workflows/docs.yml +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.github/workflows/release.yml +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.gitignore +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/.pre-commit-config.yaml +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/AGENTS.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/CODE_OF_CONDUCT.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/CONTRIBUTING.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/LICENSE +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/SECURITY.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/action.yml +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/agents.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/assets/demo.svg +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/automation.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/bench.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/changelog.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/commands.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/configuration.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/contributing.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/editors.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/git.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/hooks.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/how-it-works.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/installation.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/lsp.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/mcp.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/memory.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/models.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/sandbox.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/search.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/servers.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/docs/troubleshooting.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/examples/README.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/examples/api/fix_tests.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/examples/commands/review.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/examples/skills/commit-message/SKILL.md +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/examples/skills/commit-message/scripts/staged.sh +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/install.sh +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/overrides/main.html +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/scripts/record_demo.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/__main__.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/acp.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/action.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/api.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/backends.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/bench.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/bench_backend.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/bench_runtime.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/catalog.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/checkpoints.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/checks.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/cli.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/clipboard.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/codesearch.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/config.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/context.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/extensions.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/frontmatter.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/gitflow.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/hardware.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/hooks.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/jobs.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/limits.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/lsp.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/matching.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/__init__.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/auth.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/catalog.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/catalog.toml +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/client.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/commands.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/config.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/http.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/manager.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/protocol.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/server.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/mcp/stdio.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/memory.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/models.toml +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/notify.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/ollama.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/planning.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/render.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/repeats.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/repomap.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/residency.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/review.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/sandbox.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/secretscan.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/servers/__init__.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/servers/encord.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/servers/valohai.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/servers/xai.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/subagents.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/update.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/vision.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/src/lcode/web.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/conftest.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/fake_lsp_server.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/fake_mcp_server.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_acp.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_action.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_agent.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_api.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_backends.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_bench.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_catalog.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_checkpoints.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_checks.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_cli.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_clipboard.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_codesearch.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_config.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_context.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_context_picker.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_extensions.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_gitflow.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_hooks.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_jobs.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_lsp.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_matching.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_mcp.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_memory.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_permissions.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_plan_mode.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_repeats.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_repomap.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_residency.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_review.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_robustness.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_sandbox.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_secretscan.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_servers.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_subagents.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_tools.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_update.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_vision.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_web.py +0 -0
- {lcode_cli-0.32.0 → lcode_cli-0.33.1}/tests/test_xai.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
{ .hero-logo }
|
|
11
|
+
{ .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.
|
|
@@ -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
|
|
235
|
-
⠋ Reading the conversation (18K tokens)… (0 tokens, 21s · Ctrl+C
|
|
236
|
-
⠋ Thinking… (312 tokens, 27s · Ctrl+C
|
|
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 © 2026 Naser Derakhshan · <a href="https://github.com
|
|
|
9
9
|
theme:
|
|
10
10
|
name: material
|
|
11
11
|
custom_dir: overrides
|
|
12
|
-
logo: assets/
|
|
13
|
-
favicon: assets/
|
|
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: #
|
|
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(
|
|
18
|
-
radial-gradient(700px 500px at 10% 90%, rgba(
|
|
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 {
|
|
29
|
-
.brand
|
|
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(--
|
|
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>
|
|
@@ -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
|
-
|
|
244
|
-
|
|
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 =
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
|
|
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()
|