@scemoon/cdh 1.0.9 → 1.0.10-beta.2

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 (200) hide show
  1. package/cdh/__pycache__/__init__.cpython-314.pyc +0 -0
  2. package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
  3. package/cdh/__pycache__/cli_logging.cpython-314.pyc +0 -0
  4. package/cdh/__pycache__/config.cpython-314.pyc +0 -0
  5. package/cdh/__pycache__/project_loader.cpython-314.pyc +0 -0
  6. package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
  7. package/cdh/__pycache__/session_store.cpython-314.pyc +0 -0
  8. package/cdh/trace/__pycache__/__init__.cpython-314.pyc +0 -0
  9. package/cdh/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  10. package/cdh/validators/__pycache__/bdd.cpython-314.pyc +0 -0
  11. package/cdh/validators/__pycache__/dag.cpython-314.pyc +0 -0
  12. package/cdh/validators/__pycache__/ears.cpython-314.pyc +0 -0
  13. package/cdh/validators/__pycache__/fr_namespace.cpython-314.pyc +0 -0
  14. package/onecode/__init__.py +1 -1
  15. package/onecode/__pycache__/__init__.cpython-314.pyc +0 -0
  16. package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
  17. package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
  18. package/onecode/__pycache__/config.cpython-314.pyc +0 -0
  19. package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
  20. package/onecode/__pycache__/migrate.cpython-314.pyc +0 -0
  21. package/onecode/agent/__pycache__/__init__.cpython-314.pyc +0 -0
  22. package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
  23. package/onecode/agent/__pycache__/checkpoint.cpython-314.pyc +0 -0
  24. package/onecode/agent/__pycache__/context.cpython-314.pyc +0 -0
  25. package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
  26. package/onecode/agent/__pycache__/hooks.cpython-314.pyc +0 -0
  27. package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
  28. package/onecode/agent/__pycache__/permissions.cpython-314.pyc +0 -0
  29. package/onecode/agent/__pycache__/permissions_store.cpython-314.pyc +0 -0
  30. package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
  31. package/onecode/agent/__pycache__/turn_record.cpython-314.pyc +0 -0
  32. package/onecode/agent/agents/__pycache__/__init__.cpython-314.pyc +0 -0
  33. package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
  34. package/onecode/agent/engine.py +24 -4
  35. package/onecode/agent/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  36. package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
  37. package/onecode/agent/tools/__pycache__/apply_patch_tool.cpython-314.pyc +0 -0
  38. package/onecode/agent/tools/__pycache__/bash_tool.cpython-314.pyc +0 -0
  39. package/onecode/agent/tools/__pycache__/communication_tools.cpython-314.pyc +0 -0
  40. package/onecode/agent/tools/__pycache__/config_tool.cpython-314.pyc +0 -0
  41. package/onecode/agent/tools/__pycache__/cron_tools.cpython-314.pyc +0 -0
  42. package/onecode/agent/tools/__pycache__/errors.cpython-314.pyc +0 -0
  43. package/onecode/agent/tools/__pycache__/file_ops.cpython-314.pyc +0 -0
  44. package/onecode/agent/tools/__pycache__/file_tools.cpython-314.pyc +0 -0
  45. package/onecode/agent/tools/__pycache__/git_tools.cpython-314.pyc +0 -0
  46. package/onecode/agent/tools/__pycache__/lsp_tools.cpython-314.pyc +0 -0
  47. package/onecode/agent/tools/__pycache__/mcp_tools.cpython-314.pyc +0 -0
  48. package/onecode/agent/tools/__pycache__/permission_handler.cpython-314.pyc +0 -0
  49. package/onecode/agent/tools/__pycache__/permissions.cpython-314.pyc +0 -0
  50. package/onecode/agent/tools/__pycache__/protocol.cpython-314.pyc +0 -0
  51. package/onecode/agent/tools/__pycache__/registry.cpython-314.pyc +0 -0
  52. package/onecode/agent/tools/__pycache__/sandbox.cpython-314.pyc +0 -0
  53. package/onecode/agent/tools/__pycache__/schema_validation.cpython-314.pyc +0 -0
  54. package/onecode/agent/tools/__pycache__/skill_tools.cpython-314.pyc +0 -0
  55. package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
  56. package/onecode/agent/tools/__pycache__/web_tools.cpython-314.pyc +0 -0
  57. package/onecode/codebase/__pycache__/__init__.cpython-314.pyc +0 -0
  58. package/onecode/codebase/__pycache__/chunker.cpython-314.pyc +0 -0
  59. package/onecode/codebase/__pycache__/indexer.cpython-314.pyc +0 -0
  60. package/onecode/codebase/__pycache__/retriever.cpython-314.pyc +0 -0
  61. package/onecode/codebase/__pycache__/storage.cpython-314.pyc +0 -0
  62. package/onecode/codebase/__pycache__/tools.cpython-314.pyc +0 -0
  63. package/onecode/commands.py +245 -0
  64. package/onecode/config_screen.py +13 -13
  65. package/onecode/mcp/__pycache__/__init__.cpython-314.pyc +0 -0
  66. package/onecode/mcp/__pycache__/client.cpython-314.pyc +0 -0
  67. package/onecode/mcp/__pycache__/cloudbase.cpython-314.pyc +0 -0
  68. package/onecode/mcp/__pycache__/config.cpython-314.pyc +0 -0
  69. package/onecode/mcp/__pycache__/manager.cpython-314.pyc +0 -0
  70. package/onecode/mcp/__pycache__/oauth.cpython-314.pyc +0 -0
  71. package/onecode/memory/__pycache__/__init__.cpython-314.pyc +0 -0
  72. package/onecode/memory/__pycache__/backend.cpython-314.pyc +0 -0
  73. package/onecode/memory/__pycache__/pyramid.cpython-314.pyc +0 -0
  74. package/onecode/memory/__pycache__/recall.cpython-314.pyc +0 -0
  75. package/onecode/models/__pycache__/__init__.cpython-314.pyc +0 -0
  76. package/onecode/models/__pycache__/errors.cpython-314.pyc +0 -0
  77. package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
  78. package/onecode/models/__pycache__/ollama_manager.cpython-314.pyc +0 -0
  79. package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
  80. package/onecode/models/__pycache__/registry.cpython-314.pyc +0 -0
  81. package/onecode/models/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  82. package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
  83. package/onecode/models/providers/__pycache__/deepseek_provider.cpython-314.pyc +0 -0
  84. package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
  85. package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
  86. package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
  87. package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  88. package/onecode/models/providers/__pycache__/openai_provider.cpython-314.pyc +0 -0
  89. package/onecode/skills/__pycache__/__init__.cpython-314.pyc +0 -0
  90. package/onecode/skills/__pycache__/argument_substitution.cpython-314.pyc +0 -0
  91. package/onecode/skills/__pycache__/bootstrap.cpython-314.pyc +0 -0
  92. package/onecode/skills/__pycache__/create.cpython-314.pyc +0 -0
  93. package/onecode/skills/__pycache__/frontmatter.cpython-314.pyc +0 -0
  94. package/onecode/skills/__pycache__/loader.cpython-314.pyc +0 -0
  95. package/onecode/skills/__pycache__/model.cpython-314.pyc +0 -0
  96. package/onecode/verification/__pycache__/__init__.cpython-314.pyc +0 -0
  97. package/onecode/verification/__pycache__/aggregation.cpython-314.pyc +0 -0
  98. package/onecode/verification/__pycache__/loop.cpython-314.pyc +0 -0
  99. package/onecode/verification/__pycache__/policy.cpython-314.pyc +0 -0
  100. package/onecode/verification/gates/__pycache__/__init__.cpython-314.pyc +0 -0
  101. package/onecode/verification/gates/__pycache__/base.cpython-314.pyc +0 -0
  102. package/onecode/verification/gates/__pycache__/lint_gate.cpython-314.pyc +0 -0
  103. package/onecode/verification/gates/__pycache__/test_gate.cpython-314.pyc +0 -0
  104. package/onecode/verification/gates/__pycache__/type_gate.cpython-314.pyc +0 -0
  105. package/package.json +1 -1
  106. package/pyproject.toml +1 -1
  107. package/tui/__pycache__/__init__.cpython-314.pyc +0 -0
  108. package/tui/__pycache__/_loop.cpython-314.pyc +0 -0
  109. package/tui/__pycache__/_path_fuzzy_search.cpython-314.pyc +0 -0
  110. package/tui/__pycache__/_path_match.cpython-314.pyc +0 -0
  111. package/tui/__pycache__/agent.cpython-314.pyc +0 -0
  112. package/tui/__pycache__/agent_schema.cpython-314.pyc +0 -0
  113. package/tui/__pycache__/agents.cpython-314.pyc +0 -0
  114. package/tui/__pycache__/answer.cpython-314.pyc +0 -0
  115. package/tui/__pycache__/app.cpython-314.pyc +0 -0
  116. package/tui/__pycache__/atomic.cpython-314.pyc +0 -0
  117. package/tui/__pycache__/complete.cpython-314.pyc +0 -0
  118. package/tui/__pycache__/constants.cpython-314.pyc +0 -0
  119. package/tui/__pycache__/db.cpython-314.pyc +0 -0
  120. package/tui/__pycache__/dec.cpython-314.pyc +0 -0
  121. package/tui/__pycache__/directory.cpython-314.pyc +0 -0
  122. package/tui/__pycache__/directory_watcher.cpython-314.pyc +0 -0
  123. package/tui/__pycache__/format_path.cpython-314.pyc +0 -0
  124. package/tui/__pycache__/fuzzy.cpython-314.pyc +0 -0
  125. package/tui/__pycache__/fuzzy_index.cpython-314.pyc +0 -0
  126. package/tui/__pycache__/history.cpython-314.pyc +0 -0
  127. package/tui/__pycache__/jsonrpc.cpython-314.pyc +0 -0
  128. package/tui/__pycache__/menus.cpython-314.pyc +0 -0
  129. package/tui/__pycache__/message_log.cpython-314.pyc +0 -0
  130. package/tui/__pycache__/messages.cpython-314.pyc +0 -0
  131. package/tui/__pycache__/path_complete.cpython-314.pyc +0 -0
  132. package/tui/__pycache__/path_filter.cpython-314.pyc +0 -0
  133. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  134. package/tui/__pycache__/pill.cpython-314.pyc +0 -0
  135. package/tui/__pycache__/platform_commands.cpython-314.pyc +0 -0
  136. package/tui/__pycache__/protocol.cpython-314.pyc +0 -0
  137. package/tui/__pycache__/session_tracker.cpython-314.pyc +0 -0
  138. package/tui/__pycache__/settings.cpython-314.pyc +0 -0
  139. package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
  140. package/tui/__pycache__/shell.cpython-314.pyc +0 -0
  141. package/tui/__pycache__/shell_read.cpython-314.pyc +0 -0
  142. package/tui/__pycache__/slash_command.cpython-314.pyc +0 -0
  143. package/tui/__pycache__/version.cpython-314.pyc +0 -0
  144. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  145. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  146. package/tui/acp/__pycache__/encode_tool_call_id.cpython-314.pyc +0 -0
  147. package/tui/acp/__pycache__/event_tap.cpython-314.pyc +0 -0
  148. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  149. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  150. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  151. package/tui/ansi/__pycache__/__init__.cpython-314.pyc +0 -0
  152. package/tui/ansi/__pycache__/_ansi.cpython-314.pyc +0 -0
  153. package/tui/ansi/__pycache__/_ansi_colors.cpython-314.pyc +0 -0
  154. package/tui/ansi/__pycache__/_control_codes.cpython-314.pyc +0 -0
  155. package/tui/ansi/__pycache__/_keys.cpython-314.pyc +0 -0
  156. package/tui/ansi/__pycache__/_sgr_styles.cpython-314.pyc +0 -0
  157. package/tui/ansi/__pycache__/_stream_parser.cpython-314.pyc +0 -0
  158. package/tui/app.py +10 -1
  159. package/tui/prompt/__pycache__/extract.cpython-314.pyc +0 -0
  160. package/tui/prompt/__pycache__/resource.cpython-314.pyc +0 -0
  161. package/tui/screens/__pycache__/__init__.cpython-314.pyc +0 -0
  162. package/tui/screens/__pycache__/diff_screen.cpython-314.pyc +0 -0
  163. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  164. package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
  165. package/tui/screens/sessions.py +1 -4
  166. package/tui/visuals/__pycache__/__init__.cpython-314.pyc +0 -0
  167. package/tui/visuals/__pycache__/columns.cpython-314.pyc +0 -0
  168. package/tui/widgets/__pycache__/__init__.cpython-314.pyc +0 -0
  169. package/tui/widgets/__pycache__/aidlc.cpython-314.pyc +0 -0
  170. package/tui/widgets/__pycache__/ask_user.cpython-314.pyc +0 -0
  171. package/tui/widgets/__pycache__/condensed_path.cpython-314.pyc +0 -0
  172. package/tui/widgets/__pycache__/context_stats.cpython-314.pyc +0 -0
  173. package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
  174. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  175. package/tui/widgets/__pycache__/edit_field.cpython-314.pyc +0 -0
  176. package/tui/widgets/__pycache__/flash.cpython-314.pyc +0 -0
  177. package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
  178. package/tui/widgets/__pycache__/highlighted_textarea.cpython-314.pyc +0 -0
  179. package/tui/widgets/__pycache__/load_more.cpython-314.pyc +0 -0
  180. package/tui/widgets/__pycache__/menu.cpython-314.pyc +0 -0
  181. package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
  182. package/tui/widgets/__pycache__/non_selectable_label.cpython-314.pyc +0 -0
  183. package/tui/widgets/__pycache__/note.cpython-314.pyc +0 -0
  184. package/tui/widgets/__pycache__/path_search.cpython-314.pyc +0 -0
  185. package/tui/widgets/__pycache__/pending_prompts.cpython-314.pyc +0 -0
  186. package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
  187. package/tui/widgets/__pycache__/project_directory_tree.cpython-314.pyc +0 -0
  188. package/tui/widgets/__pycache__/prompt.cpython-314.pyc +0 -0
  189. package/tui/widgets/__pycache__/question.cpython-314.pyc +0 -0
  190. package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
  191. package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
  192. package/tui/widgets/__pycache__/session_tabs.cpython-314.pyc +0 -0
  193. package/tui/widgets/__pycache__/shell_terminal.cpython-314.pyc +0 -0
  194. package/tui/widgets/__pycache__/side_bar.cpython-314.pyc +0 -0
  195. package/tui/widgets/__pycache__/slash_complete.cpython-314.pyc +0 -0
  196. package/tui/widgets/__pycache__/strike_text.cpython-314.pyc +0 -0
  197. package/tui/widgets/__pycache__/terminal.cpython-314.pyc +0 -0
  198. package/tui/widgets/__pycache__/throbber.cpython-314.pyc +0 -0
  199. package/tui/widgets/__pycache__/user_input.cpython-314.pyc +0 -0
  200. package/tui/widgets/conversation.py +0 -1
