@scemoon/cdh 1.0.0 → 1.0.3

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 (116) hide show
  1. package/cdh/__pycache__/__main__.cpython-314.pyc +0 -0
  2. package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
  3. package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
  4. package/cdh/cli.py +473 -87
  5. package/cdh/scaffold.py +386 -121
  6. package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
  7. package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
  8. package/onecode/__pycache__/config.cpython-314.pyc +0 -0
  9. package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
  10. package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
  11. package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
  12. package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
  13. package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
  14. package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
  15. package/onecode/agent/agents/types.py +123 -79
  16. package/onecode/agent/cdh_loader.py +26 -13
  17. package/onecode/agent/engine.py +426 -235
  18. package/onecode/agent/onecode_agent_acp.py +200 -73
  19. package/onecode/agent/session.py +8 -13
  20. package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
  21. package/onecode/agent/tools/__pycache__/task_tools.cpython-314.pyc +0 -0
  22. package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
  23. package/onecode/agent/tools/agent_tools.py +4 -4
  24. package/onecode/agent/tools/todo_tools.py +188 -0
  25. package/onecode/cli.py +3 -3
  26. package/onecode/commands.py +439 -194
  27. package/onecode/config.py +3 -3
  28. package/onecode/config_screen.py +4 -2
  29. package/onecode/models/__pycache__/errors.cpython-314.pyc +0 -0
  30. package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
  31. package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
  32. package/onecode/models/errors.py +11 -1
  33. package/onecode/models/messages.py +35 -16
  34. package/onecode/models/provider.py +243 -12
  35. package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
  36. package/onecode/models/providers/__pycache__/deepseek_provider.cpython-314.pyc +0 -0
  37. package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
  38. package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
  39. package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
  40. package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  41. package/onecode/models/providers/__pycache__/openai_provider.cpython-314.pyc +0 -0
  42. package/onecode/models/providers/anthropic_provider.py +4 -2
  43. package/onecode/models/providers/deepseek_provider.py +2 -0
  44. package/onecode/models/providers/glm_provider.py +2 -1
  45. package/onecode/models/providers/minimax_provider.py +2 -1
  46. package/onecode/models/providers/minimaxi_provider.py +76 -25
  47. package/onecode/models/providers/ollama_provider.py +2 -1
  48. package/onecode/models/providers/openai_provider.py +4 -0
  49. package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
  50. package/onecode/tasks/__pycache__/__init__.cpython-314.pyc +0 -0
  51. package/onecode/tasks/__pycache__/manager.cpython-314.pyc +0 -0
  52. package/onecode/tasks/__pycache__/models.cpython-314.pyc +0 -0
  53. package/onecode/tasks/manager.py +1 -1
  54. package/package.json +1 -1
  55. package/pyproject.toml +10 -0
  56. package/run.js +24 -7
  57. package/tui/__pycache__/app.cpython-314.pyc +0 -0
  58. package/tui/__pycache__/cli.cpython-314.pyc +0 -0
  59. package/tui/__pycache__/db.cpython-314.pyc +0 -0
  60. package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
  61. package/tui/__pycache__/directory_watcher.cpython-314.pyc +0 -0
  62. package/tui/__pycache__/messages.cpython-314.pyc +0 -0
  63. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  64. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  65. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  66. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  67. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  68. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  69. package/tui/acp/agent.py +60 -11
  70. package/tui/acp/api.py +6 -0
  71. package/tui/acp/messages.py +11 -0
  72. package/tui/acp/prompt.py +1 -1
  73. package/tui/acp/protocol.py +31 -0
  74. package/tui/app.py +60 -8
  75. package/tui/db.py +23 -0
  76. package/tui/directory_watcher.py +35 -1
  77. package/tui/messages.py +4 -1
  78. package/tui/paths.py +3 -3
  79. package/tui/screens/__pycache__/agent_modal.cpython-314.pyc +0 -0
  80. package/tui/screens/__pycache__/component_picker.cpython-314.pyc +0 -0
  81. package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
  82. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  83. package/tui/screens/__pycache__/projects_app.cpython-314.pyc +0 -0
  84. package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
  85. package/tui/screens/__pycache__/settings.cpython-314.pyc +0 -0
  86. package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
  87. package/tui/screens/component_picker.py +148 -0
  88. package/tui/screens/main.py +47 -95
  89. package/tui/screens/projects.tcss +10 -0
  90. package/tui/screens/projects_app.py +33 -5
  91. package/tui/screens/projects_screen.py +171 -9
  92. package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
  93. package/tui/widgets/__pycache__/ask_user.cpython-314.pyc +0 -0
  94. package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
  95. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  96. package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
  97. package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
  98. package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
  99. package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
  100. package/tui/widgets/__pycache__/project_directory_tree.cpython-314.pyc +0 -0
  101. package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
  102. package/tui/widgets/__pycache__/question.cpython-314.pyc +0 -0
  103. package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
  104. package/tui/widgets/__pycache__/subagent_screen.cpython-314.pyc +0 -0
  105. package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
  106. package/tui/widgets/ask_user.py +1 -1
  107. package/tui/widgets/conversation.py +99 -42
  108. package/tui/widgets/modified_files.py +155 -23
  109. package/tui/widgets/plan.py +22 -0
  110. package/tui/widgets/project_directory_tree.py +22 -10
  111. package/tui/widgets/project_grid_select.py +23 -1
  112. package/tui/widgets/question.py +1 -1
  113. package/tui/widgets/subagent.py +202 -36
  114. package/tui/widgets/subagent_screen.py +186 -0
  115. package/tui/widgets/tool_call.py +5 -8
  116. package/onecode/agent/tools/task_tools.py +0 -249
