Nymbo commited on
Commit
78d32d0
·
verified ·
1 Parent(s): 304768f

Create File_System.py

Browse files
Files changed (1) hide show
  1. Modules/File_System.py +506 -0
Modules/File_System.py ADDED
@@ -0,0 +1,506 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import stat
7
+ from datetime import datetime
8
+ from typing import Annotated, Optional
9
+ import re
10
+
11
+ import gradio as gr
12
+
13
+ from app import _log_call_end, _log_call_start, _truncate_for_log
14
+ from ._docstrings import autodoc
15
+
16
+
17
+ TOOL_SUMMARY = (
18
+ "Browse and manage files within a safe root. "
19
+ "Actions: list, read, write, append, mkdir, move, copy, delete, info, help. "
20
+ "Fill other fields as needed. "
21
+ "Use paths like `.` because all paths are relative to the root (`/`). "
22
+ "Use 'help' to see action-specific required fields and examples."
23
+ )
24
+
25
+ HELP_TEXT = (
26
+ "File System — actions and usage\n\n"
27
+ "Root: paths resolve under Nymbo-Tools/Filesystem by default (or NYMBO_TOOLS_ROOT if set). "
28
+ "Absolute paths are disabled unless UNSAFE_ALLOW_ABS_PATHS=1.\n\n"
29
+ "Actions and fields:\n"
30
+ "- list: path='.' (default), recursive=false, show_hidden=false, max_entries=200\n"
31
+ "- read: path, offset=0, max_chars=4000 (shows next_cursor when truncated)\n"
32
+ "- write: path, content (UTF-8), create_dirs=true\n"
33
+ "- append: path, content (UTF-8), create_dirs=true\n"
34
+ "- mkdir: path (directory), exist_ok=true\n"
35
+ "- move: path (src), dest_path (dst), overwrite=false\n"
36
+ "- copy: path (src), dest_path (dst), overwrite=false\n"
37
+ "- delete: path, recursive=true (required for directories)\n"
38
+ "- info: path\n"
39
+ "- help: show this guide\n\n"
40
+ "Errors are returned as JSON with fields: {status:'error', code, message, path?, hint?, data?}.\n\n"
41
+ "Examples:\n"
42
+ "- list current: action=list, path='.'\n"
43
+ "- make folder: action=mkdir, path='notes'\n"
44
+ "- write file: action=write, path='notes/todo.txt', content='hello'\n"
45
+ "- read file: action=read, path='notes/todo.txt', max_chars=200\n"
46
+ "- move file: action=move, path='notes/todo.txt', dest_path='notes/todo-old.txt', overwrite=true\n"
47
+ "- delete dir: action=delete, path='notes', recursive=true\n"
48
+ )
49
+
50
+
51
+ def _default_root() -> str:
52
+ # Prefer explicit root via env var
53
+ root = os.getenv("NYMBO_TOOLS_ROOT")
54
+ if root and root.strip():
55
+ return os.path.abspath(os.path.expanduser(root.strip()))
56
+ # Default to "Nymbo-Tools/Filesystem" alongside this module package
57
+ try:
58
+ here = os.path.abspath(__file__)
59
+ tools_dir = os.path.dirname(os.path.dirname(here)) # .../Nymbo-Tools
60
+ default_root = os.path.abspath(os.path.join(tools_dir, "Filesystem"))
61
+ return default_root
62
+ except Exception:
63
+ # Final fallback
64
+ return os.path.abspath(os.getcwd())
65
+
66
+
67
+ ROOT_DIR = _default_root()
68
+ # Ensure the default root directory exists to make listing/writing more convenient
69
+ try:
70
+ os.makedirs(ROOT_DIR, exist_ok=True)
71
+ except Exception:
72
+ pass
73
+ ALLOW_ABS = bool(int(os.getenv("UNSAFE_ALLOW_ABS_PATHS", "0")))
74
+
75
+ def _safe_err(exc: Exception | str) -> str:
76
+ """Return an error string with any absolute root replaced by '/' and slashes normalized.
77
+ This handles variants like backslashes and duplicate slashes in OS messages.
78
+ """
79
+ s = str(exc)
80
+ # Normalize to forward slashes for comparison
81
+ s_norm = s.replace("\\", "/")
82
+ root_fwd = ROOT_DIR.replace("\\", "/")
83
+ # Collapse duplicate slashes in root representation
84
+ root_variants = {ROOT_DIR, root_fwd, re.sub(r"/+", "/", root_fwd)}
85
+ for variant in root_variants:
86
+ if variant:
87
+ s_norm = s_norm.replace(variant, "/")
88
+ # Collapse duplicate slashes in final output
89
+ s_norm = re.sub(r"/+", "/", s_norm)
90
+ return s_norm
91
+
92
+
93
+ def _err(code: str, message: str, *, path: Optional[str] = None, hint: Optional[str] = None, data: Optional[dict] = None) -> str:
94
+ """Return a structured error JSON string.
95
+ Fields: status='error', code, message, path?, hint?, data?, root='/'
96
+ """
97
+ payload = {
98
+ "status": "error",
99
+ "code": code,
100
+ "message": message,
101
+ "root": "/",
102
+ }
103
+ if path is not None and path != "":
104
+ payload["path"] = path
105
+ if hint:
106
+ payload["hint"] = hint
107
+ if data:
108
+ payload["data"] = data
109
+ return json.dumps(payload, ensure_ascii=False)
110
+
111
+
112
+ def _resolve_path(path: str) -> tuple[str, str]:
113
+ """
114
+ Resolve a user-provided path to an absolute, normalized path constrained to ROOT_DIR
115
+ (unless UNSAFE_ALLOW_ABS_PATHS=1). Returns (abs_path, error_message). error_message is empty when ok.
116
+ """
117
+ try:
118
+ user_input = (path or ".").strip()
119
+ raw = os.path.expanduser(user_input)
120
+ if os.path.isabs(raw):
121
+ if not ALLOW_ABS:
122
+ # Absolute paths are not allowed in safe mode
123
+ return "", _err(
124
+ "absolute_path_disabled",
125
+ "Absolute paths are disabled in safe mode.",
126
+ path=raw.replace("\\", "/"),
127
+ hint="Use a path relative to / (e.g., notes/todo.txt)."
128
+ )
129
+ abs_path = os.path.abspath(raw)
130
+ else:
131
+ abs_path = os.path.abspath(os.path.join(ROOT_DIR, raw))
132
+ # Constrain to ROOT when not unsafe mode
133
+ if not ALLOW_ABS:
134
+ try:
135
+ common = os.path.commonpath([os.path.normpath(ROOT_DIR), os.path.normpath(abs_path)])
136
+ except Exception:
137
+ # Fallback to simple check
138
+ root_cmp = os.path.normcase(os.path.normpath(ROOT_DIR))
139
+ abs_cmp = os.path.normcase(os.path.normpath(abs_path))
140
+ if not abs_cmp.startswith(root_cmp):
141
+ return "", _err(
142
+ "path_outside_root",
143
+ "Path not allowed outside root.",
144
+ path=user_input.replace("\\", "/"),
145
+ hint="Use a path under / (the tool's root)."
146
+ )
147
+ else:
148
+ root_cmp = os.path.normcase(os.path.normpath(ROOT_DIR))
149
+ common_cmp = os.path.normcase(os.path.normpath(common))
150
+ if common_cmp != root_cmp:
151
+ return "", _err(
152
+ "path_outside_root",
153
+ "Path not allowed outside root.",
154
+ path=user_input.replace("\\", "/"),
155
+ hint="Use a path under / (the tool's root)."
156
+ )
157
+ return abs_path, ""
158
+ except Exception as exc:
159
+ return "", _err(
160
+ "resolve_path_failed",
161
+ "Failed to resolve path.",
162
+ path=(path or ""),
163
+ data={"error": _safe_err(exc)}
164
+ )
165
+
166
+
167
+ def _fmt_size(num_bytes: int) -> str:
168
+ units = ["B", "KB", "MB", "GB", "TB"]
169
+ size = float(num_bytes)
170
+ for unit in units:
171
+ if size < 1024.0:
172
+ return f"{size:.1f} {unit}"
173
+ size /= 1024.0
174
+ return f"{size:.1f} PB"
175
+
176
+
177
+ def _display_path(abs_path: str) -> str:
178
+ """Return a user-friendly path relative to ROOT_DIR using forward slashes.
179
+ Example: ROOT_DIR -> '/', a file under it -> '/sub/dir/file.txt'."""
180
+ try:
181
+ norm_root = os.path.normpath(ROOT_DIR)
182
+ norm_abs = os.path.normpath(abs_path)
183
+ common = os.path.commonpath([norm_root, norm_abs])
184
+ if os.path.normcase(common) == os.path.normcase(norm_root):
185
+ rel = os.path.relpath(norm_abs, norm_root)
186
+ if rel == ".":
187
+ return "/"
188
+ return "/" + rel.replace("\\", "/")
189
+ except Exception:
190
+ pass
191
+ # Fallback to original absolute path
192
+ return abs_path.replace("\\", "/")
193
+
194
+
195
+ def _list_dir(abs_path: str, *, show_hidden: bool, recursive: bool, max_entries: int) -> str:
196
+ lines: list[str] = []
197
+ total = 0
198
+ root_display = "/"
199
+ listing_display = _display_path(abs_path)
200
+ for root, dirs, files in os.walk(abs_path):
201
+ # filter hidden
202
+ if not show_hidden:
203
+ dirs[:] = [d for d in dirs if not d.startswith('.')]
204
+ files = [f for f in files if not f.startswith('.')]
205
+ try:
206
+ rel_root = os.path.relpath(root, ROOT_DIR)
207
+ except Exception:
208
+ rel_root = root
209
+ rel_root_disp = "/" if rel_root == "." else "/" + rel_root.replace("\\", "/")
210
+ lines.append(f"\n📂 {rel_root_disp}")
211
+ # sort
212
+ dirs.sort()
213
+ files.sort()
214
+ for d in dirs:
215
+ p = os.path.join(root, d)
216
+ try:
217
+ mtime = datetime.fromtimestamp(os.path.getmtime(p)).isoformat(sep=' ', timespec='seconds')
218
+ except Exception:
219
+ mtime = "?"
220
+ lines.append(f" • [DIR] {d} (modified {mtime})")
221
+ total += 1
222
+ if total >= max_entries:
223
+ lines.append(f"\n… Truncated at {max_entries} entries.")
224
+ return "\n".join(lines).strip()
225
+ for f in files:
226
+ p = os.path.join(root, f)
227
+ try:
228
+ size = _fmt_size(os.path.getsize(p))
229
+ mtime = datetime.fromtimestamp(os.path.getmtime(p)).isoformat(sep=' ', timespec='seconds')
230
+ except Exception:
231
+ size, mtime = "?", "?"
232
+ lines.append(f" • {f} ({size}, modified {mtime})")
233
+ total += 1
234
+ if total >= max_entries:
235
+ lines.append(f"\n… Truncated at {max_entries} entries.")
236
+ return "\n".join(lines).strip()
237
+ if not recursive:
238
+ break
239
+ header = f"Listing of {listing_display}\nRoot: {root_display}\nEntries: {total}"
240
+ return (header + "\n" + "\n".join(lines)).strip()
241
+
242
+
243
+ def _read_file(abs_path: str, *, offset: int, max_chars: int) -> str:
244
+ if not os.path.exists(abs_path):
245
+ return _err("file_not_found", f"File not found: {_display_path(abs_path)}", path=_display_path(abs_path))
246
+ if os.path.isdir(abs_path):
247
+ return _err("is_directory", f"Path is a directory, not a file: {_display_path(abs_path)}", path=_display_path(abs_path), hint="Provide a file path.")
248
+ try:
249
+ with open(abs_path, 'r', encoding='utf-8', errors='replace') as f:
250
+ data = f.read()
251
+ except Exception as exc:
252
+ return _err("read_failed", "Failed to read file.", path=_display_path(abs_path), data={"error": _safe_err(exc)})
253
+ total = len(data)
254
+ start = max(0, min(offset, total))
255
+ if max_chars > 0:
256
+ end = min(total, start + max_chars)
257
+ else:
258
+ end = total
259
+ chunk = data[start:end]
260
+ next_cursor = end if end < total else None
261
+ meta = {
262
+ "offset": start,
263
+ "returned": len(chunk),
264
+ "total": total,
265
+ "next_cursor": next_cursor,
266
+ "path": _display_path(abs_path),
267
+ }
268
+ header = (
269
+ f"Reading {_display_path(abs_path)}\n"
270
+ f"Offset {start}, returned {len(chunk)} of {total}."
271
+ + (f"\nNext cursor: {next_cursor}" if next_cursor is not None else "")
272
+ )
273
+ sep = "\n\n---\n\n"
274
+ return header + sep + chunk
275
+
276
+
277
+ def _ensure_parent(abs_path: str, create_dirs: bool) -> None:
278
+ parent = os.path.dirname(abs_path)
279
+ if parent and not os.path.exists(parent):
280
+ if create_dirs:
281
+ os.makedirs(parent, exist_ok=True)
282
+ else:
283
+ raise FileNotFoundError(f"Parent directory does not exist: {_display_path(parent)}")
284
+
285
+
286
+ def _write_file(abs_path: str, content: str, *, append: bool, create_dirs: bool) -> str:
287
+ try:
288
+ _ensure_parent(abs_path, create_dirs)
289
+ mode = 'a' if append else 'w'
290
+ with open(abs_path, mode, encoding='utf-8') as f:
291
+ f.write(content or "")
292
+ return f"{'Appended to' if append else 'Wrote'} file: {_display_path(abs_path)} (chars={len(content or '')})"
293
+ except Exception as exc:
294
+ return _err("write_failed", "Failed to write file.", path=_display_path(abs_path), data={"error": _safe_err(exc)})
295
+
296
+
297
+ def _mkdir(abs_path: str, exist_ok: bool) -> str:
298
+ try:
299
+ os.makedirs(abs_path, exist_ok=exist_ok)
300
+ return f"Created directory: {_display_path(abs_path)}"
301
+ except Exception as exc:
302
+ return _err("mkdir_failed", "Failed to create directory.", path=_display_path(abs_path), data={"error": _safe_err(exc)})
303
+
304
+
305
+ def _move_copy(action: str, src: str, dst: str, *, overwrite: bool) -> str:
306
+ try:
307
+ if not os.path.exists(src):
308
+ return _err("source_not_found", f"Source not found: {_display_path(src)}", path=_display_path(src))
309
+ if os.path.isdir(dst):
310
+ # allow moving into an existing directory
311
+ dst_path = os.path.join(dst, os.path.basename(src))
312
+ else:
313
+ dst_path = dst
314
+ if os.path.exists(dst_path):
315
+ if overwrite:
316
+ if os.path.isdir(dst_path):
317
+ shutil.rmtree(dst_path)
318
+ else:
319
+ os.remove(dst_path)
320
+ else:
321
+ return _err(
322
+ "destination_exists",
323
+ f"Destination already exists: {_display_path(dst_path)}",
324
+ path=_display_path(dst_path),
325
+ hint="Set overwrite=True to replace the destination."
326
+ )
327
+ if action == 'move':
328
+ shutil.move(src, dst_path)
329
+ else:
330
+ if os.path.isdir(src):
331
+ shutil.copytree(src, dst_path)
332
+ else:
333
+ shutil.copy2(src, dst_path)
334
+ return f"{action.capitalize()}d: {_display_path(src)} -> {_display_path(dst_path)}"
335
+ except Exception as exc:
336
+ return _err(f"{action}_failed", f"Failed to {action}.", path=_display_path(src), data={"error": _safe_err(exc), "destination": _display_path(dst)})
337
+
338
+
339
+ def _delete(abs_path: str, *, recursive: bool) -> str:
340
+ try:
341
+ if not os.path.exists(abs_path):
342
+ return _err("path_not_found", f"Path not found: {_display_path(abs_path)}", path=_display_path(abs_path))
343
+ if os.path.isdir(abs_path):
344
+ if not recursive:
345
+ # Refuse to delete a dir unless recursive=True
346
+ return _err("requires_recursive", "Refusing to delete a directory without recursive=True", path=_display_path(abs_path), hint="Pass recursive=True to delete a directory.")
347
+ shutil.rmtree(abs_path)
348
+ else:
349
+ os.remove(abs_path)
350
+ return f"Deleted: {_display_path(abs_path)}"
351
+ except Exception as exc:
352
+ return _err("delete_failed", "Failed to delete path.", path=_display_path(abs_path), data={"error": _safe_err(exc)})
353
+
354
+
355
+ def _info(abs_path: str) -> str:
356
+ try:
357
+ st = os.stat(abs_path)
358
+ except Exception as exc:
359
+ return _err("stat_failed", "Failed to stat path.", path=_display_path(abs_path), data={"error": _safe_err(exc)})
360
+ info = {
361
+ "path": _display_path(abs_path),
362
+ "type": "directory" if stat.S_ISDIR(st.st_mode) else "file",
363
+ "size": st.st_size,
364
+ "modified": datetime.fromtimestamp(st.st_mtime).isoformat(sep=' ', timespec='seconds'),
365
+ "created": datetime.fromtimestamp(st.st_ctime).isoformat(sep=' ', timespec='seconds'),
366
+ "mode": oct(st.st_mode),
367
+ "root": "/",
368
+ }
369
+ return json.dumps(info, indent=2)
370
+
371
+
372
+ @autodoc(summary=TOOL_SUMMARY)
373
+ def File_System(
374
+ action: Annotated[str, "Operation to perform: 'list', 'read', 'write', 'append', 'mkdir', 'move', 'copy', 'delete', 'info'."],
375
+ path: Annotated[str, "Target path, relative to root unless UNSAFE_ALLOW_ABS_PATHS=1."] = ".",
376
+ content: Annotated[Optional[str], "Content for write/append actions (UTF-8)."] = None,
377
+ dest_path: Annotated[Optional[str], "Destination for move/copy (relative to root unless unsafe absolute allowed)."] = None,
378
+ recursive: Annotated[bool, "For list (recurse into subfolders) and delete (required for directories)."] = False,
379
+ show_hidden: Annotated[bool, "Include hidden files (dotfiles)."] = False,
380
+ max_entries: Annotated[int, "Max entries to list (for list)."] = 200,
381
+ offset: Annotated[int, "Start offset for reading files (for read)."] = 0,
382
+ max_chars: Annotated[int, "Max characters to return when reading (0 = full file)."] = 4000,
383
+ create_dirs: Annotated[bool, "Create parent directories for write/append if missing."] = True,
384
+ overwrite: Annotated[bool, "Allow overwrite for move/copy destinations."] = False,
385
+ ) -> str:
386
+ _log_call_start(
387
+ "File_System",
388
+ action=action,
389
+ path=path,
390
+ dest_path=dest_path,
391
+ recursive=recursive,
392
+ show_hidden=show_hidden,
393
+ max_entries=max_entries,
394
+ offset=offset,
395
+ max_chars=max_chars,
396
+ create_dirs=create_dirs,
397
+ overwrite=overwrite,
398
+ )
399
+ action = (action or "").strip().lower()
400
+ if action not in {"list", "read", "write", "append", "mkdir", "move", "copy", "delete", "info", "help"}:
401
+ result = _err(
402
+ "invalid_action",
403
+ "Invalid action.",
404
+ hint="Choose from: list, read, write, append, mkdir, move, copy, delete, info, help."
405
+ )
406
+ _log_call_end("File_System", _truncate_for_log(result))
407
+ return result
408
+
409
+ abs_path, err = _resolve_path(path)
410
+ if err:
411
+ _log_call_end("File_System", _truncate_for_log(err))
412
+ return err
413
+
414
+ try:
415
+ if action == "help":
416
+ result = HELP_TEXT
417
+ elif action == "list":
418
+ if not os.path.exists(abs_path):
419
+ result = _err("path_not_found", f"Path not found: {_display_path(abs_path)}", path=_display_path(abs_path))
420
+ else:
421
+ result = _list_dir(abs_path, show_hidden=show_hidden, recursive=recursive, max_entries=max_entries)
422
+ elif action == "read":
423
+ result = _read_file(abs_path, offset=offset, max_chars=max_chars)
424
+ elif action in {"write", "append"}:
425
+ # Prevent attempts to write to root or any directory
426
+ if _display_path(abs_path) == "/" or os.path.isdir(abs_path):
427
+ result = _err(
428
+ "invalid_write_path",
429
+ "Invalid path for write/append.",
430
+ path=_display_path(abs_path),
431
+ hint="Provide a file path under / (e.g., /notes/todo.txt)."
432
+ )
433
+ else:
434
+ result = _write_file(abs_path, content or "", append=(action == "append"), create_dirs=create_dirs)
435
+ elif action == "mkdir":
436
+ result = _mkdir(abs_path, exist_ok=True)
437
+ elif action in {"move", "copy"}:
438
+ if not dest_path:
439
+ result = _err("missing_dest_path", "dest_path is required for move/copy (ignored for other actions).")
440
+ else:
441
+ abs_dst, err2 = _resolve_path(dest_path)
442
+ if err2:
443
+ result = err2
444
+ else:
445
+ result = _move_copy(action, abs_path, abs_dst, overwrite=overwrite)
446
+ elif action == "delete":
447
+ result = _delete(abs_path, recursive=recursive)
448
+ else: # info
449
+ result = _info(abs_path)
450
+ except Exception as exc:
451
+ result = _err("exception", "Unhandled error during operation.", data={"error": _safe_err(exc)})
452
+
453
+ _log_call_end("File_System", _truncate_for_log(result))
454
+ return result
455
+
456
+
457
+ def build_interface() -> gr.Interface:
458
+ return gr.Interface(
459
+ fn=File_System,
460
+ inputs=[
461
+ gr.Radio(
462
+ label="Action",
463
+ choices=["list", "read", "write", "append", "mkdir", "move", "copy", "delete", "info", "help"],
464
+ value="help",
465
+ ),
466
+ gr.Textbox(label="Path", placeholder=". or src/file.txt", max_lines=1, value="."),
467
+ gr.Textbox(label="Content (for write/append)", lines=6, placeholder="Text to write..."),
468
+ gr.Textbox(label="Destination (for move/copy)", max_lines=1),
469
+ gr.Checkbox(label="Recursive (list/delete)", value=False),
470
+ gr.Checkbox(label="Show hidden (list)", value=False),
471
+ gr.Slider(minimum=10, maximum=5000, step=10, value=200, label="Max entries (list)"),
472
+ gr.Slider(minimum=0, maximum=1_000_000, step=100, value=0, label="Offset (read)"),
473
+ gr.Slider(minimum=0, maximum=100_000, step=500, value=4000, label="Max chars (read, 0=all)"),
474
+ gr.Checkbox(label="Create parent dirs (write)", value=True),
475
+ gr.Checkbox(label="Overwrite destination (move/copy)", value=False),
476
+ ],
477
+ outputs=gr.Textbox(label="Result", lines=20),
478
+ title="File System",
479
+ description=(
480
+ "<div id=\"fs-desc\" style=\"text-align:center; overflow:hidden;\">Browse and interact with a filesystem. "
481
+ "Actions are required, fill other fields as needed."
482
+ "</div>"
483
+ ),
484
+ api_description=TOOL_SUMMARY,
485
+ flagging_mode="never",
486
+ submit_btn="Run",
487
+ css=(
488
+ """
489
+ /* Hide scrollbars/arrows that can appear on the description block in some browsers */
490
+ article.prose, .prose, .gr-prose {
491
+ overflow: visible !important;
492
+ max-height: none !important;
493
+ -ms-overflow-style: none !important; /* IE/Edge */
494
+ scrollbar-width: none !important; /* Firefox */
495
+ }
496
+ article.prose::-webkit-scrollbar,
497
+ .prose::-webkit-scrollbar,
498
+ .gr-prose::-webkit-scrollbar {
499
+ display: none !important; /* Chrome/Safari */
500
+ }
501
+ """
502
+ ),
503
+ )
504
+
505
+
506
+ __all__ = ["File_System", "build_interface"]