Binary file
@@ -1 +1 @@
1
- __version__ = "1.0.7"
1
+ __version__ = "1.0.10-beta.1"
@@ -1381,6 +1381,16 @@ class AgentEngine:
1381
1381
  return json.dumps({"success": False, "error": f"{name} denied"})
1382
1382
  if perm == AgentPermission.ASK:
1383
1383
  return json.dumps({"success": False, "error": f"{name} requires approval", "requires_approval": True})
1384
+ if name == "Spawn" and self._is_plan_mode():
1385
+ agent_type = inp.get("agent_type", "general")
1386
+ if agent_type in ("build", "solo", "general"):
1387
+ return json.dumps({
1388
+ "success": False,
1389
+ "error": (
1390
+ f"Spawn({agent_type}) denied in plan mode. "
1391
+ "Use explore or scout for research."
1392
+ ),
1393
+ })
1384
1394
  return None
1385
1395
 
1386
1396
  def hidden_tool_names(self) -> set[str]:
@@ -1455,17 +1465,24 @@ class AgentEngine:
1455
1465
  logger.warning("Failed to save plan document: %s", e)
1456
1466
  return None
1457
1467
 
1458
- def _plan_handoff(self, plan_path: str | None) -> None:
1468
+ def _plan_handoff(self, plan_path: str | None) -> bool:
1459
1469
  """Switch from the plan agent to the execution agent after approval.
1460
1470
 
1461
1471
  Keeps the approved task list (todos) and injects the approved plan
1462
1472
  document into the execution agent's system context so it survives
1463
1473
  compaction and stays the single source of truth.
1474
+
1475
+ Returns True if handoff was successful, False otherwise.
1464
1476
  """