@@ -8,269 +8,514 @@ if TYPE_CHECKING:
8
8
  from tui.widgets.conversation import Conversation
9
9
 
10
10
 
11
- async def _mode(conversation: Conversation, parameters: str) -> None:
12
- from onecode.config import load_config, save_config
11
+ _KNOWN_PROVIDERS = {
12
+ "anthropic": ["claude-3-opus", "claude-3-sonnet", "claude-3-haiku"],
13
+ "openai": ["gpt-4o", "gpt-4-turbo", "gpt-4o-mini"],
14
+ "deepseek": ["deepseek-chat", "deepseek-reasoner"],
15
+ "minimax": ["MiniMax-M2.7", "MiniMax-M2.5"],
16
+ "minimaxi": ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M3"],
17
+ "glm": ["glm-4-plus", "glm-4-flash"],
18
+ "ollama": [],
19
+ }
13
20
 
14
- mode = parameters.strip().lower()
15
- if mode not in ("build", "plan", "solo"):
16
- conversation.notify("Mode must be: build, plan, or solo", title="/mode", severity="error")
17
- return
18
- cfg = load_config()
19
- cfg.default_mode = mode
21
+
22
+ # ── small helpers ──────────────────────────────────────────────────────
23
+
24
+
25
+ def _load_cfg():
26
+ from onecode.config import load_config
27
+ return load_config()
28
+
29
+
30
+ def _save_cfg(cfg):
31
+ from onecode.config import save_config
20
32
  save_config(cfg)
21
- conversation.app.settings.set("mode", mode)
22
- await conversation.set_mode(mode)
23
- conversation.flash(f"onecode mode set to [b]{mode}")
24
33
 
25
34
 
26
- async def _provider(conversation: Conversation, parameters: str) -> None:
27
- from onecode.config import load_config, save_config
35
+ def _post_note(conversation, text):
36
+ from tui.widgets.markdown_note import MarkdownNote
37
+ return conversation.post(MarkdownNote(text))
38
+
39
+
40
+ async def _post(conversation, text):
41
+ await _post_note(conversation, text)
42
+
43
+
44
+ # ── status: one-shot snapshot of every onecode: setting ───────────────
45
+
28
46
 
