skills + ui fixes

frdel committed Feb 2, 2026 at 19:45 UTC 5bb41360c255a82a0a7026f700462d2e0cdf5c1d
5 files changed +37 -128
conf/skill.default.gitignore new
+10
@@ -0,0 +1,10 @@
1 +# Python environments & cache
2 +venv/
3 +**/__pycache__/
4 +
5 +# Node.js dependencies
6 +**/node_modules/
7 +**/.npm/
8 +
9 +# Version control metadata
10 +**/.git/
prompts/agent.system.tool.skills.md
+3 -48
@@ -116,40 +116,10 @@ Security:
116 - Only files within skill directory accessible
117 - Supports markdown, text, code files
118
119 -### 4. search skills by query
120 -
121 -Searches skills by text matching in name, description, and tags
122 -Returns ranked results by relevance score
123 -Use when: looking for skills without knowing exact name
124 -
125 -~~~json
126 -{
127 - "thoughts": [
128 - "User needs web scraping capability",
129 - "Not sure of exact skill name",
130 - "Searching for web-related skills"
131 - ],
132 - "headline": "Searching for web scraping skills",
133 - "tool_name": "skills_tool",
134 - "tool_args": {
135 - "method": "search",
136 - "query": "web scraping html parsing"
137 - }
138 -}
139 -~~~
140 -
141 -Required args:
142 -- query: search text (searches name, description, tags)
143 -
144 -Scoring:
145 -- Name match: +3 points
146 -- Description match: +2 points
147 -- Tag match: +1 point per tag
148 -- Results sorted by descending score
119
120 ## Running skill scripts
121
152 -When a skill includes scripts (listed under its files), use code_execution_tool directly to run them.
122 +When a skill includes scripts (listed under its files), use code_execution_tool with runtime terminal directly to run them.
123 The skill's "load" output shows the skill directory path and lists available scripts.
124 Use read_file to inspect a script before running it if needed.
125
@@ -166,28 +136,13 @@ Example: running a Python script from a skill
136 ],
137 "headline": "Converting PDF to images",
138 "tool_name": "code_execution_tool",
169 - "tool_args": {
170 - "runtime": "python",
171 - "code": "import subprocess\nsubprocess.run(['python', '/path/to/skill/scripts/convert_pdf_to_images.py', '/path/to/document.pdf', '/tmp/images'], check=True)"
172 - }
173 -}
174 -~~~
175 -
176 -Example: running a shell script from a skill
177 -~~~json
178 -{
179 - "thoughts": [
180 - "Skill provides a shell script for data processing",
181 - "Running it via terminal runtime"
182 - ],
183 - "headline": "Running data processing script",
184 - "tool_name": "code_execution_tool",
139 "tool_args": {
140 "runtime": "terminal",
187 - "code": "cd /path/to/skill && bash scripts/process.sh /data/input.csv /tmp/output"
141 + "code": "python /path/to/skill/scripts/convert_pdf_to_images.py /path/to/document.pdf /tmp/images"
142 }
143 }
144 ~~~
145 +
146 ## Best Practices
147
148 ### When to use skills vs other tools
python/helpers/files.py
+1 -1
@@ -352,7 +352,7 @@ def find_file_in_dirs(_filename: str, _directories: list[str]):
352 )
353
354
355 -def get_unique_filenames_in_dirs(dir_paths: list[str], type: Literal["file", "dir", "any"] = "file", pattern: str = "*"):
355 +def get_unique_filenames_in_dirs(dir_paths: list[str], pattern: str = "*", type: Literal["file", "dir", "any"] = "file"):
356 # returns absolute paths for unique filenames, priority by order in dir_paths
357 seen = set()
358 result = []
python/tools/skills_tool.py
+18 -76
@@ -4,8 +4,7 @@ from pathlib import Path
4 from typing import List
5
6 from python.helpers.tool import Tool, Response
7 -from python.helpers import files
8 -from python.helpers import projects
7 +from python.helpers import projects, files, file_tree
8 from python.helpers import skills as skills_helper
9
10
@@ -32,9 +31,9 @@ class SkillsTool(Tool):
31 try:
32 if method == "list":
33 return Response(message=self._list(), break_loop=False)
35 - if method == "search":
36 - query = str(kwargs.get("query") or "").strip()
37 - return Response(message=self._search(query), break_loop=False)
34 + # if method == "search":
35 + # query = str(kwargs.get("query") or "").strip()
36 + # return Response(message=self._search(query), break_loop=False)
37 if method == "load":
38 skill_name = str(kwargs.get("skill_name") or "").strip()
39 return Response(message=self._load(skill_name), break_loop=False)
@@ -159,8 +158,7 @@ class SkillsTool(Tool):
158
159 if referenced_files:
160 lines.append("Files in skill directory (use skills_tool method=read_file to open):")
162 - for p in referenced_files:
163 - lines.append(f"- {p}")
161 + lines.append(referenced_files)
162 else:
163 lines.append("No additional files found in skill directory.")
164
@@ -196,77 +194,21 @@ class SkillsTool(Tool):
194 text = content.decode("utf-8", errors="replace")
195 return f"File: {file_path}\n\n{text}"
196
199 - def _list_skill_files(self, skill_dir: Path, *, max_files: int = 80) -> List[str]:
197 + def _list_skill_files(self, skill_dir: Path, *, max_files: int = 80) -> str:
198 if not skill_dir.exists():
199 return []
200
203 - results: List[str] = []
204 -
205 - preferred_dirs = ["scripts", "references", "assets", "templates", "docs"]
206 -
207 - # 1) Root-level files (excluding SKILL.md)
208 - try:
209 - for p in sorted(skill_dir.iterdir(), key=lambda x: x.name):
210 - if len(results) >= max_files:
211 - return results
212 - if p.name.startswith("."):
213 - continue
214 - if p.is_file():
215 - if p.name == "SKILL.md":
216 - continue
217 - results.append(p.name)
218 - except Exception:
219 - pass
220 -
221 - # 2) Preferred optional directories (one level deep)
222 - for dname in preferred_dirs:
223 - dpath = skill_dir / dname
224 - if not dpath.exists() or not dpath.is_dir():
225 - continue
226 - try:
227 - for p in sorted(dpath.iterdir(), key=lambda x: x.name):
228 - if len(results) >= max_files:
229 - return results
230 - if p.name.startswith("."):
231 - continue
232 - if p.is_file():
233 - results.append(f"{dname}/{p.name}")
234 - elif p.is_dir():
235 - # Show one nested level (common in assets/templates/*)
236 - nested_added = False
237 - try:
238 - for sub in sorted(p.iterdir(), key=lambda x: x.name):
239 - if sub.name.startswith("."):
240 - continue
241 - if sub.is_file():
242 - results.append(f"{dname}/{p.name}/{sub.name}")
243 - nested_added = True
244 - break
245 - except Exception:
246 - pass
247 - if not nested_added:
248 - results.append(f"{dname}/{p.name}/")
249 - except Exception:
250 - continue
251 -
252 - # 3) Other directories (one level deep)
253 - try:
254 - for p in sorted(skill_dir.iterdir(), key=lambda x: x.name):
255 - if len(results) >= max_files:
256 - return results
257 - if p.name.startswith(".") or p.name in preferred_dirs:
258 - continue
259 - if p.is_dir():
260 - for sub in sorted(p.iterdir(), key=lambda x: x.name):
261 - if len(results) >= max_files:
262 - return results
263 - if sub.name.startswith("."):
264 - continue
265 - if sub.is_file():
266 - results.append(f"{p.name}/{sub.name}")
267 - except Exception:
268 - pass
269 -
270 - return results
201 + tree = file_tree.file_tree(
202 + str(skill_dir),
203 + max_depth=10,
204 + folders_first=True,
205 + max_files=100,
206 + max_folders=100,
207 + output_mode="string",
208 + max_lines=300,
209 + ignore=files.read_file("conf/skill.default.gitignore")
210 + )
211 + return tree
212 +
213
214
webui/components/messages/process-group/process-group.css
+5 -3
@@ -463,11 +463,13 @@
463 margin-top: 0;
464 transition: grid-template-rows 0.2s ease-out, opacity 0.15s ease-out, margin-top 0.2s ease-out;
465 overflow: hidden;
466 - -webkit-overflow-scrolling: touch; /* smooth scrolling on iOS */
467 - overscroll-behavior: contain; /* avoid scroll chaining */
466 max-width: 100%;
467 }
468
469 +.process-step-detail-content{
470 + white-space: pre-wrap;
471 +}
472 +
473 .process-step-detail,
474 .process-step-detail-scroll {
475 scrollbar-width: thin; /* Firefox */
@@ -534,7 +536,7 @@
536 font-size: var(--font-size-xs);
537 line-height: 1.5;
538 -webkit-overflow-scrolling: touch; /* smooth scrolling on iOS */
537 - overscroll-behavior-x: contain; /* avoid scroll chaining */
539 + overscroll-behavior-x: contain;
540 }
541
542 /* .process-step-detail-scroll pre {