lcode-cli 0.38.0__tar.gz → 0.39.0__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 (181) hide show
  1. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/CHANGELOG.md +23 -1
  2. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/PKG-INFO +1 -1
  3. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/configuration.md +2 -0
  4. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/mcp.md +5 -81
  5. lcode_cli-0.39.0/docs/modes.md +58 -0
  6. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/quickstart.md +1 -1
  7. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/usage.md +4 -3
  8. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/mkdocs.yml +1 -0
  9. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/__init__.py +1 -1
  10. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/agent.py +5 -0
  11. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/api.py +3 -0
  12. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/cli.py +12 -0
  13. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/config.py +5 -0
  14. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/mcp/catalog.toml +0 -48
  15. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/mcp/commands.py +6 -0
  16. lcode_cli-0.39.0/src/lcode/modes.py +102 -0
  17. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/repl.py +57 -10
  18. lcode_cli-0.39.0/tests/test_modes.py +154 -0
  19. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_xai.py +0 -11
  20. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/.editorconfig +0 -0
  21. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/.github/CODEOWNERS +0 -0
  22. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  23. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  24. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  25. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
  26. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  27. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/.github/dependabot.yml +0 -0
  28. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/.github/workflows/ci.yml +0 -0
  29. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/.github/workflows/docs.yml +0 -0
  30. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/.github/workflows/release.yml +0 -0
  31. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/.gitignore +0 -0
  32. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/.pre-commit-config.yaml +0 -0
  33. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/AGENTS.md +0 -0
  34. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/CODE_OF_CONDUCT.md +0 -0
  35. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/CONTRIBUTING.md +0 -0
  36. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/LICENSE +0 -0
  37. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/README.md +0 -0
  38. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/SECURITY.md +0 -0
  39. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/action.yml +0 -0
  40. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/agents.md +0 -0
  41. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/assets/demo.svg +0 -0
  42. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/assets/extra.css +0 -0
  43. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/automation.md +0 -0
  44. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/bench.md +0 -0
  45. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/changelog.md +0 -0
  46. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/commands.md +0 -0
  47. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/contributing.md +0 -0
  48. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/editors.md +0 -0
  49. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/git.md +0 -0
  50. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/hooks.md +0 -0
  51. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/how-it-works.md +0 -0
  52. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/index.md +0 -0
  53. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/installation.md +0 -0
  54. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/lsp.md +0 -0
  55. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/memory.md +0 -0
  56. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/models.md +0 -0
  57. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/sandbox.md +0 -0
  58. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/search.md +0 -0
  59. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/servers.md +0 -0
  60. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/docs/troubleshooting.md +0 -0
  61. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/examples/README.md +0 -0
  62. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/examples/api/fix_tests.py +0 -0
  63. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/examples/commands/review.md +0 -0
  64. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/examples/skills/commit-message/SKILL.md +0 -0
  65. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
  66. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/install.sh +0 -0
  67. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/overrides/main.html +0 -0
  68. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/pyproject.toml +0 -0
  69. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/scripts/make_logos.py +0 -0
  70. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/scripts/record_demo.py +0 -0
  71. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/scripts/social-preview.html +0 -0
  72. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/__main__.py +0 -0
  73. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/acp.py +0 -0
  74. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/action.py +0 -0
  75. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/backends.py +0 -0
  76. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/bench.py +0 -0
  77. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/bench_backend.py +0 -0
  78. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/bench_runtime.py +0 -0
  79. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/catalog.py +0 -0
  80. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/checkpoints.py +0 -0
  81. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/checks.py +0 -0
  82. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/clipboard.py +0 -0
  83. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/cloud.py +0 -0
  84. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/codesearch.py +0 -0
  85. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/context.py +0 -0
  86. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/escalate.py +0 -0
  87. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/extensions.py +0 -0
  88. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/frontmatter.py +0 -0
  89. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/gitflow.py +0 -0
  90. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/hardware.py +0 -0
  91. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/hooks.py +0 -0
  92. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/jobs.py +0 -0
  93. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/limits.py +0 -0
  94. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/lsp.py +0 -0
  95. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/matching.py +0 -0
  96. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/mcp/__init__.py +0 -0
  97. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/mcp/auth.py +0 -0
  98. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/mcp/catalog.py +0 -0
  99. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/mcp/client.py +0 -0
  100. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/mcp/config.py +0 -0
  101. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/mcp/http.py +0 -0
  102. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/mcp/manager.py +0 -0
  103. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/mcp/protocol.py +0 -0
  104. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/mcp/server.py +0 -0
  105. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/mcp/stdio.py +0 -0
  106. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/memory.py +0 -0
  107. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/models.toml +0 -0
  108. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/notify.py +0 -0
  109. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/ollama.py +0 -0
  110. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/permissions.py +0 -0
  111. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/planning.py +0 -0
  112. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/render.py +0 -0
  113. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/repeats.py +0 -0
  114. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/repomap.py +0 -0
  115. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/residency.py +0 -0
  116. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/review.py +0 -0
  117. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/sandbox.py +0 -0
  118. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/screenshots.py +0 -0
  119. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/secretscan.py +0 -0
  120. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/servers/__init__.py +0 -0
  121. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/servers/encord.py +0 -0
  122. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/servers/valohai.py +0 -0
  123. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/servers/xai.py +0 -0
  124. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/sessions.py +0 -0
  125. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/steer.py +0 -0
  126. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/subagents.py +0 -0
  127. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/tools.py +0 -0
  128. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/unreachable.py +0 -0
  129. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/update.py +0 -0
  130. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/vision.py +0 -0
  131. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/src/lcode/web.py +0 -0
  132. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/conftest.py +0 -0
  133. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/fake_lsp_server.py +0 -0
  134. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/fake_mcp_server.py +0 -0
  135. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_acp.py +0 -0
  136. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_action.py +0 -0
  137. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_agent.py +0 -0
  138. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_api.py +0 -0
  139. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_backends.py +0 -0
  140. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_bench.py +0 -0
  141. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_catalog.py +0 -0
  142. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_checkpoints.py +0 -0
  143. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_checks.py +0 -0
  144. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_cli.py +0 -0
  145. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_clipboard.py +0 -0
  146. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_cloud.py +0 -0
  147. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_codesearch.py +0 -0
  148. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_config.py +0 -0
  149. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_context.py +0 -0
  150. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_context_picker.py +0 -0
  151. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_escalate.py +0 -0
  152. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_extensions.py +0 -0
  153. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_gitflow.py +0 -0
  154. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_hooks.py +0 -0
  155. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_jobs.py +0 -0
  156. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_look.py +0 -0
  157. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_lsp.py +0 -0
  158. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_matching.py +0 -0
  159. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_mcp.py +0 -0
  160. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_memory.py +0 -0
  161. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_model_picker.py +0 -0
  162. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_permissions.py +0 -0
  163. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_plan_mode.py +0 -0
  164. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_repeats.py +0 -0
  165. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_repomap.py +0 -0
  166. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_residency.py +0 -0
  167. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_review.py +0 -0
  168. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_robustness.py +0 -0
  169. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_sandbox.py +0 -0
  170. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_screenshots.py +0 -0
  171. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_secretscan.py +0 -0
  172. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_servers.py +0 -0
  173. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_sessions.py +0 -0
  174. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_steer.py +0 -0
  175. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_stuck.py +0 -0
  176. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_subagents.py +0 -0
  177. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_tools.py +0 -0
  178. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_update.py +0 -0
  179. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_vision.py +0 -0
  180. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/tests/test_web.py +0 -0
  181. {lcode_cli-0.38.0 → lcode_cli-0.39.0}/uv.lock +0 -0