29
- provider = parameters.strip()
30
- if not provider:
31
- conversation.notify("Provider name required", title="/provider", severity="error")
47
+ async def _status(conversation: "Conversation", parameters: str) -> None:
48
+ """Render a single capability panel: provider, model, mode, skills, MCP."""
49
+ cfg = _load_cfg()
50
+ from onecode.skills.loader import SkillLoader
51
+ from onecode.mcp.manager import MCPManager
52
+
53
+ skills = SkillLoader().get_all()
54
+ enabled_skills = sorted(n for n, s in skills.items() if s.enabled)
55
+ disabled_skills = sorted(n for n, s in skills.items() if not s.enabled)
56
+
57
+ servers = MCPManager().list()
58
+ enabled_mcps = sorted(s["name"] for s in servers if s.get("enabled", True))
59
+ disabled_mcps = sorted(s["name"] for s in servers if not s.get("enabled", True))
60
+
61
+ lines = [
62
+ "**onecode — current state**\n",
63
+ f"- **provider**: `{cfg.default_provider}`",
64
+ f"- **model**: `{cfg.default_model}`",
65
+ f"- **mode**: `{cfg.default_mode}` (build | plan | solo)",
66
+ f"- **log level**: `{cfg.log_level}`",
67
+ f"- **skills**: {len(enabled_skills)} enabled, {len(disabled_skills)} disabled",
68
+ ]
69
+ if enabled_skills:
70
+ lines.append(f" - enabled: {', '.join(f'`{n}`' for n in enabled_skills)}")
71
+ if disabled_skills:
72
+ lines.append(f" - disabled: {', '.join(f'`{n}`' for n in disabled_skills)}")
73
+ lines.append(f"- **mcp servers**: {len(enabled_mcps)} enabled, {len(disabled_mcps)} disabled")
74
+ if enabled_mcps:
75
+ lines.append(f" - enabled: {', '.join(f'`{n}`' for n in enabled_mcps)}")
76
+ if disabled_mcps:
77
+ lines.append(f" - disabled: {', '.join(f'`{n}`' for n in disabled_mcps)}")
78
+
79
+ lines += [
80
+ "\n**What can I do here?**",
81
+ "- `/onecode:provider [set <name|n>]` — show / change LLM provider",
82
+ "- `/onecode:model [set <name|n>]` — show / change LLM model",
83
+ "- `/onecode:skill list|enable|disable|add|remove [name]`",
84
+ "- `/onecode:mcp list|enable|disable|add|remove [name|url]`",
85
+ "- `/onecode:help` — show this overview",
86
+ ]
87
+ await _post(conversation, "\n".join(lines))
88
+
89
+
90
+ # ── provider ───────────────────────────────────────────────────────────
91
+
92
+
93
+ async def _provider(conversation: "Conversation", parameters: str) -> None:
94
+ cfg = _load_cfg()
95
+ args = parameters.strip().split()
96
+ sub = args[0].lower() if args else ""
97
+
98
+ if sub in ("", "show", "list"):
99
+ registered = sorted(_KNOWN_PROVIDERS.keys())
100
+ lines = [
101
+ "**LLM Provider**\n",
102
+ f"- current: `{cfg.default_provider}`",
103
+ f"- available ({len(registered)}):",
104
+ ]
105
+ for i, name in enumerate(registered, 1):
106
+ marker = " ← active" if name == cfg.default_provider else ""
107
+ models = _KNOWN_PROVIDERS[name]
108
+ model_hint = f" models: {', '.join(models)}" if models else " models: (any local)"
109
+ lines.append(f" `{i}) {name}`{marker}{model_hint}")
110
+ lines += [
111
+ "\n**How to switch**",
112
+ "- `/onecode:provider set <name>` — e.g. `set minimaxi`",
113
+ "- `/onecode:provider set <n>` — by number from the list above",
114
+ "\n*Tip:* the provider change applies to new sessions; the current session keeps the active model until you start a new conversation.",
115
+ ]
116
+ await _post(conversation, "\n".join(lines))
32
117
  return
33
- cfg = load_config()
34
- cfg.default_provider = provider
35
- save_config(cfg)
36
- conversation.app.settings.set("provider", provider)
37
- conversation.flash(f"onecode provider set to [b]{provider}")
38
118
 
119
+ if sub == "set":
120
+ if len(args) < 2:
121
+ conversation.notify("Usage: /onecode:provider set <name|n>", title="/onecode:provider", severity="error")
122
+ return
123
+ target = args[1].strip()
124
+ registered = sorted(_KNOWN_PROVIDERS.keys())
125
+ if target.isdigit():
126
+ idx = int(target)
127
+ if 1 <= idx <= len(registered):
128
+ target = registered[idx - 1]
129
+ if target not in registered:
130
+ conversation.notify(
131
+ f"Unknown provider '{target}'. Use /onecode:provider to list available ones.",
132
+ title="/onecode:provider",
133
+ severity="error",
134
+ )
135
+ return
136
+ cfg.default_provider = target
137
+ _save_cfg(cfg)
138
+ try:
139
+ conversation.app.settings.set("provider", target)
140
+ except Exception:
141
+ pass
142
+ conversation.flash(f"onecode provider set to [b]{target}")
143
+ await _status(conversation, "")
144
+ return
39
145
 