1477
+ logger.info("_plan_handoff called: plan_path=%s, current_agent=%s",
1478
+ plan_path, self.current_agent.name)
1465
1479
  try:
1466
1480
  if self.current_agent.name != "plan":
1467
- return
1481
+ logger.warning("_plan_handoff skipped: not in plan mode (current=%s)",
1482
+ self.current_agent.name)
1483
+ return False
1468
1484
  self.set_agent("solo", keep_todos=True)
1485
+ logger.info("_plan_handoff: agent switched to solo, context updated")
1469
1486
  plan_text = ""
1470
1487
  if plan_path:
1471
1488
  try:
@@ -1489,7 +1506,9 @@ class AgentEngine:
1489
1506
  if not self.context.replace_system_section("<!-- APPROVED_PLAN -->", section):
1490
1507
  self.context.insert_system_before_non_system(section)
1491
1508
  except Exception as e:
1492
- logger.warning("Plan handoff failed: %s", e)
1509
+ logger.error("Plan handoff failed: %s", e, exc_info=True)
1510
+ return False
1511
+ return True
1493
1512
 
1494
1513
  def _react_phase_text(self, round_no: int) -> str:
1495
1514
  """Per-round CoT guidance header; the plan agent has its own stages."""
@@ -2869,7 +2888,8 @@ class AgentEngine:
2869
2888
  }