@@ -6,6 +6,27 @@ All notable changes to lcode are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.39.0] - 2026-10-23
10
+
11
+ ### Added
12
+
13
+ - **Modes: code, image and video** (#148). `/mode image`, `/mode video` and `/mode code` switch
14
+ what lcode makes. `lcode --mode` starts in one, and the `mode` setting is the default. The
15
+ banner, status bar and prompt show the mode, and sessions keep theirs.
16
+ - **The GPU goes to one model at a time:** switching to image or video mode unloads the coding
17
+ model, and switching back asks ComfyUI to free its memory.
18
+ - **Image and video mode don't generate yet:** they say what's coming (#149–#157) instead of
19
+ sending your words to the coding model. See [Modes](https://nasser1941.github.io/lcode/modes/).
20
+
21
+ ### Changed
22
+
23
+ - **Code mode makes code only.** The MCP servers that generated images and videos (`comfyui`,
24
+ `xai`) are out of the MCP catalog, and lcode doesn't load them in code mode even if they're
25
+ still in `mcp.json` (it says so once). Image and video generation are moving to their own modes.
26
+ - **`/mode` switches code, image and video.** The permission mode is set with `/permissions` now;
27
+ `/mode yolo` and the like still work, with a note. Shift+Tab cycles the permission mode as
28
+ before.
29
+
9
30
  ## [0.38.0] - 2026-10-21
10
31
 
11
32
  ### Added
@@ -855,7 +876,8 @@ First public release.
855
876
  - `AGENTS.md` project instructions and `/init` to generate them.
856
877
  - One-line installer for Linux and macOS.
857
878
 
858
- [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.38.0...HEAD
879
+ [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.39.0...HEAD
880
+ [0.39.0]: https://github.com/nasser1941/lcode/compare/v0.38.0...v0.39.0
859
881
  [0.38.0]: https://github.com/nasser1941/lcode/compare/v0.37.0...v0.38.0
860
882
  [0.37.0]: https://github.com/nasser1941/lcode/compare/v0.36.0...v0.37.0
861
883
  [0.36.0]: https://github.com/nasser1941/lcode/compare/v0.35.1...v0.36.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lcode-cli
3
- Version: 0.38.0
3
+ Version: 0.39.0
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/
@@ -23,6 +23,8 @@ lcode config path # print the file location
23
23
  | `backend` | `ollama` | The model server: `ollama`, `lmstudio`, `llama.cpp`, `vllm`, `mlx` or `openai` (any OpenAI-compatible API); see [Other model servers](servers.md) |
24
24
  | `base_url` | the backend's | The server's OpenAI-compatible address, e.g. `http://localhost:1234/v1` |
25
25
  | `permission_mode` | `ask` | `ask`, `plan`, `auto-edit` or `yolo` |
26
+ | `mode` | `code` | What lcode makes when it starts: `code`, `image` or `video` ([modes](modes.md)) |
27
+ | `comfyui_url` | `http://127.0.0.1:8188` | ComfyUI's address, for image and video mode |
26
28
  | `think` | `true` | Let the model reason before answering |
27
29
  | `web` | `on` | Web search and page fetching: `on`, `ask` (before each search/website) or `off` |
28
30
  | `search_backend` | `auto` | `auto`, `ollama`, `brave`, `tavily` or `searxng`; `auto` uses the first one configured |
@@ -28,8 +28,6 @@ and find the code for the top one."*
28
28
  | `gcp` | `gcloud` commands on your Google Cloud projects | `npx` and the `gcloud` CLI, signed in |
29
29
  | `github` | Repositories, issues, pull requests, Actions | a GitHub token (or the GitHub CLI) |
30
30
  | `playwright` | A real (headless) browser: open pages, click, fill forms | `npx` |
31
- | `comfyui` | Generate and edit images with local models (FLUX, SDXL, SD 1.5, Qwen-Image) | `uvx` and [ComfyUI](#images-with-comfyui) running locally |
32
- | `xai` | Generate and edit images, and generate videos and speech, with xAI's Grok Imagine (paid, in the cloud) | `uvx`, an xAI API key ([below](#images-videos-and-speech-with-xai)) |
33
31
  | `context7` | Up-to-date docs and examples for thousands of libraries | nothing (an API key is optional) |
34
32
  | `sentry` | Errors, issues, traces and releases | a Sentry account (browser sign-in) |
35
33
  | `postgres` | Schemas, read-only queries, query performance | `uvx`, a connection URL |
@@ -87,86 +85,12 @@ Google Cloud project:
87
85
  See [Google's guide](https://developers.google.com/workspace/guides/configure-mcp-servers) for
88
86
  details.
89
87
 
90
- ### Images with ComfyUI
88
+ ### Images and videos
91
89
 
92
- [ComfyUI](https://github.com/comfyanonymous/ComfyUI) runs image generation and editing models
93
- locally; its official MCP server lets lcode use it, for app icons, illustrations, mockups or
94
- placeholder art.
95
-
96
- ```bash
97
- uv tool install comfy-cli && comfy install # ComfyUI, once
98
- comfy launch --background -- --disable-smart-memory # start it (http://127.0.0.1:8188)
99
- lcode mcp add comfyui
100
- ```
101
-
102
- Then add models in ComfyUI (its model manager, or ask lcode: *"download FLUX.2 Klein 4B in
103
- ComfyUI"*), and ask for images: *"make a 512×512 app icon of a paper plane and save it in
104
- assets/"*. To edit an image, ask lcode to upload it and run an editing workflow (Kontext or
105
- Qwen-Image-Edit).
106
-
107
- | Model | Good for | On a 12 GB GPU |
108
- |---|---|---|
109
- | FLUX.2 Klein 4B | Generation and editing, fast | Fits (about 8 GB) |
110
- | SDXL and its community models | Generation, inpainting | Fits |
111
- | Stable Diffusion 1.5 and its community models | Light generation, inpainting | Fits easily |
112
- | FLUX.1 Dev and finetunes | High-quality generation | Needs an fp8 or GGUF version; slower |
113
- | FLUX.1 Kontext Dev | Editing an image from instructions | Needs an fp8 or GGUF version; slower |
114
- | Qwen-Image / Qwen-Image-Edit | Generation and editing, good text in images | Heavy: a GGUF version and RAM offloading |
115
-
116
- Only ComfyUI's local tools are turned on; Comfy Cloud's partner tools are left out, so prompts and
117
- images stay on your machine.
118
-
119
- **Sharing one GPU.** A GPU rarely holds a coding model and an image model at the same time, so the
120
- two take turns: before ComfyUI generates, lcode unloads its own model (the `free_gpu` setting), and
121
- ComfyUI started with `--disable-smart-memory` gives the GPU back after each image. lcode's model
122
- then reloads for the next step, which adds a few seconds to half a minute per image request. With
123
- enough GPU memory for both (or on a Mac with plenty of memory), remove `free_gpu` from the server's
124
- settings in `mcp.json`. lcode can also look at the results ([Images](usage.md#images)).
125
-
126
- Measured on an RTX 4080 Laptop GPU (12 GB) with qwen3.6-35b at 64K context and FLUX.2 Klein Base 4B
127
- (fp8, 512×512, 20 steps):
128
-
129
- | | |
130
- |---|---|
131
- | Generating while lcode's model is loaded | Fails: ComfyUI runs out of GPU memory |
132
- | Freeing the GPU (lcode unloads its model) | 0.2 s |
133
- | Generating one image | about 12 s |
134
- | Reloading lcode's model afterwards | about 20 s |
135
-
136
- The first time, the model has to find the right template and fill in its settings (model file names,
137
- size, prompt), which can take several minutes of trial and error. Once an image comes out right, ask
138
- lcode to save that workflow in your project (for example `assets/icon.workflow.json`) and reuse it:
139
- later images are a single `run_workflow` call.
140
-
141
- ### Images, videos and speech with xAI
142
-
143
- xAI's [Grok Imagine](https://docs.x.ai/developers/model-capabilities/imagine) and
144
- [text to speech](https://docs.x.ai/developers/models/text-to-speech) APIs make images, videos and
145
- speech in the cloud: no GPU needed, but each call is billed to your xAI account. lcode ships an MCP
146
- server for them, `lcode-mcp-xai`.
147
-
148
- ```bash
149
- lcode mcp add xai # asks for your xAI API key (create one at https://console.x.ai)
150
- ```
151
-
152
- The key is typed hidden and kept in `~/.config/lcode/mcp.json`, readable only by you. If
153
- `XAI_API_KEY` is already set in your environment, lcode uses that and doesn't store the key.
154
-
155
- | Tool | | Costs |
156
- |---|---|---|
157
- | `generate_image` | Images from a description: 1–10 at a time, aspect ratio, 1k or 2k | $0.02–0.05 an image |
158
- | `edit_image` | Change a local image as described | the same |
159
- | `generate_video` | A 1–15 second MP4 from a description, optionally starting from a local image, up to 1080p. Waits for it (a minute or more) | $0.02–0.08 a second |
160
- | `video_status` | Fetch a video that wasn't ready within 10 minutes | |
161
- | `text_to_speech` | Speech in one of 26 voices and 20 languages, as MP3 or WAV | per character |
162
-
163
- Files are saved in `generated/` in the project, or where you ask (*"generate an app icon and save
164
- it as assets/icon.png"*). xAI has no music generation API.
165
-
166
- Your prompts, and the images you edit or animate, go to xAI. lcode asks before each call, since each
167
- one costs money. To stop asking for some tools, list them under `"allow"` for the server in
168
- `mcp.json`, for example `"allow": ["generate_image", "text_to_speech"]`. xAI applies its own usage
169
- policies to what it generates; when it refuses a prompt, lcode passes its message to the model.
90
+ Code mode makes code only, so the servers that generated images and videos in earlier versions
91
+ (`comfyui`, `xai`) aren't in the catalog any more, and lcode doesn't load them in code mode if
92
+ they're still in your `mcp.json`. Image and video generation get their own [modes](modes.md),
93
+ with their own models and controls.
170
94
 
171
95
  ### Metabase
172
96
 
@@ -0,0 +1,58 @@
1
+ # Modes: code, image and video
2
+
3
+ lcode makes three kinds of things, each in its own mode with its own models:
4
+
5
+ | Mode | What it makes | The model doing the work |
6
+ |---|---|---|
7
+ | **code** (default) | Code: it reads, writes, runs and checks your project | A coding language model in Ollama (or another server) |
8
+ | **image** | Images from a description and reference images | An image model, run by ComfyUI on your GPU |
9
+ | **video** | Videos from a description, a start image or references | A video model, run by ComfyUI on your GPU |
10
+
11
+ ```text
12
+ ❯ /mode image
13
+ Mode: image (local image generation).
14
+ Unloaded qwen3.6-35b to free the GPU for images.
15
+ image ❯
16
+ ```
17
+
18
+ - **Switching:** `/mode code`, `/mode image` and `/mode video` switch in a session. `lcode --mode image`
19
+ starts in one, and `lcode config set mode video` makes it the default. `/mode` alone shows
20
+ the current one.
21
+ - **Where it shows:** the banner, the status bar and the prompt (`image ❯`) show the mode.
22
+ - **Sessions:** a session keeps the mode it was made in, and resuming it brings that mode back.
23
+
24
+ ## Code mode makes code only
25
+
26
+ Code mode is the coding agent, as before, and it no longer generates images or videos. The MCP
27
+ servers that did (`comfyui` and `xai`) are gone from the MCP catalog, and lcode doesn't load them
28
+ in code mode even if they're still in your `mcp.json`. It says so once.
29
+
30
+ ## One model on the GPU at a time
31
+
32
+ Image and video modes don't need the big coding model: the image or video model does the work. A
33
+ small optional model will help with prompts later. Image and video models need the GPU to
34
+ themselves:
35
+
36
+ - **Into image or video mode:** lcode unloads its coding model from Ollama.
37
+ - **Back to code mode:** lcode asks ComfyUI to free its memory (at `comfyui_url`, by default
38
+ `http://127.0.0.1:8188`), then loads the coding model again in the background.
39
+
40
+ ## Coming next
41
+
42
+ Image and video modes don't generate yet. Until the items below arrive, a request in those modes
43
+ says what's coming instead of going anywhere.
44
+
45
+ - **#149 Local generation engine:** lcode drives ComfyUI itself: finding or installing it, jobs,
46
+ progress, cancelling, and saving outputs with their settings.
47
+ - **#150 Media model catalog:** image and video models that fit your machine, installed on
48
+ demand.
49
+ - **#151, #152 Image mode:** generating, variations, sizes and seeds, a gallery, editing,
50
+ references, inpainting, upscaling.
51
+ - **#153, #154 Video mode:** from text or a start image, first and last frames, several
52
+ references, camera moves, longer clips, sound.
53
+ - **#155 Movies:** storyboards with a consistent cast, shots stitched into one film.
54
+ - **#156 Prompt assistant:** a small local model for prompts and follow-ups.
55
+ - **#157 Cloud generation**, with costs shown (Grok Imagine first).
56
+
57
+ The permission mode (`ask`, `plan`, `auto-edit`, `yolo`) is a different setting: `/permissions`
58
+ sets it, and Shift+Tab cycles it as before.
@@ -12,7 +12,7 @@ lcode
12
12
 
13
13
  ```text
14
14
  ╭──────────────────────────────────────────────╮
15
- │ lcode v0.38.0 — local coding agent │
15
+ │ lcode v0.39.0 — local coding agent │
16
16
  │ │
17
17
  │ model lcode-qwen3.6-35b │
18
18
  │ context 256K tokens │
@@ -79,7 +79,8 @@ reasoning is on.
79
79
  | `/models`, `/model [number or name]` | List the models and how they fit this machine, numbered: type a number to switch, or `/model 2`, `/model qwen3.5-9b` |
80
80
  | `/think [on\|off]` | Toggle reasoning |
81
81
  | `/verbose` | Toggle showing the reasoning text |
82
- | `/mode [ask\|plan\|auto-edit\|yolo]` | Set the permission mode |
82
+ | `/mode [code\|image\|video]` | Switch what lcode makes: [code, images or videos](modes.md) |
83
+ | `/permissions [ask\|plan\|auto-edit\|yolo]` | Set the permission mode |
83
84
  | `/cd DIR` | Change the working directory |
84
85
  | `/todos` | Show the model's task list |
85
86
  | `/status` | The task sheet: your latest requests, the plan and the model's notes, files changed, the last check |
@@ -315,7 +316,7 @@ stays loaded:
315
316
  | The model was loaded | `◆ Loaded qwen3.6-35b in 15s: 22.3 GB, 45% on the GPU and the rest in system RAM.`, and a warning when less of it is on the GPU than usual, because another program or model is using GPU memory |
316
317
  | A dense model doesn't fit on the GPU (at startup and on `/model`) | `⚠ qwen3.8-27b is a dense model: all 27B parameters work on every token. It's capable, but slow here. Only about 61% of it fits in the 12 GB of GPU memory…`, with what to do instead |
317
318
  | The model that looks at images needs the memory | `Loading the vision model … to look at bug.png; qwen3.6-35b makes room and reloads afterwards`, then `◆ qwen3.6-35b was unloaded to make room…`, and the next step says `Reloading qwen3.6-35b into memory (it made room for the vision model …)` |
318
- | A tool needs the GPU to itself, such as image generation | `◆ Unloaded qwen3.6-35b so comfyui can use the GPU; it reloads afterwards.` |
319
+ | A tool needs the GPU to itself (an MCP server with `free_gpu`) | `◆ Unloaded qwen3.6-35b so the server can use the GPU; it reloads afterwards.` |
319
320
  | Ollama ran out of memory | `⚠` what ran out (the prompt batch, or the context next to the model), what lcode retries with, and what it remembers for next time |
320
321
 
321
322
  While the model loads in the background at startup, the status bar says
@@ -470,7 +471,7 @@ a screen and sees what you would, measured:
470
471
  | `auto-edit` | automatic | ask, except read-only commands |
471
472
  | `yolo` | automatic | automatic |
472
473
 
473
- ++shift+tab++ cycles through them in this order, `/mode` sets one, and `--plan`, `--auto-edit` and
474
+ ++shift+tab++ cycles through them in this order, `/permissions` sets one, and `--plan`, `--auto-edit` and
474
475
  `--yolo` start in one.
475
476
 
476
477
  Read-only commands run without asking: `ls`, `cat`, `head`, `tail`, `grep`, `rg`, `find`, `tree`,
@@ -92,6 +92,7 @@ nav:
92
92
  - Quickstart: quickstart.md
93
93
  - Guide:
94
94
  - Using lcode: usage.md
95
+ - Modes (code, image, video): modes.md
95
96
  - Memory: memory.md
96
97
  - Subagents: agents.md
97
98
  - Commands and skills: commands.md
@@ -3,7 +3,7 @@
3
3
  From a program: `from lcode import Session` (see lcode.api).
4
4
  """
5
5
 
6
- __version__ = "0.38.0"
6
+ __version__ = "0.39.0"
7
7
 
8
8
  API = ("Session", "Result", "Options", "SetupError")
9
9
 
@@ -304,6 +304,8 @@ class Progress:
304
304
  class Settings:
305
305
  model: str # Ollama model name to call
306
306
  context: int
307
+ mode: str = "code" # what lcode makes: code, image or video (lcode.modes)
308
+ comfyui_url: str = "http://127.0.0.1:8188"
307
309
  num_batch: int | None = None
308
310
  keep_alive: str = "30m"
309
311
  think: bool = True
@@ -680,6 +682,7 @@ class Agent:
680
682
  data = {
681
683
  "cwd": str(self.workspace), # where `lcode -c` there finds it, wherever the shell moved
682
684
  "model": self.settings.model,
685
+ "mode": self.settings.mode,
683
686
  "name": self.session_name,
684
687
  "title": self.session_title,
685
688
  "messages": self.messages,
@@ -726,6 +729,8 @@ class Agent:
726
729
  self.session_id = info.id
727
730
  self.checkpoints.load(info.id, data.get("checkpoints") or [])
728
731
  self.session_name = data.get("name", "")
732
+ if data.get("mode") in ("code", "image", "video"):
733
+ self.settings.mode = data["mode"] # a session belongs to the mode it was made in
729
734
  self.session_title = data.get("title") or sessions.title_from(self.messages)
730
735
  self.tools.read_mtimes.clear() # files may have changed since; the model must read them again
731
736
  self.tools.todos = list(data.get("todos") or [])
@@ -54,6 +54,7 @@ class Options:
54
54
  model: str | None = None
55
55
  context: int | str | None = None
56
56
  permission_mode: str | None = None # ask, plan, auto-edit or yolo
57
+ mode: str | None = None # code, image or video
57
58
  allowed_tools: list[str] | None = None # only these tools (default: all)
58
59
  max_steps: int | None = None # model steps per request
59
60
  think: bool | None = None
@@ -179,6 +180,8 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
179
180
  think=cfg["think"] if options.think is None else options.think,
180
181
  show_thinking=options.show_thinking,
181
182
  permission_mode=options.permission_mode or cfg["permission_mode"],
183
+ mode=options.mode or cfg["mode"],
184
+ comfyui_url=cfg["comfyui_url"],
182
185
  web=switch(options.web, cfg["web"], "on"),
183
186
  search_backend=cfg["search_backend"],
184
187
  searxng_url=cfg["searxng_url"],
@@ -714,6 +714,7 @@ def cmd_chat(args) -> None:
714
714
  model=args.model,
715
715
  context=args.context,
716
716
  permission_mode=mode,
717
+ mode=args.mode,
717
718
  allowed_tools=tools,
718
719
  max_steps=args.max_steps,
719
720
  think=False if args.no_think else None,
@@ -733,6 +734,16 @@ def cmd_chat(args) -> None:
733
734
  sys.exit(1)
734
735
  fail(str(e))
735
736
  exit_code = 0
737
+ if args.prompt and agent.settings.mode != "code":
738
+ from lcode import modes
739
+
740
+ message = modes.COMING[agent.settings.mode]
741
+ if quiet:
742
+ print(json.dumps({"type": "result", "status": "error", "error": message}), flush=True)
743
+ else:
744
+ out.print(f"[yellow]{escape(message)}[/]")
745
+ api.close_agent(agent)
746
+ sys.exit(2)
736
747
  if args.escalate and args.prompt:
737
748
  agent.escalate_approved = True # the flag is the user's OK for this run
738
749
  try:
@@ -798,6 +809,7 @@ def build_parser() -> argparse.ArgumentParser:
798
809
  help="work in a git worktree of its own, on branch NAME (new, or continue one); removed if nothing changed",
799
810
  )
800
811
  parser.add_argument("--plan", action="store_true", help="start in plan mode: agree on a plan before any change")
812
+ parser.add_argument("--mode", choices=config.WORK_MODES, help="what lcode makes: code (default), image or video")
801
813
  parser.add_argument("--auto-edit", action="store_true", help="apply file edits without asking")
802
814
  parser.add_argument("--yolo", action="store_true", help="never ask for permission (edits and commands)")
803
815
  parser.add_argument(
@@ -23,6 +23,7 @@ else:
23
23
 
24
24
  DEFAULT_MODEL = "qwen3.6-35b"
25
25
  PERMISSION_MODES = ("ask", "plan", "auto-edit", "yolo") # Shift+Tab cycles in this order
26
+ WORK_MODES = ("code", "image", "video") # what lcode makes (lcode.modes)
26
27
  WEB_MODES = ("on", "ask", "off")
27
28
  SEARCH_BACKENDS = ("auto", "ollama", "brave", "tavily", "searxng")
28
29
  MCP_TOOL_MODES = ("auto", "direct", "search")
@@ -40,6 +41,8 @@ SETTINGS: dict[str, tuple[object, type, str]] = {
40
41
  "backend": ("ollama", str, "model server: ollama | lmstudio | llama.cpp | vllm | mlx | openai (any OpenAI API)"),
41
42
  "base_url": (None, str, "OpenAI-compatible server address, e.g. http://localhost:1234/v1 (default: the backend's)"),
42
43
  "permission_mode": ("ask", str, "ask | plan | auto-edit | yolo"),
44
+ "mode": ("code", str, "what lcode makes when it starts: code | image | video"),
45
+ "comfyui_url": ("http://127.0.0.1:8188", str, "ComfyUI, for image and video mode"),
43
46
  "think": (True, bool, "let the model reason before answering (slower, better)"),
44
47
  "web": ("on", str, "web search and page fetching for the model: on | ask | off"),
45
48
  "search_backend": ("auto", str, "auto | ollama | brave | tavily | searxng (keys come from environment variables)"),
@@ -203,6 +206,8 @@ def coerce(key: str, value: object) -> object:
203
206
  raise ConfigError(f"search_backend must be one of {', '.join(SEARCH_BACKENDS)}")
204
207
  if key == "permission_mode" and value not in PERMISSION_MODES:
205
208
  raise ConfigError(f"permission_mode must be one of {', '.join(PERMISSION_MODES)}")
209
+ if key == "mode" and value not in WORK_MODES:
210
+ raise ConfigError(f"mode must be one of {', '.join(WORK_MODES)}")
206
211
  if typ is bool and isinstance(value, str):
207
212
  if value.lower() not in ("true", "false", "1", "0", "yes", "no", "on", "off"):
208
213
  raise ConfigError(f"{key} must be true or false")
@@ -130,34 +130,6 @@ requires = ["npx"]
130
130
  setup = "Runs a headless browser; the first use may download it."
131
131
  server = { command = "npx", args = ["-y", "@playwright/mcp@latest", "--headless"] }
132
132
 
133
- [comfyui]
134
- name = "ComfyUI"
135
- description = "Generate and edit images with models you run locally in ComfyUI: FLUX, SDXL, SD 1.5, Qwen-Image"
136
- homepage = "https://github.com/Comfy-Org/comfy-mcp"
137
- requires = ["uvx"]
138
- setup = """Needs ComfyUI on this machine (http://127.0.0.1:8188) with at least one image model:
139
- uv tool install comfy-cli && comfy install
140
- comfy launch --background -- --disable-smart-memory
141
- --disable-smart-memory makes ComfyUI give the GPU back after each image. Before generating, lcode
142
- unloads its own model so ComfyUI has the GPU; it reloads afterwards. Only the local tools are on;
143
- Comfy Cloud's partner tools are left out, so nothing leaves this machine."""
144
-
145
- [comfyui.server]
146
- command = "uvx"
147
- args = ["--python", "3.12", "--from", "comfy-mcp", "--with", "comfy-cli>=1.14.0", "comfy-mcp"]
148
- env = { COMFYUI_URL = "${COMFYUI_URL}" }
149
- tools = [
150
- "server_info", "generate_image", "run_workflow", "run_template", "search_templates", "get_template", "fetch_template",
151
- "list_workflow_slots", "set_workflow_slot", "job", "fetch_outputs", "upload_file", "search_models",
152
- "download_model", "system_stats", "free_memory", "launch_comfyui",
153
- ]
154
- free_gpu = ["generate_image", "run_workflow", "run_template"]
155
-
156
- [[comfyui.inputs]]
157
- var = "COMFYUI_URL"
158
- prompt = "ComfyUI address (Enter for this machine, 127.0.0.1:8188)"
159
- optional = true
160
-
161
133
  [context7]
162
134
  name = "Context7"
163
135
  description = "Current documentation and code examples for thousands of libraries"
@@ -282,23 +254,3 @@ var = "VALOHAI_HOST"
282
254
  prompt = "Valohai address (Enter for app.valohai.com; for a self-hosted installation, its URL)"
283
255
  optional = true
284
256
 
285
- [xai]
286
- name = "xAI Grok Imagine"
287
- description = "Generate and edit images, and generate videos and speech, with xAI's Grok Imagine and voice API (paid, in the cloud)"
288
- homepage = "https://nasser1941.github.io/lcode/mcp/#xai"
289
- requires = ["uvx"]
290
- setup = """Uses xAI's API: your prompts, and images you edit or animate, go to xAI, and every call is billed to your xAI
291
- account (an image $0.02-0.05, video $0.02-0.08 a second, speech per character). Create an API key at https://console.x.ai.
292
- Files are saved in generated/ in the project. lcode asks before each call; to stop asking for some tools, list them
293
- under "allow" for this server in mcp.json. xAI has no music generation."""
294
-
295
- [xai.server]
296
- command = "uvx"
297
- args = ["--isolated", "--python", "3.12", "--from", "lcode-cli", "lcode-mcp-xai"]
298
- env = { XAI_API_KEY = "${XAI_API_KEY}" }
299
- timeout = 900 # a video takes minutes
300
-
301
- [[xai.inputs]]
302
- var = "XAI_API_KEY"
303
- prompt = "xAI API key (create one at https://console.x.ai)"
304
- secret = true
@@ -60,6 +60,12 @@ def start_session(cwd: Path, tool_mode: str, console: Console, interactive: bool
60
60
  servers, problems = mcp_config.load_all(cwd, include_project=use_project)
61
61
  for problem in problems:
62
62
  console.print(f"[yellow]MCP: {escape(problem)}[/]")
63
+ from lcode import modes
64
+
65
+ servers, left_out = modes.code_mode_servers(servers) # images and videos have their own modes
66
+ note = modes.note_once(left_out)
67
+ if note and interactive:
68
+ console.print(f"[dim]{escape(note)}[/]")
63
69
  if not servers:
64
70
  return None
65
71
  manager = McpManager(cwd, servers, tool_mode)
@@ -0,0 +1,102 @@
1
+ """What lcode makes: code, images or videos.
2
+
3
+ - **code** is the coding agent: a language model with tools, and nothing else. It doesn't generate images or
4
+ videos: the MCP servers that do (`comfyui`, `xai`) aren't loaded in code mode.
5
+ - **image** and **video** generate media on the GPU with image and video models (through ComfyUI, #149). They
6
+ don't need the big coding model: lcode unloads it when you switch to them, and frees ComfyUI's memory when you
7
+ switch back, so one model has the GPU at a time.
8
+
9
+ Until the generation engine lands, image and video mode say what's coming instead of sending the prompt anywhere.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from typing import TYPE_CHECKING
16
+
17
+ import requests
18
+
19
+ from lcode import config
20
+
21
+ if TYPE_CHECKING:
22
+ from lcode.agent import Agent
23
+ from lcode.mcp.config import ServerConfig
24
+
25
+ MODES = ("code", "image", "video")
26
+ TITLES = {"code": "local coding agent", "image": "local image generation", "video": "local video generation"}
27
+ MEDIA_SERVERS = frozenset({"comfyui", "xai"}) # the MCP catalog's image and video servers
28
+ MEDIA_COMMANDS = ("comfy-mcp", "lcode-mcp-xai") # the same servers, under any name
29
+ NOTED_FILE = "media-servers-noted"
30
+ COMING = {
31
+ "image": (
32
+ "Image mode doesn't generate yet: lcode's own ComfyUI engine (#149), the image models (#150) and "
33
+ "generation (#151) come next. /mode code switches back."
34
+ ),
35
+ "video": (
36
+ "Video mode doesn't generate yet: lcode's own ComfyUI engine (#149), the video models (#150) and "
37
+ "generation (#153) come next. /mode code switches back."
38
+ ),
39
+ }
40
+
41
+
42
+ def is_media_server(server: ServerConfig) -> bool:
43
+ """An MCP server that generates images or videos: those belong to image and video mode, not code mode."""
44
+ if server.name.lower() in MEDIA_SERVERS:
45
+ return True
46
+ target = " ".join(str(part) for part in [*server.command, server.url])
47
+ return any(name in target for name in MEDIA_COMMANDS)
48
+
49
+
50
+ def code_mode_servers(servers: list[ServerConfig]) -> tuple[list[ServerConfig], list[str]]:
51
+ """The servers code mode loads, and the names of the media servers it leaves out."""
52
+ kept = [s for s in servers if not is_media_server(s)]
53
+ return kept, [s.name for s in servers if is_media_server(s)]
54
+
55
+
56
+ def note_once(left_out: list[str]) -> str:
57
+ """The note about media servers left out of code mode, the first time only."""
58
+ if not left_out:
59
+ return ""
60
+ marker = config.STATE_DIR / NOTED_FILE
61
+ if marker.exists():
62
+ return ""
63
+ try:
64
+ marker.parent.mkdir(parents=True, exist_ok=True)
65
+ marker.write_text(json.dumps(sorted(left_out)) + "\n")
66
+ except OSError:
67
+ pass
68
+ names = ", ".join(left_out)
69
+ return (
70
+ f"Code mode makes code only, so the MCP servers that make images and videos ({names}) aren't loaded "
71
+ "here any more. Image and video generation move to their own modes: /mode image, /mode video."
72
+ )
73
+
74
+
75
+ def free_comfyui(url: str) -> bool:
76
+ """Ask ComfyUI at `url` to give the GPU back (unload its models, free its memory). Whether it answered."""
77
+ try:
78
+ r = requests.post(url.rstrip("/") + "/free", json={"unload_models": True, "free_memory": True}, timeout=3)
79
+ except requests.RequestException:
80
+ return False
81
+ return r.status_code == 200
82
+
83
+
84
+ def switch(agent: Agent, new: str) -> list[str]:
85
+ """Switch the agent to another mode, handing the GPU over. The lines to show."""
86
+ old = agent.settings.mode
87
+ if new == old:
88
+ return [f"Already in {new} mode."]
89
+ lines = []
90
+ if old == "code" and getattr(agent.ollama, "kind", "ollama") == "ollama":
91
+ try:
92
+ agent.ollama.unload(agent.settings.model) # the image or video model needs the GPU
93
+ lines.append(f"Unloaded {agent.settings.model} to free the GPU for {new}s.")
94
+ except Exception: # not loaded, or Ollama gone: nothing to free
95
+ pass
96
+ if new == "code" and free_comfyui(agent.settings.comfyui_url):
97
+ lines.append("ComfyUI gave the GPU back.")
98
+ agent.settings.mode = new
99
+ lines.insert(0, f"Mode: {new} ({TITLES[new]}).")
100
+ if new != "code":
101
+ lines.append(COMING[new])
102
+ return lines