40
- async def _model(conversation: Conversation, parameters: str) -> None:
41
- from onecode.config import load_config, save_config
146
+ await _post(conversation, _HELP_PROVIDER)
147
+
148
+
149
+ _HELP_PROVIDER = (
150
+ "**/onecode:provider** — show / switch the LLM provider\n\n"
151
+ "**Sub-commands**\n"
152
+ "- `(none)` or `show` — list registered providers with current marker\n"
153
+ "- `set <name>` — switch provider (name or list-number)\n\n"
154
+ "**Examples**\n"
155
+ "- `/onecode:provider` — show the picker\n"
156
+ "- `/onecode:provider set minimaxi` — switch by name\n"
157
+ "- `/onecode:provider set 3` — switch by list number"
158
+ )
159
+
160
+
161
+ # ── model ──────────────────────────────────────────────────────────────
162
+
163
+
164
+ async def _model(conversation: "Conversation", parameters: str) -> None:
165
+ cfg = _load_cfg()
166
+ args = parameters.strip().split()
167
+ sub = args[0].lower() if args else ""
168
+
169
+ provider = cfg.default_provider
170
+ known = _KNOWN_PROVIDERS.get(provider, [])
171
+
172
+ if sub in ("", "show", "list"):
173
+ lines = [
174
+ "**LLM Model**\n",
175
+ f"- provider: `{provider}`",
176
+ f"- current: `{cfg.default_model}`",
177
+ ]
178
+ if known:
179
+ lines.append(f"- known models for `{provider}` ({len(known)}):")
180
+ for i, name in enumerate(known, 1):
181
+ marker = " ← active" if name == cfg.default_model else ""
182
+ lines.append(f" `{i}) {name}`{marker}")
183
+ else:
184
+ lines.append("- *(no preset list — type any model name)*")
185
+ lines += [
186
+ "\n**How to switch**",
187
+ "- `/onecode:model set <name>` — e.g. `set MiniMax-M2.7`",
188
+ "- `/onecode:model set <n>` — by number from the list above",
189
+ "- `/onecode:model set <any-string>` — for providers without a preset",
190
+ "\n*Tip:* changing the model mid-session does not retroactively update the active request; new turns will use the new model.",
191
+ ]
192
+ await _post(conversation, "\n".join(lines))
193
+ return
42
194
 
43
- model = parameters.strip()
44
- if not model:
45
- conversation.notify("Model name required", title="/model", severity="error")
195
+ if sub == "set":
196
+ if len(args) < 2:
197
+ conversation.notify("Usage: /onecode:model set <name|n>", title="/onecode:model", severity="error")
198
+ return
199
+ target = args[1].strip()
200
+ if target.isdigit() and known:
201
+ idx = int(target)
202
+ if 1 <= idx <= len(known):
203
+ target = known[idx - 1]
204
+ if not target:
205
+ conversation.notify("Model name required", title="/onecode:model", severity="error")
206
+ return
207
+ cfg.default_model = target
208
+ _save_cfg(cfg)
209
+ try:
210
+ conversation.app.settings.set("model", target)
211
+ except Exception:
212
+ pass
213
+ conversation.flash(f"onecode model set to [b]{target}")
214
+ await _status(conversation, "")
46
215
  return
47
- cfg = load_config()
48
- cfg.default_model = model
49
- save_config(cfg)
50
- conversation.app.settings.set("model", model)
51
- conversation.flash(f"onecode model set to [b]{model}")
52
216
 
217
+ await _post(conversation, _HELP_MODEL)
53
218
 
54
- async def _skill(conversation: Conversation, parameters: str) -> None:
55
- from tui.widgets.markdown_note import MarkdownNote
219
+
220
+ _HELP_MODEL = (
221
+ "**/onecode:model** — show / switch the LLM model\n\n"
222
+ "**Sub-commands**\n"
223
+ "- `(none)` or `show` — list known models for the current provider\n"
224
+ "- `set <name>` — switch model (name or list-number)\n\n"
225
+ "**Examples**\n"
226
+ "- `/onecode:model` — show the picker for the current provider\n"
227
+ "- `/onecode:model set MiniMax-M2.7` — switch by name\n"
228
+ "- `/onecode:model set 1` — switch by list number"
229
+ )
230
+
231
+
232
+ # ── skill ──────────────────────────────────────────────────────────────
233
+
234
+
235
+ async def _skill(conversation: "Conversation", parameters: str) -> None:
56
236
  from onecode.skills.manager import SkillManager
57
237
  from onecode.skills.loader import SkillLoader
58
238
  from onecode.skills.create import create_skill_scaffold
59
239
 
60
240
  parts = parameters.strip().split()
61
- subcmd = parts[0].lower() if parts else ""
241
+ sub = parts[0].lower() if parts else ""
62
242
  rest = " ".join(parts[1:]) if len(parts) > 1 else ""
63
243
 
64
- if subcmd == "list":
65
- loader = SkillLoader()
66
- all_skills = loader.get_all()
67
- if not all_skills:
68
- await conversation.post(MarkdownNote("**No skills found.**\n\nUse `/skill add <name>` to create a new skill."))
244
+ if sub in ("", "list", "show"):
245
+ skills = SkillLoader().get_all()
246
+ if not skills:
247
+ await _post(conversation, "**Skills**\n\n_No skills installed._\n\nUse `/onecode:skill add <name>` to scaffold a new one.")
69
248
  return