2870
2889
  # Plan approved → hand off to the execution agent (keeps todos).
2871
2890
  if plan_submit:
2872
- self._plan_handoff(plan_path)
2891
+ if not self._plan_handoff(plan_path):
2892
+ logger.error("Plan handoff failed but continued execution")
2873
2893
  # answer is a JSON string when multi-question, plain text otherwise
2874
2894
  try:
2875
2895
  parsed = json.loads(answer)
@@ -78,6 +78,7 @@ async def _status(conversation: "Conversation", parameters: str) -> None:
78
78
 
79
79
  lines += [
80
80
  "\n**What can I do here?**",
81
+ "- `/onecode:config` — open the TUI config editor",
81
82
  "- `/onecode:provider [set <name|n>]` — show / change LLM provider",
82
83
  "- `/onecode:model [set <name|n>]` — show / change LLM model",
83
84
  "- `/onecode:skill list|enable|disable|add|remove [name]`",
@@ -619,6 +620,244 @@ async def _compact(conversation: "Conversation", parameters: str) -> None:
619
620
  conversation.notify(f"Compaction failed: {e}", title="/onecode:compact", severity="error")
620
621
 
621
622
 
623
+ # ── config ─────────────────────────────────────────────────────────────
624
+
625
+
626
+ def _mask_api_key(v: str | None) -> str:
627
+ if not v:
628
+ return "(not set)"
629
+ if v.startswith("${") and v.endswith("}"):
630
+ return f"{v} (env ref)"
631
+ if len(v) <= 4:
632
+ return "***"
633
+ return v[:4] + "***" + v[-4:]
634
+
635
+
636
+ async def _config(conversation: "Conversation", parameters: str) -> None:
637
+ cfg = _load_cfg()
638
+ parts = parameters.strip().split()
639
+ sub = parts[0].lower() if parts else ""
640
+ rest = " ".join(parts[1:]) if len(parts) > 1 else ""
641
+
642
+ if sub in ("", "tui", "screen", "edit"):
643
+ conversation.app.push_screen("config")
644
+ return
645
+
646
+ if sub == "list":
647
+ import yaml
648
+ from onecode.config import _dataclass_to_dict
649
+ raw = _dataclass_to_dict(cfg)
650
+ for pid, pcfg in raw.get("providers", {}).items():
651
+ if pcfg.get("api_key"):
652
+ pcfg["api_key"] = _mask_api_key(pcfg["api_key"])
653
+ await _post(conversation, "**onecode config**\n\n```yaml\n" + yaml.dump(raw, default_flow_style=False) + "```")
654
+ return
655
+
656
+ if sub == "mode":
657
+ mode_parts = rest.strip().split()
658
+ mode_sub = mode_parts[0].lower() if mode_parts else ""
659
+ if mode_sub == "get" or mode_sub == "":
660
+ await _post(conversation, f"**mode** = `{cfg.default_mode}` (build | plan | solo)")
661
+ return
662
+ if mode_sub == "set":
663
+ if len(mode_parts) < 2:
664
+ conversation.notify("Usage: /onecode:config mode set <build|plan|solo>", title="/onecode:config mode set", severity="error")
665
+ return
666
+ value = mode_parts[1]
667
+ if value not in ("build", "plan", "solo"):
668
+ conversation.notify("Mode must be one of: build, plan, solo", title="/onecode:config mode set", severity="error")
669
+ return
670
+ cfg.default_mode = value
671
+ _save_cfg(cfg)
672
+ conversation.flash(f"onecode mode set to [b]{value}")
673
+ return
674
+ await _post(conversation, _HELP_CONFIG)
675
+ return
676
+
677
+ if sub == "model":
678
+ model_parts = rest.strip().split()
679
+ model_sub = model_parts[0].lower() if model_parts else ""
680
+ if model_sub == "get" or model_sub == "":
681
+ await _post(conversation, f"**model** = `{cfg.default_model}`")
682
+ return
683
+ if model_sub == "set":
684
+ if len(model_parts) < 2:
685
+ conversation.notify("Usage: /onecode:config model set <model-name>", title="/onecode:config model set", severity="error")
686
+ return
687
+ value = model_parts[1]
688
+ cfg.default_model = value
689
+ _save_cfg(cfg)
690
+ conversation.flash(f"onecode model set to [b]{value}")
691
+ return
692
+ await _post(conversation, _HELP_CONFIG)
693
+ return
694
+
695
+ if sub == "provider":
696
+ prov_parts = rest.strip().split()
697
+ prov_sub = prov_parts[0].lower() if prov_parts else ""
698
+ prov_rest = " ".join(prov_parts[1:]) if len(prov_parts) > 1 else ""
699
+
700
+ if prov_sub in ("", "get"):
701
+ await _post(conversation, f"**provider** = `{cfg.default_provider}`")
702
+ return
703
+
704
+ if prov_sub == "set":
705
+ if not prov_rest:
706
+ conversation.notify("Usage: /onecode:config provider set <name>", title="/onecode:config provider set", severity="error")
707
+ return
708
+ cfg.default_provider = prov_rest
709
+ _save_cfg(cfg)
710
+ conversation.flash(f"onecode provider set to [b]{prov_rest}")
711
+ return
712
+
713
+ if prov_sub == "list":
714
+ from onecode.models.provider import ProviderRegistry
715
+ from onecode.models.registry import ModelRegistry
716
+ try:
717
+ from onecode.models import providers as _ # noqa: F401
718
+ except ImportError:
719
+ pass
720
+ try:
721
+ ModelRegistry.initialize()
722
+ except Exception:
723
+ pass
724
+ registered = sorted(ProviderRegistry.list())
725
+ current = cfg.default_provider
726
+ lines = [f"**Available providers** ({len(registered)}):\n"]
727
+ for i, name in enumerate(registered, 1):
728
+ marker = " ← active" if name == current else ""
729
+ try:
730
+ models = ModelRegistry.list_by_provider(name)
731
+ except Exception:
732
+ models = []
733
+ if models:
734
+ model_names = sorted({m.id for m in models})
735
+ shown = ", ".join(model_names[:5])
736
+ if len(model_names) > 5:
737
+ shown += f", … (+{len(model_names) - 5})"
738
+ model_hint = f" models: {shown}"
739
+ else:
740
+ model_hint = " models: (any local)"
741
+ lines.append(f" `{i}) {name}`{marker}{model_hint}")
742
+ lines.append("\n*Switch with: `/onecode:config provider set <name>`*")
743
+ await _post(conversation, "\n".join(lines))
744
+ return
745
+
746
+ if prov_sub in ("api-key", "apikey", "key"):
747
+ key_parts = prov_rest.strip().split()
748
+ key_sub = key_parts[0].lower() if key_parts else ""
749
+ key_rest = " ".join(key_parts[1:]) if len(key_parts) > 1 else ""
750
+
751
+ if key_sub == "get":
752
+ if not key_rest:
753
+ conversation.notify("Usage: /onecode:config provider api-key get <provider>", title="/onecode:config provider api-key get", severity="error")
754
+ return
755
+ pcfg = cfg.providers.get(key_rest)
756
+ if pcfg:
757
+ await _post(conversation, f"**{key_rest} api-key** = `{_mask_api_key(pcfg.api_key)}`")
758
+ else:
759
+ await _post(conversation, f"**{key_rest} api-key** = `(not configured)`")
760
+ return
761
+
762
+ if key_sub == "set":
763
+ if len(key_parts) < 3:
764
+ conversation.notify("Usage: /onecode:config provider api-key set <provider> <key>", title="/onecode:config provider api-key set", severity="error")
765
+ return
766
+ provider_id = key_parts[1]
767
+ key_value = key_parts[2]
768
+ if provider_id not in cfg.providers:
769
+ from onecode.config import ProviderConfig
770
+ cfg.providers[provider_id] = ProviderConfig()
771
+ cfg.providers[provider_id].api_key = key_value
772
+ _save_cfg(cfg)
773
+ conversation.flash(f"onecode {provider_id} api-key set")
774
+ return
775
+
776
+ await _post(conversation, _HELP_CONFIG)
777
+ return
778
+
779
+ if prov_sub == "endpoint":
780
+ ep_parts = prov_rest.strip().split()
781
+ ep_sub = ep_parts[0].lower() if ep_parts else ""
782
+ ep_rest = " ".join(ep_parts[1:]) if len(ep_parts) > 1 else ""
783
+
784
+ if ep_sub == "get":
785
+ if not ep_rest:
786
+ conversation.notify("Usage: /onecode:config provider endpoint get <provider>", title="/onecode:config provider endpoint get", severity="error")
787
+ return
788
+ pcfg = cfg.providers.get(ep_rest)
789
+ if pcfg:
790
+ ep = pcfg.endpoint or "(default)"
791
+ await _post(conversation, f"**{ep_rest} endpoint** = `{ep}`")
792
+ else:
793
+ await _post(conversation, f"**{ep_rest} endpoint** = `(not configured)`")
794
+ return
795
+
796
+ if ep_sub == "set":
797
+ if len(ep_parts) < 3:
798
+ conversation.notify("Usage: /onecode:config provider endpoint set <provider> <url>", title="/onecode:config provider endpoint set", severity="error")
799
+ return
800
+ provider_id = ep_parts[1]
801
+ ep_value = ep_parts[2]
802
+ if provider_id not in cfg.providers:
803
+ from onecode.config import ProviderConfig
804
+ cfg.providers[provider_id] = ProviderConfig()
805
+ cfg.providers[provider_id].endpoint = ep_value
806
+ _save_cfg(cfg)
807
+ conversation.flash(f"onecode {provider_id} endpoint set")
808
+ return
809
+
810
+ await _post(conversation, _HELP_CONFIG)
811
+ return
812
+
813
+ await _post(conversation, _HELP_CONFIG)
814
+ return
815
+
816
+ if sub == "log-level":
817
+ ll_parts = rest.strip().split()
818
+ ll_sub = ll_parts[0].lower() if ll_parts else ""
819
+ if ll_sub == "get" or ll_sub == "":
820
+ await _post(conversation, f"**log-level** = `{cfg.log_level}` (debug | info | warn | error)")
821
+ return
822
+ if ll_sub == "set":
823
+ if len(ll_parts) < 2:
824
+ conversation.notify("Usage: /onecode:config log-level set <debug|info|warn|error>", title="/onecode:config log-level set", severity="error")
825
+ return
826
+ value = ll_parts[1]
827
+ if value not in ("debug", "info", "warn", "error"):
828
+ conversation.notify("Log level must be one of: debug, info, warn, error", title="/onecode:config log-level set", severity="error")
829
+ return
830
+ cfg.log_level = value
831
+ _save_cfg(cfg)
832
+ conversation.flash(f"onecode log-level set to [b]{value}")
833
+ return
834
+ await _post(conversation, _HELP_CONFIG)
835
+ return
836
+
837
+ await _post(conversation, _HELP_CONFIG)
838
+
839
+
840
+ _HELP_CONFIG = (
841
+ "**/onecode:config** — manage onecode configuration\n\n"
842
+ "**Sub-commands**\n"
843
+ "- `(none)` or `tui` — open the TUI config editor\n"
844
+ "- `list` — show full YAML configuration (api-key masked)\n"
845
+ "- `mode get|set <build|plan|solo>`\n"
846
+ "- `model get|set <model-name>`\n"
847
+ "- `provider get|set <name>` — get/set default provider\n"
848
+ "- `provider list` — list all available providers\n"
849
+ "- `provider api-key get|set <provider> <key>` — manage provider api-key\n"
850
+ "- `provider endpoint get|set <provider> <url>` — manage provider endpoint\n"
851
+ "- `log-level get|set <debug|info|warn|error>`\n\n"
852
+ "**Examples**\n"
853
+ "- `/onecode:config` — open config editor\n"
854
+ "- `/onecode:config list` — show full config\n"
855
+ "- `/onecode:config mode set solo`\n"
856
+ "- `/onecode:config provider api-key set openai sk-xxx`\n"
857
+ "- `/onecode:config provider endpoint set openai https://api.openai.com/v1`"
858
+ )
859
+
860
+
622
861
  # ── registration ──────────────────────────────────────────────────────
623
862
 
624
863
 
@@ -670,4 +909,10 @@ def register_commands() -> None:
670
909
  "Immediately compact context to reduce token usage",
671
910
  _compact,
672
911
  "(no args)",
912
+ )
913
+ platform_commands.register(
914
+ "config",
915
+ "Open config editor TUI, or manage mode/model/provider/api-key/endpoint/log-level",
916
+ _config,
917
+ "mode|model|provider|log-level [get|set <v>]",
673
918
  )
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from textual import on
4
- from textual.app import App, ComposeResult
4
+ from textual.app import ComposeResult
5
5
  from textual.binding import Binding