249
+ lines = ["**Skills**\n"]
250
+ names = sorted(skills.keys())
251
+ for i, name in enumerate(names, 1):
252
+ s = skills[name]
253
+ marker = "✓" if s.enabled else "✗"
254
+ desc = s.description or "(no description)"
255
+ lines.append(f"- `{i}) {marker}` **{name}** — {desc}")
256
+ lines += [
257
+ "\n**How to manage**",
258
+ "- `/onecode:skill enable <name|n>` — turn on",
259
+ "- `/onecode:skill disable <name|n>` — turn off",
260
+ "- `/onecode:skill add <name>` — scaffold a new skill",
261
+ "- `/onecode:skill remove <name|n>` — delete a skill",
262
+ ]
263
+ await _post(conversation, "\n".join(lines))
264
+ return
70
265
 
71
- lines = ["**Available Skills**\n"]
72
- for name, skill in sorted(all_skills.items()):
73
- enabled = skill.enabled
74
- status = "✓" if enabled else "✗"
75
- lines.append(f"- {status} **{name}**: {skill.description}")
76
- if skill.path:
77
- lines.append(f" - Path: `{skill.path}`")
78
- await conversation.post(MarkdownNote("\n".join(lines)))
266
+ if sub == "enable" or sub == "disable":
267
+ if not rest.strip():
268
+ conversation.notify(f"Usage: /onecode:skill {sub} <name|n>", title=f"/onecode:skill {sub}", severity="error")
269
+ return
270
+ target = _resolve_indexed_name(conversation, rest.strip(), SkillLoader().get_all(), sub)
271
+ if target is None:
272
+ return
273
+ mgr = SkillManager()
274
+ mgr.enable(target, sub == "enable")
275
+ conversation.flash(f"skill [b]{target}[/b] {sub}d")
276
+ await _skill(conversation, "list")
277
+ return
79
278
 
80
- elif subcmd == "add":
279
+ if sub == "add":
81
280
  name = rest.strip()
82
281
  if not name:
83
- conversation.notify("Skill name required", title="/skill add", severity="error")
282
+ conversation.notify("Usage: /onecode:skill add <name>", title="/onecode:skill add", severity="error")
84
283
  return
85
-
86
284
  mgr = SkillManager()
87
285
  if mgr.get(name):
88
- conversation.notify(f"Skill '{name}' already exists", title="/skill add", severity="error")
286
+ conversation.notify(f"Skill '{name}' already exists", title="/onecode:skill add", severity="error")
89
287
  return
90
-
91
288
  err = create_skill_scaffold(mgr.skills_dir, name, f"A skill for {name}")
92
289
  if err:
93
- conversation.notify(f"Error: {err}", title="/skill add", severity="error")
290
+ conversation.notify(f"Error: {err}", title="/onecode:skill add", severity="error")
94
291
  return
95
-
96
- await conversation.post(MarkdownNote(
292
+ await _post(
293
+ conversation,
97
294
  f"**Skill '{name}' created**\n\n"
98
- f"Location: `{mgr.skills_dir / name}`\n\n"
99
- f"Edit the SKILL.md file to add instructions, then use `/skill list` to see it."
100
- ))
101
-
102
- elif subcmd == "remove":
103
- name = rest.strip()
104
- if not name:
105
- conversation.notify("Skill name required", title="/skill remove", severity="error")
106
- return
107
-
108
- mgr = SkillManager()
109
- err = mgr.remove(name)
110
- if err:
111
- conversation.notify(f"Error: {err}", title="/skill remove", severity="error")
112
- return
113
-
114
- await conversation.post(MarkdownNote(f"**Skill '{name}' removed.**"))
295
+ f"- location: `{mgr.skills_dir / name}`\n"
296
+ f"- edit `SKILL.md` to add instructions\n"
297
+ f"- then `/onecode:skill list` to confirm it shows up",
298
+ )
299
+ return
115
300
 
116
- elif subcmd == "enable":
117
- name = rest.strip()
118
- if not name:
119
- conversation.notify("Skill name required", title="/skill enable", severity="error")
301
+ if sub == "remove":
302
+ if not rest.strip():
303
+ conversation.notify("Usage: /onecode:skill remove <name|n>", title="/onecode:skill remove", severity="error")
120
304
  return
121
-
122
- mgr = SkillManager()
123
- skill_data = mgr.get(name)
124
- if not skill_data:
125
- conversation.notify(f"Skill '{name}' not found", title="/skill enable", severity="error")
305
+ target = _resolve_indexed_name(conversation, rest.strip(), SkillLoader().get_all(), sub)
306
+ if target is None:
126
307
  return
308
+ SkillManager().remove(target)
309
+ conversation.flash(f"skill [b]{target}[/b] removed")
310
+ await _skill(conversation, "list")
311
+ return
127
312
 
128
- mgr.enable(name, True)
129
- await conversation.post(MarkdownNote(f"**Skill '{name}' enabled.**"))
313
+ await _post(conversation, _HELP_SKILL)
130
314
 
131
- elif subcmd == "disable":
132
- name = rest.strip()
133
- if not name:
134
- conversation.notify("Skill name required", title="/skill disable", severity="error")
135
- return
136
315
 
137
- mgr = SkillManager()
138
- skill_data = mgr.get(name)
139
- if not skill_data:
140
- conversation.notify(f"Skill '{name}' not found", title="/skill disable", severity="error")
141
- return
316
+ _HELP_SKILL = (
317
+ "**/onecode:skill** manage installed skills\n\n"
318
+ "**Sub-commands**\n"
319
+ "- `(none)` or `list` show all skills with status\n"
320
+ "- `enable <name|n>` — turn a skill on\n"
321
+ "- `disable <name|n>` — turn a skill off\n"
322
+ "- `add <name>` — scaffold a new skill\n"
323
+ "- `remove <name|n>` — delete a skill\n\n"
324
+ "**Examples**\n"
325
+ "- `/onecode:skill` — list\n"
326
+ "- `/onecode:skill enable 2` — enable the skill at index 2 from the list\n"
327
+ "- `/onecode:skill add my-skill` — scaffold a new skill named `my-skill`"
328
+ )
142
329
 
143
- mgr.enable(name, False)
144
- await conversation.post(MarkdownNote(f"**Skill '{name}' disabled.**"))
145
330
 
146
- else:
147
- await conversation.post(MarkdownNote(
148
- "**Skill Management**\n\n"
149
- "Usage:\n"
150
- "- `/skill list` - List all skills\n"
151
- "- `/skill add <name>` - Create a new skill\n"
152
- "- `/skill remove <name>` - Remove a skill\n"
153
- "- `/skill enable <name>` - Enable a skill\n"
154
- "- `/skill disable <name>` - Disable a skill"
155
- ))
331
+ # ── mcp ────────────────────────────────────────────────────────────────
156
332
 
157
333
 
158
- async def _mcp(conversation: Conversation, parameters: str) -> None:
159
- from tui.widgets.markdown_note import MarkdownNote
334
+ async def _mcp(conversation: "Conversation", parameters: str) -> None:
160
335
  from onecode.mcp.manager import MCPManager
161
336
 
162
337
  parts = parameters.strip().split()
163
- subcmd = parts[0].lower() if parts else ""
338
+ sub = parts[0].lower() if parts else ""
164
339
  rest = " ".join(parts[1:]) if len(parts) > 1 else ""
165
340
 
166
- if subcmd == "list":
167
- mgr = MCPManager()
168
- servers = mgr.list()
341
+ if sub in ("", "list", "show"):
342
+ servers = MCPManager().list()
169
343
  if not servers:
170
- await conversation.post(MarkdownNote(
171
- "**No MCP Servers**\n\n"
172
- "Use `/mcp add <name> <url>` to add an MCP server."
173
- ))
344
+ await _post(
345
+ conversation,
346
+ "**MCP Servers**\n\n_None configured._\n\n"
347
+ "Use `/onecode:mcp add <name> <url>` for an SSE server, or "
348
+ "`/onecode:mcp add <name> --type stdio --command <cmd>` for stdio.",
349
+ )
174
350
  return
175
-
176
- lines = ["**Configured MCP Servers**\n"]
177
- for s in servers:
178
- name = s.get("name", "unknown")
351
+ lines = ["**MCP Servers**\n"]
352
+ names = [s.get("name", "?") for s in servers]
353
+ for i, s in enumerate(servers, 1):
354
+ name = s.get("name", "?")
179
355
  transport = s.get("transport", "sse")
180
356
  enabled = s.get("enabled", True)
181
- status = "✓" if enabled else "✗"
357
+ marker = "✓" if enabled else "✗"
182
358
  if transport == "sse":
183
- url = s.get("url", "")
184
- lines.append(f"- {status} **{name}** (SSE)")
185
- if url:
186
- lines.append(f" - URL: `{url}`")
359
+ lines.append(f"- `{i}) {marker}` **{name}** (SSE) — `{s.get('url', '')}`")
187
360
  else:
188
361
  cmd = s.get("command", "")