6
6
  from textual.containers import Horizontal, Vertical
7
7
  from textual.reactive import var
8
+ from textual.screen import ModalScreen
8
9
  from textual.widget import Widget
9
10
  from textual.widgets import Button, Label, Static
10
11
 
@@ -130,8 +131,7 @@ Button:disabled {
130
131
  """
131
132
 
132
133
 
133
- class ConfigScreen(App):
134
- TITLE = "CDH Configuration"
134
+ class ConfigScreen(ModalScreen):
135
135
  CSS = CSS
136
136
 
137
137
  BINDINGS = [
@@ -141,7 +141,6 @@ class ConfigScreen(App):
141
141
  Binding("right", "go_enter", "Select"),
142
142
  Binding("enter", "confirm", "Confirm"),
143
143
  Binding("escape", "cancel", "Cancel"),
144
- Binding("ctrl+q", "quit_app", "Quit"),
145
144
  ]
146
145
 
147
146
  MODE_OPTIONS = [
@@ -178,7 +177,6 @@ class ConfigScreen(App):
178
177
  with Horizontal(id="button-row"):
179
178
  yield Button("Confirm (↵)", id="save")
180
179
  yield Button("Cancel (ESC)", id="cancel")
181
- yield Button("Quit (Ctrl+Q)", id="quit")
182
180
 
183
181
  def on_mount(self) -> None:
184
182
  self._build_menu()
@@ -461,26 +459,28 @@ class ConfigScreen(App):
461
459
  def action_cancel(self) -> None:
462
460
  if self.view != "menu":
463
461
  self.action_go_back()
464
-
465
- def action_quit_app(self) -> None:
466
- self.exit()
462
+ else:
463
+ self.dismiss()
467
464
 
468
465
  def _on_click(self, event) -> None:
469
466
  pass
470
467
 
471
468
  @on(Button.Pressed, "#save")
472
469
  def on_save(self) -> None:
473
- self.notify("Configuration saved")
474
- self.exit()
470
+ self.app.notify("Configuration saved")
471
+ self.dismiss()
475
472
 
476
473
  @on(Button.Pressed, "#cancel")
477
474
  def on_cancel_button(self) -> None:
478
- self.exit()
475
+ self.dismiss()
479
476
 
480
477
  @on(Button.Pressed, "#quit")
481
478
  def on_quit(self) -> None:
482
- self.exit()
479
+ self.dismiss()
483
480
 
484
481
 
485
482
  def main() -> None:
486
- ConfigScreen().run()
483
+ from textual.app import App
484
+ app = App()
485
+ app.push_screen(ConfigScreen())
486
+ app.run()