189
- args = " ".join(s.get("args", []))
190
- lines.append(f"- {status} **{name}** (stdio)")
191
- if cmd:
192
- lines.append(f" - Command: `{cmd} {args}`")
193
- await conversation.post(MarkdownNote("\n".join(lines)))
362
+ args = " ".join(s.get("args") or [])
363
+ lines.append(f"- `{i}) {marker}` **{name}** (stdio) — `{cmd} {args}`".rstrip())
364
+ lines += [
365
+ "\n**How to manage**",
366
+ "- `/onecode:mcp enable <name|n>`",
367
+ "- `/onecode:mcp disable <name|n>`",
368
+ "- `/onecode:mcp remove <name|n>`",
369
+ ]
370
+ await _post(conversation, "\n".join(lines))
371
+ return
372
+
373
+ if sub == "enable" or sub == "disable":
374
+ if not rest.strip():
375
+ conversation.notify(f"Usage: /onecode:mcp {sub} <name|n>", title=f"/onecode:mcp {sub}", severity="error")
376
+ return
377
+ mgr = MCPManager()
378
+ target = _resolve_indexed_name(conversation, rest.strip(), {s["name"]: s for s in mgr.list()}, sub)
379
+ if target is None:
380
+ return
381
+ err = mgr.enable(target, sub == "enable")
382
+ if err:
383
+ conversation.notify(f"Error: {err}", title=f"/onecode:mcp {sub}", severity="error")
384
+ return
385
+ conversation.flash(f"mcp server [b]{target}[/b] {sub}d")
386
+ await _mcp(conversation, "list")
387
+ return
194
388
 
195
- elif subcmd == "add":
389
+ if sub == "add":
196
390
  args_parts = rest.split()
197
391
  if len(args_parts) < 2:
198
- conversation.notify("Usage: /mcp add <name> <url> [--type stdio]", title="/mcp add", severity="error")
392
+ await _post(
393
+ conversation,
394
+ "**/onecode:mcp add** — register a new MCP server\n\n"
395
+ "- **SSE** (default): `/onecode:mcp add <name> <url>`\n"
396
+ "- **stdio**: `/onecode:mcp add <name> --type stdio --command <cmd> --args a,b`",
397
+ )
199
398
  return
200
-
201
399
  name = args_parts[0]
202
400
  url = args_parts[1]
203
- transport = "sse"
204
-
205
401
  mgr = MCPManager()
206
402
  if mgr.get(name):
207
- conversation.notify(f"MCP server '{name}' already exists", title="/mcp add", severity="error")
403
+ conversation.notify(f"MCP server '{name}' already exists", title="/onecode:mcp add", severity="error")
208
404
  return
209
-
210
405
  mgr.add(name, url, transport="sse")
211
- await conversation.post(MarkdownNote(
212
- f"**MCP Server '{name}' added**\n\n"
213
- f"URL: `{url}`\n"
214
- f"Transport: SSE\n\n"
215
- f"Use `/mcp list` to see all servers."
216
- ))
217
-
218
- elif subcmd == "remove":
219
- name = rest.strip()
220
- if not name:
221
- conversation.notify("Server name required", title="/mcp remove", severity="error")
222
- return
223
-
224
- mgr = MCPManager()
225
- if not mgr.get(name):
226
- conversation.notify(f"MCP server '{name}' not found", title="/mcp remove", severity="error")
227
- return
228
-
229
- mgr.remove(name)
230
- await conversation.post(MarkdownNote(f"**MCP Server '{name}' removed.**"))
231
-
232
- elif subcmd == "enable":
233
- name = rest.strip()
234
- if not name:
235
- conversation.notify("Server name required", title="/mcp enable", severity="error")
236
- return
237
-
238
- mgr = MCPManager()
239
- err = mgr.enable(name, True)
240
- if err:
241
- conversation.notify(f"Error: {err}", title="/mcp enable", severity="error")
242
- return
243
-
244
- await conversation.post(MarkdownNote(f"**MCP Server '{name}' enabled.**"))
406
+ await _post(
407
+ conversation,
408
+ f"**MCP Server '{name}' added**\n\n- transport: SSE\n- URL: `{url}`\n\nUse `/onecode:mcp list` to confirm.",
409
+ )
410
+ return
245
411
 
246
- elif subcmd == "disable":
247
- name = rest.strip()
248
- if not name:
249
- conversation.notify("Server name required", title="/mcp disable", severity="error")
412
+ if sub == "remove":
413
+ if not rest.strip():
414
+ conversation.notify("Usage: /onecode:mcp remove <name|n>", title="/onecode:mcp remove", severity="error")
250
415
  return
251
-
252
416
  mgr = MCPManager()
253
- err = mgr.enable(name, False)
254
- if err:
255
- conversation.notify(f"Error: {err}", title="/mcp disable", severity="error")
417
+ target = _resolve_indexed_name(conversation, rest.strip(), {s["name"]: s for s in mgr.list()}, sub)
418
+ if target is None:
256
419
  return
420
+ mgr.remove(target)
421
+ conversation.flash(f"mcp server [b]{target}[/b] removed")
422
+ await _mcp(conversation, "list")
423
+ return
257
424
 
258
- await conversation.post(MarkdownNote(f"**MCP Server '{name}' disabled.**"))
259
-
260
- else:
261
- await conversation.post(MarkdownNote(
262
- "**MCP Server Management**\n\n"
263
- "Usage:\n"
264
- "- `/mcp list` - List all MCP servers\n"
265
- "- `/mcp add <name> <url>` - Add an SSE MCP server\n"
266
- "- `/mcp remove <name>` - Remove an MCP server\n"
267
- "- `/mcp enable <name>` - Enable an MCP server\n"
268
- "- `/mcp disable <name>` - Disable an MCP server"
269
- ))
425
+ await _post(conversation, _HELP_MCP)
426
+
427
+
428
+ _HELP_MCP = (
429
+ "**/onecode:mcp** — manage MCP (Model Context Protocol) servers\n\n"
430
+ "**Sub-commands**\n"
431
+ "- `(none)` or `list` show configured servers\n"
432
+ "- `add <name> <url>` add an SSE server\n"
433
+ "- `enable <name|n>` / `disable <name|n>`\n"
434
+ "- `remove <name|n>`\n\n"
435
+ "**Examples**\n"
436
+ "- `/onecode:mcp` — list servers\n"
437
+ "- `/onecode:mcp add my-server https://example.com/mcp`\n"
438
+ "- `/onecode:mcp disable 1` — disable the first server in the list"
439
+ )
440
+
441
+
442
+ # ── helpers ────────────────────────────────────────────────────────────
443
+
444
+
445
+ def _resolve_indexed_name(conversation, raw, items, sub_label):
446
+ """Accept either a literal name or a 1-based index from the most recent list.
447
+
448
+ ``items`` is either a ``dict[name, ...]`` (skills) or already indexed
449
+ by name (MCP). Returns the resolved name or ``None`` after notifying
450
+ the user on error.
451
+ """
452
+ raw = (raw or "").strip()
453
+ if not raw:
454
+ return None
455
+ keys = list(items.keys())
456
+ if raw.isdigit():
457
+ idx = int(raw)
458
+ if 1 <= idx <= len(keys):
459
+ return keys[idx - 1]
460
+ conversation.notify(
461
+ f"Index {idx} out of range (1..{len(keys)}). Run `/onecode:{sub_label}` first to see numbers.",
462
+ title=f"/onecode:{sub_label}",
463
+ severity="error",
464
+ )
465
+ return None
466
+ if raw in items:
467
+ return raw
468
+ conversation.notify(
469
+ f"Unknown name '{raw}'. Run `/onecode:{sub_label}` first to see available names.",
470
+ title=f"/onecode:{sub_label}",
471
+ severity="error",
472
+ )
473
+ return None
474
+
475
+
476
+ # ── registration ──────────────────────────────────────────────────────
270
477
 
271
478
 
272
479
  def register_commands() -> None:
273
- platform_commands.register("model", "Set onecode LLM model", _model, "<model name>")
274
- platform_commands.register("provider", "Set onecode LLM provider", _provider, "<provider name>")
275
- platform_commands.register("skill", "Manage skills (list, add, remove, enable, disable)", _skill, "list|add|remove [name]")
276
- platform_commands.register("mcp", "Manage MCP servers (list, add, remove, enable, disable)", _mcp, "list|add|remove [name]")
480
+ """Register the slash commands exposed to the TUI prompt.
481
+
482
+ The help text intentionally covers both **what** the command does and
483
+ **how** to invoke it (sub-command + index syntax), so the
484
+ autocomplete panel is self-documenting.
485
+ """
486
+ platform_commands.register(
487
+ "help",
488
+ "Show onecode: command overview and current state",
489
+ _status,
490
+ "(no args)",
491
+ )
492
+ platform_commands.register(
493
+ "status",
494
+ "Show current provider, model, mode, skills, and MCP servers",
495
+ _status,
496
+ "(no args)",
497
+ )
498
+ platform_commands.register(
499
+ "provider",
500
+ "Show / switch the LLM provider (current + numbered list)",
501
+ _provider,
502
+ "[set <name|n>]",
503
+ )
504
+ platform_commands.register(
505
+ "model",
506
+ "Show / switch the LLM model for the current provider",
507
+ _model,
508
+ "[set <name|n>]",
509
+ )
510
+ platform_commands.register(
511
+ "skill",
512
+ "Manage skills: list, enable, disable, add, remove (accepts name or list number)",
513
+ _skill,
514
+ "list|enable|disable|add|remove [name|n]",
515
+ )
516
+ platform_commands.register(
517
+ "mcp",
518
+ "Manage MCP servers: list, add, enable, disable, remove (accepts name or list number)",
519
+ _mcp,
520
+ "list|add|enable|disable|remove [name|n]",
521
+ )