workdir outside project, gitkeeps

frdel committed Feb 8, 2026 at 14:54 UTC c223b468183b9f3d5b37d5b9462c9dc41dc2f768
29 files changed +426 -86
.gitignore
+6 -3
@@ -21,11 +21,14 @@ knowledge/custom/
21 instruments/
22
23 # Handle logs directory
24 -logs/*
24 +logs/**
25 +!logs/**/
26
27 # Handle tmp and usr directory
27 -tmp/*
28 -usr/*
28 +tmp/**
29 +!tmp/**/
30 +usr/**
31 +!usr/**/
32
33 # Handle knowledge directory
34
conf/projects.default.gitignore
+5 -8
@@ -1,13 +1,10 @@
1 -# A0 project meta folder
2 -.a0proj/
3 -
1 # Python environments & cache
5 -venv/
6 -**/__pycache__/
2 +venv/**
3 +**/__pycache__/**
4
5 # Node.js dependencies
9 -**/node_modules/
10 -**/.npm/
6 +**/node_modules/**
7 +**/.npm/**
8
9 # Version control metadata
13 -**/.git/
10 +**/.git/**
conf/workdir.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.extras.agent_info.md
+2 -1
@@ -1,3 +1,4 @@
1 # Agent info
2 Agent Number: {{number}}
3 -Profile: {{profile}}
\ No newline at end of file
3 +Profile: {{profile}}
4 +LLM: {{llm}}
\ No newline at end of file
prompts/agent.extras.workdir_structure.md renamed
+1 -1
@@ -1,4 +1,4 @@
1 -# File structure of project {{project_name}}
1 +# File structure of working directory {{folder}}
2 - this is filtered overview not full scan
3 - list yourself if needed
4 - maximum depth: {{max_depth}}
prompts/agent.system.main.tips.md
+1 -1
@@ -7,7 +7,7 @@ never assume success
7 memory refers memory tools not own knowledge
8
9 ## Files
10 -when not in project save files in /root
10 +when not in project save files in {{workdir_path}}
11 don't use spaces in file names
12
13 ## Skills
prompts/agent.system.main.tips.py new
+24
@@ -0,0 +1,24 @@
1 +from python.helpers.files import VariablesPlugin
2 +from python.helpers import settings
3 +from python.helpers import projects
4 +from python.helpers import runtime
5 +from python.helpers import files
6 +from typing import Any
7 +
8 +class WorkdirPath(VariablesPlugin):
9 + def get_variables(
10 + self, file: str, backup_dirs: list[str] | None = None, **kwargs
11 + ) -> dict[str, Any]:
12 +
13 + # agent = kwargs.get("_agent")
14 + # if agent and getattr(agent, "context", None):
15 + # project_name = projects.get_context_project_name(agent.context)
16 + # if project_name:
17 + # folder = projects.get_project_folder(project_name)
18 + # if runtime.is_development():
19 + # folder = files.normalize_a0_path(folder)
20 + # return {"workdir_path": folder}
21 +
22 + set = settings.get_settings()
23 + return {"workdir_path": set["workdir_path"]}
24 +
prompts/agent.system.tool.code_exe.md
+1 -1
@@ -5,7 +5,7 @@ place code in "code" arg; escape carefully and indent properly
5 select "runtime" arg: "terminal" "python" "nodejs" "output"
6 select "session" number, 0 default, others for multitasking
7 if code runs long, use runtime "output" to wait
8 -use reset true on next call to kill previous process when stuck default false
8 +use argument reset true on next call to kill previous process when stuck default false
9 use "pip" "npm" "apt-get" in "terminal" to install package
10 to output, use print() or console.log()
11 if tool outputs error, adjust code before retrying;
python/api/chat_files_path_get.py
+2 -2
@@ -1,5 +1,5 @@
1 from python.helpers.api import ApiHandler, Request, Response
2 -from python.helpers import files, memory, notification, projects, notification, runtime
2 +from python.helpers import files, memory, notification, projects, notification, runtime, settings
3 import os
4
5
@@ -14,7 +14,7 @@ class GetChatFilesPath(ApiHandler):
14 if project_name:
15 folder = files.normalize_a0_path(projects.get_project_folder(project_name))
16 else:
17 - folder = "/root" # root in container
17 + folder = settings.get_settings()["workdir_path"]
18
19 return {
20 "ok": True,
python/api/settings_workdir_file_structure.py new
+32
@@ -0,0 +1,32 @@
1 +from python.helpers.api import ApiHandler, Request, Response
2 +
3 +from python.helpers import file_tree, files
4 +
5 +
6 +class SettingsWorkdirFileStructure(ApiHandler):
7 + async def process(self, input: dict, request: Request) -> dict | Response:
8 + workdir_path = input.get("workdir_path", "")
9 + workdir_path = files.get_abs_path_development(workdir_path)
10 + if not workdir_path:
11 + raise Exception("workdir_path is required")
12 +
13 + tree = str(
14 + file_tree.file_tree(
15 + workdir_path,
16 + max_depth=int(input.get("workdir_max_depth", 0) or 0),
17 + max_files=int(input.get("workdir_max_files", 0) or 0),
18 + max_folders=int(input.get("workdir_max_folders", 0) or 0),
19 + max_lines=int(input.get("workdir_max_lines", 0) or 0),
20 + ignore=input.get("workdir_gitignore", "") or "",
21 + output_mode=file_tree.OUTPUT_MODE_STRING,
22 + )
23 + )
24 +
25 + if "\n" not in tree:
26 + tree += "\n # Empty"
27 +
28 + return {"data": tree}
29 +
30 + @classmethod
31 + def get_methods(cls) -> list[str]:
32 + return ["POST"]
python/extensions/message_loop_prompts_after/_70_include_agent_info.py
+5 -1
@@ -1,14 +1,18 @@
1 from python.helpers.extension import Extension
2 from agent import LoopData
3
4 +
5 class IncludeAgentInfo(Extension):
6 async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
6 -
7 +
8 # read prompt
9 agent_info_prompt = self.agent.read_prompt(
10 "agent.extras.agent_info.md",
11 number=self.agent.number,
12 profile=self.agent.config.profile or "Default",
13 + llm=self.agent.config.chat_model.provider
14 + + "/"
15 + + self.agent.config.chat_model.name,
16 )
17
18 # add agent info to the prompt
python/extensions/message_loop_prompts_after/_75_include_project_extras.py deleted
-47
@@ -1,47 +0,0 @@
1 -from python.helpers.extension import Extension
2 -from agent import LoopData
3 -from python.helpers import projects
4 -
5 -
6 -class IncludeProjectExtras(Extension):
7 - async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
8 -
9 - # active project
10 - project_name = projects.get_context_project_name(self.agent.context)
11 - if not project_name:
12 - return
13 -
14 - # project config
15 - project = projects.load_basic_project_data(project_name)
16 -
17 - # load file structure if enabled
18 - if project["file_structure"]["enabled"]:
19 - file_structure = projects.get_file_structure(project_name)
20 - gitignore = cleanup_gitignore(project["file_structure"]["gitignore"])
21 -
22 - # read prompt
23 - file_structure_prompt = self.agent.read_prompt(
24 - "agent.extras.project.file_structure.md",
25 - max_depth=project["file_structure"]["max_depth"],
26 - gitignore=gitignore,
27 - project_name=project_name,
28 - file_structure=file_structure,
29 - )
30 - # add file structure to the prompt
31 - loop_data.extras_temporary["project_file_structure"] = file_structure_prompt
32 -
33 -
34 -def cleanup_gitignore(gitignore_raw: str) -> str:
35 - """Process gitignore: split lines, strip, remove comments, remove empty lines."""
36 - gitignore_lines = []
37 - for line in gitignore_raw.split('\n'):
38 - # Strip whitespace
39 - line = line.strip()
40 - # Remove inline comments (everything after #)
41 - if '#' in line:
42 - line = line.split('#')[0].strip()
43 - # Keep only non-empty lines
44 - if line:
45 - gitignore_lines.append(line)
46 -
47 - return '\n'.join(gitignore_lines) if gitignore_lines else "nothing ignored"
python/extensions/message_loop_prompts_after/_75_include_workdir_extras.py new
+92
@@ -0,0 +1,92 @@
1 +from python.helpers.extension import Extension
2 +from agent import LoopData
3 +from python.helpers import projects
4 +from python.helpers import settings
5 +from python.helpers import runtime
6 +from python.helpers import file_tree
7 +from python.helpers import files
8 +
9 +class IncludeWorkdirExtras(Extension):
10 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
11 +
12 + project_name = projects.get_context_project_name(self.agent.context)
13 +
14 + enabled = False
15 + max_depth = 0
16 + max_files = 0
17 + max_folders = 0
18 + max_lines = 0
19 + gitignore_raw = ""
20 + folder = ""
21 + file_structure = ""
22 +
23 + if project_name:
24 + project = projects.load_basic_project_data(project_name)
25 + enabled = project["file_structure"]["enabled"]
26 +
27 + if not enabled:
28 + return
29 +
30 + max_depth = project["file_structure"]["max_depth"]
31 + gitignore_raw = project["file_structure"]["gitignore"]
32 +
33 + folder = projects.get_project_folder(project_name)
34 + if runtime.is_development():
35 + folder = files.normalize_a0_path(folder)
36 +
37 + file_structure = projects.get_file_structure(project_name)
38 + else:
39 + set = settings.get_settings()
40 + enabled = bool(set["workdir_show"])
41 +
42 + if not enabled:
43 + return
44 +
45 + max_depth = set["workdir_max_depth"]
46 + max_files = set["workdir_max_files"]
47 + max_folders = set["workdir_max_folders"]
48 + max_lines = set["workdir_max_lines"]
49 + gitignore_raw = set["workdir_gitignore"]
50 +
51 + folder = set["workdir_path"]
52 + scan_path = files.get_abs_path_development(folder)
53 +
54 + file_structure = str(
55 + file_tree.file_tree(
56 + scan_path,
57 + max_depth=max_depth,
58 + max_files=max_files,
59 + max_folders=max_folders,
60 + max_lines=max_lines,
61 + ignore=gitignore_raw,
62 + output_mode=file_tree.OUTPUT_MODE_STRING,
63 + )
64 + )
65 +
66 + gitignore = cleanup_gitignore(gitignore_raw)
67 +
68 + file_structure_prompt = self.agent.read_prompt(
69 + "agent.extras.workdir_structure.md",
70 + max_depth=max_depth,
71 + gitignore=gitignore,
72 + folder=folder,
73 + file_structure=file_structure,
74 + )
75 +
76 + loop_data.extras_temporary["project_file_structure"] = file_structure_prompt
77 +
78 +
79 +def cleanup_gitignore(gitignore_raw: str) -> str:
80 + """Process gitignore: split lines, strip, remove comments, remove empty lines."""
81 + gitignore_lines = []
82 + for line in gitignore_raw.split('\n'):
83 + # Strip whitespace
84 + line = line.strip()
85 + # Remove inline comments (everything after #)
86 + if '#' in line:
87 + line = line.split('#')[0].strip()
88 + # Keep only non-empty lines
89 + if line:
90 + gitignore_lines.append(line)
91 +
92 + return '\n'.join(gitignore_lines) if gitignore_lines else "nothing ignored"
python/helpers/backup.py
-3
@@ -63,9 +63,6 @@ class BackupService:
63 return f"""# User data
64 # All persistent user data is now centralized in /usr for easier backup and restore
65 {agent_root}/usr/**
66 -
67 -# Root folder
68 -/root/**
66 """
67
68 def _get_agent_zero_version(self) -> str:
python/helpers/file_tree.py
+19 -9
@@ -8,7 +8,7 @@ from typing import Any, Callable, Iterable, Literal, Optional, Sequence
8
9 from pathspec import PathSpec
10
11 -from python.helpers.files import get_abs_path
11 +from python.helpers import files as files_helper
12
13 SORT_BY_NAME = "name"
14 SORT_BY_CREATED = "created"
@@ -62,10 +62,12 @@ def file_tree(
62 :data:`OUTPUT_MODE_NESTED`.
63
64 Returns:
65 - ``OUTPUT_MODE_STRING`` → ``str``: multi-line ASCII tree.
66 - ``OUTPUT_MODE_FLAT`` → ``list[dict]``: flattened sequence of TreeItem dictionaries.
67 - ``OUTPUT_MODE_NESTED`` → ``list[dict]``: nested TreeItem dictionaries where folders
68 - include ``items`` arrays.
65 + ``OUTPUT_MODE_STRING`` → ``str``: multi-line ASCII tree. The first line is the root banner and
66 + uses a dockerized absolute path for display.
67 + ``OUTPUT_MODE_FLAT`` → ``list[dict]``: flattened sequence of TreeItem dictionaries, with a
68 + synthetic root folder item prepended at index 0 (using a dockerized absolute path for display).
69 + ``OUTPUT_MODE_NESTED`` → ``list[dict]``: a single synthetic root folder item (using a dockerized
70 + absolute path for display) whose ``items`` contains the nested TreeItem dictionaries.
71
72 Notes:
73 * The utility is synchronous; avoid calling from latency-sensitive async loops.
@@ -81,7 +83,8 @@ def file_tree(
83 epoch = item[\"created\"].timestamp()
84
85 """
84 - abs_root = get_abs_path(relative_path)
86 + abs_root = files_helper.get_abs_path(relative_path)
87 + output_root = files_helper.get_abs_path_dockerized(relative_path)
88
89 if not os.path.exists(abs_root):
90 raise FileNotFoundError(f"Path does not exist: {relative_path!r}")
@@ -234,8 +237,15 @@ def file_tree(
237 if not visible_ids or id(node) in visible_ids:
238 yield node
239
240 + def make_root_item(items: list[dict] | None) -> dict:
241 + root_item = root_node.as_dict()
242 + root_item["name"] = output_root
243 + root_item["text"] = f"{output_root.rstrip(os.sep)}/"
244 + root_item["items"] = items
245 + return root_item
246 +
247 if output_mode == OUTPUT_MODE_STRING:
238 - display_name = relative_path.strip() or root_name
248 + display_name = output_root #relative_path.strip() or root_name
249 root_line = f"{display_name.rstrip(os.sep)}/"
250 lines = [root_line]
251 for node in iter_visible():
@@ -243,9 +253,9 @@ def file_tree(
253 return "\n".join(lines)
254
255 if output_mode == OUTPUT_MODE_FLAT:
246 - return _build_tree_items_flat(list(iter_visible()))
256 + return [make_root_item(None)] + _build_tree_items_flat(list(iter_visible()))
257
248 - return _to_nested_structure(root_node.items or [])
258 + return [make_root_item(_to_nested_structure(root_node.items or []))]
259
260
261 @dataclass(slots=True)
python/helpers/files.py
+13
@@ -509,6 +509,19 @@ def get_abs_path(*relative_paths):
509 "Convert relative paths to absolute paths based on the base directory."
510 return os.path.join(get_base_dir(), *relative_paths)
511
512 +def get_abs_path_dockerized(*relative_paths):
513 + "Ensures the abs path is dockerized (i.e. /a0/... path)"
514 + abs = get_abs_path(*relative_paths)
515 + from python.helpers import runtime
516 + if runtime.is_dockerized():
517 + return abs
518 + return normalize_a0_path(abs)
519 +
520 +def get_abs_path_development(*relative_paths):
521 + "Ensures the abs path is relevant for dev environment"
522 + abs = get_abs_path(*relative_paths)
523 + return fix_dev_path(abs)
524 +
525
526 def deabsolute_path(path: str):
527 "Convert absolute paths to relative paths based on the base directory."
python/helpers/settings.py
+16 -1
@@ -50,7 +50,6 @@ def get_default_value(name: str, value: T) -> T:
50 )
51 return value
52
53 -
53 class Settings(TypedDict):
54 version: str
55
@@ -96,6 +95,14 @@ class Settings(TypedDict):
95 agent_memory_subdir: str
96 agent_knowledge_subdir: str
97
98 + workdir_path: str
99 + workdir_show: bool
100 + workdir_max_depth: int
101 + workdir_max_files: int
102 + workdir_max_folders: int
103 + workdir_max_lines: int
104 + workdir_gitignore: str
105 +
106 memory_recall_enabled: bool
107 memory_recall_delayed: bool
108 memory_recall_interval: int
@@ -479,6 +486,7 @@ def _write_sensitive_settings(settings: Settings):
486
487
488 def get_default_settings() -> Settings:
489 + gitignore = files.read_file(files.get_abs_path("conf/workdir.gitignore"))
490 return Settings(
491 version=_get_version(),
492 chat_model_provider=get_default_value("chat_model_provider", "openrouter"),
@@ -536,6 +544,13 @@ def get_default_settings() -> Settings:
544 agent_profile=get_default_value("agent_profile", "agent0"),
545 agent_memory_subdir=get_default_value("agent_memory_subdir", "default"),
546 agent_knowledge_subdir=get_default_value("agent_knowledge_subdir", "custom"),
547 + workdir_path=get_default_value("workdir_path", files.get_abs_path_dockerized("usr/workdir")),
548 + workdir_show=get_default_value("workdir_show", True),
549 + workdir_max_depth=get_default_value("workdir_max_depth", 5),
550 + workdir_max_files=get_default_value("workdir_max_files", 20),
551 + workdir_max_folders=get_default_value("workdir_max_folders", 20),
552 + workdir_max_lines=get_default_value("workdir_max_lines", 250),
553 + workdir_gitignore=get_default_value("workdir_gitignore", gitignore),
554 rfc_auto_docker=get_default_value("rfc_auto_docker", True),
555 rfc_url=get_default_value("rfc_url", "localhost"),
556 rfc_password="",
python/tools/code_execution_tool.py
+19 -7
@@ -3,7 +3,7 @@ from dataclasses import dataclass
3 import shlex
4 import time
5 from python.helpers.tool import Tool, Response
6 -from python.helpers import files, rfc_exchange, projects, runtime
6 +from python.helpers import files, rfc_exchange, projects, runtime, settings
7 from python.helpers.print_style import PrintStyle
8 from python.helpers.shell_local import LocalInteractiveSession
9 from python.helpers.shell_ssh import SSHInteractiveSession
@@ -135,6 +135,7 @@ class CodeExecution(Tool):
135
136 # initialize local or remote interactive shell interface for session 0 if needed
137 if session is not None and session not in shells:
138 + cwd = await self.ensure_cwd()
139 if self.agent.config.code_exec_ssh_enabled:
140 pswd = (
141 self.agent.config.code_exec_ssh_pass
@@ -147,10 +148,10 @@ class CodeExecution(Tool):
148 self.agent.config.code_exec_ssh_port,
149 self.agent.config.code_exec_ssh_user,
150 pswd,
150 - cwd=self.get_cwd(),
151 + cwd=cwd,
152 )
153 else:
153 - shell = LocalInteractiveSession(cwd=self.get_cwd())
154 + shell = LocalInteractiveSession(cwd=cwd)
155
156 shells[session] = ShellWrap(id=session, session=shell, running=False)
157 await shell.connect()
@@ -472,13 +473,24 @@ class CodeExecution(Tool):
473 output = truncate_text_agent(agent=self.agent, output=output, threshold=1000000) # ~1MB, larger outputs should be dumped to file, not read from terminal
474 return output
475
475 - def get_cwd(self):
476 + async def ensure_cwd(self) -> str | None:
477 project_name = projects.get_context_project_name(self.agent.context)
477 - if not project_name:
478 + if project_name:
479 + path = projects.get_project_folder(project_name)
480 + else:
481 + set = settings.get_settings()
482 + path = set.get("workdir_path")
483 +
484 + if not path:
485 return None
479 - project_path = projects.get_project_folder(project_name)
480 - normalized = files.normalize_a0_path(project_path)
486 +
487 + normalized = files.normalize_a0_path(path)
488 + await runtime.call_development_function(make_dir, normalized)
489 return normalized
490 +
491 +def make_dir(path: str):
492 + import os
493 + os.makedirs(path, exist_ok=True)
494
495
496
\ No newline at end of file
run_ui.py
+2
@@ -6,6 +6,7 @@ import socket
6 import struct
7 from functools import wraps
8 import threading
9 +import asyncio
10
11 import uvicorn
12 from flask import Flask, request, Response, session, redirect, url_for, render_template_string
@@ -197,6 +198,7 @@ async def login_handler():
198 session['authentication'] = login.get_credentials_hash()
199 return redirect(url_for('serve_index'))
200 else:
201 + await asyncio.sleep(1)
202 error = 'Invalid Credentials. Please try again.'
203
204 login_page_content = files.read_file("webui/login.html")
usr/agents/.gitkeep
usr/knowledge/.gitkeep
usr/knowledge/main/.gitkeep
usr/knowledge/solutions/.gitkeep
usr/workdir/.gitkeep
webui/components/settings/agent/agent-settings.html
+13
@@ -51,6 +51,12 @@
51 <span>Speech</span>
52 </a>
53 </li>
54 + <li>
55 + <a href="#section-workdir">
56 + <img src="/public/folder.svg" alt="Workdir" />
57 + <span>Workdir</span>
58 + </a>
59 + </li>
60 </ul>
61 </nav>
62
@@ -82,6 +88,13 @@
88 <div id="section-speech" class="section">
89 <x-component path="settings/agent/speech.html"></x-component>
90 </div>
91 +
92 + <div id="section-workdir" class="section">
93 + <x-component path="settings/agent/workdir.html"></x-component>
94 + </div>
95 +
96 +
97 +
98 </div>
99 </template>
100 </div>
webui/components/settings/agent/workdir-file-structure-test.html new
+23
@@ -0,0 +1,23 @@
1 +<html>
2 + <head>
3 + <title>Workdir structure test</title>
4 +
5 + <script type="module">
6 + import { store as settingsStore } from "/components/settings/settings-store.js";
7 + </script>
8 + </head>
9 +
10 + <body>
11 + <div x-data>
12 + <template x-if="$store.settings && $store.settings.settings">
13 + <div>
14 + <h3>Workdir structure test - <span x-text="$store.settings.settings.workdir_path"></span></h3>
15 + <pre x-text="$store.settings.workdirFileStructureTestOutput"></pre>
16 + </div>
17 + </template>
18 + </div>
19 + </body>
20 +
21 + <style>
22 + </style>
23 +</html>
webui/components/settings/agent/workdir.html new
+113
@@ -0,0 +1,113 @@
1 +<html>
2 + <head>
3 + <title>Working directory</title>
4 +
5 + <script type="module">
6 + import { store } from "/components/settings/settings-store.js";
7 + import { store as fileBrowserStore } from "/components/modals/file-browser/file-browser-store.js";
8 + </script>
9 + </head>
10 +
11 + <body>
12 + <div x-data>
13 + <template x-if="$store.settings?.settings">
14 + <div>
15 + <div class="section-title">Default working directory</div>
16 + <div class="section-description">
17 + When project is not selected, agent is instructed to use this directory as it's workplace. New terminal sessions will be spawned with this CWD.
18 + </div>
19 +
20 + <div class="field">
21 + <div class="field-label">
22 + <div class="field-title">Workdir path</div>
23 + <div class="field-description">Full path to the working directory inside the linux container. Default is /a0/usr/workdir.</div>
24 + </div>
25 + <div class="field-control">
26 + <input type="text" x-model="$store.settings.settings.workdir_path" />
27 + </div>
28 + </div>
29 +
30 + <div class="field">
31 + <div class="field-label">
32 + <div class="field-title">Show workdir structure to the agent</div>
33 + <div class="field-description">
34 + When turned on, the workdir file structure will be injected into the context window of the agent.
35 + </div>
36 + </div>
37 + <div class="field-control">
38 + <label class="toggle">
39 + <input type="checkbox" x-model="$store.settings.settings.workdir_show" />
40 + <span class="toggler"></span>
41 + </label>
42 + </div>
43 + </div>
44 +
45 + <template x-if="$store.settings.settings.workdir_show">
46 + <div>
47 + <div class="field">
48 + <div class="field-label">
49 + <div class="field-title">Max Depth</div>
50 + <div class="field-description">Set the maximum depth for the file structure (0 = unlimited).</div>
51 + </div>
52 + <div class="field-control">
53 + <input type="number" x-model.number="$store.settings.settings.workdir_max_depth" min="0" step="1" />
54 + </div>
55 + </div>
56 +
57 + <div class="field">
58 + <div class="field-label">
59 + <div class="field-title">Max Lines</div>
60 + <div class="field-description">Maximum total lines outputted for the agent (0 = unlimited).</div>
61 + </div>
62 + <div class="field-control">
63 + <input type="number" x-model.number="$store.settings.settings.workdir_max_lines" min="0" step="1" />
64 + </div>
65 + </div>
66 +
67 + <div class="field">
68 + <div class="field-label">
69 + <div class="field-title">Max Folders</div>
70 + <div class="field-description">Maximum number of subfolders to display under one folder (0 = unlimited).</div>
71 + </div>
72 + <div class="field-control">
73 + <input type="number" x-model.number="$store.settings.settings.workdir_max_folders" min="0" step="1" />
74 + </div>
75 + </div>
76 +
77 + <div class="field">
78 + <div class="field-label">
79 + <div class="field-title">Max Files</div>
80 + <div class="field-description">Maximum number of files to display under one folder (0 = unlimited).</div>
81 + </div>
82 + <div class="field-control">
83 + <input type="number" x-model.number="$store.settings.settings.workdir_max_files" min="0" step="1" />
84 + </div>
85 + </div>
86 +
87 + <div class="field field-full">
88 + <div class="field-label">
89 + <div class="field-title">Ignored files / folders</div>
90 + <div class="field-description">Specify patterns in gitignore format.</div>
91 + </div>
92 + <div class="field-control">
93 + <textarea x-model="$store.settings.settings.workdir_gitignore" rows="5" placeholder="Enter gitignore patterns (e.g., node_modules/, .git/, *.log)"></textarea>
94 + </div>
95 + </div>
96 +
97 + <div class="field">
98 + <div class="field-label">
99 + <div class="field-title">Test output</div>
100 + <div class="field-description">Preview the current workdir structure output.</div>
101 + </div>
102 + <div class="field-control">
103 + <button class="btn btn-field" @click="$store.settings.testWorkdirFileStructure()">Test output</button>
104 + <button class="btn btn-field" style="margin-left: 0.5em;" @click="$store.fileBrowser.open($store.settings.settings.workdir_path)">Browse</button>
105 + </div>
106 + </div>
107 + </div>
108 + </template>
109 + </div>
110 + </template>
111 + </div>
112 + </body>
113 +</html>
webui/components/settings/settings-store.js
+20
@@ -27,6 +27,7 @@ const model = {
27 error: null,
28 settings: null,
29 additional: null,
30 + workdirFileStructureTestOutput: "",
31
32 // Tab state
33 _activeTab: DEFAULT_TAB,
@@ -152,6 +153,25 @@ const model = {
153 }
154 },
155
156 + async testWorkdirFileStructure() {
157 + if (!this.settings) return;
158 + try {
159 + const response = await API.callJsonApi("settings_workdir_file_structure", {
160 + workdir_path: this.settings.workdir_path,
161 + workdir_max_depth: this.settings.workdir_max_depth,
162 + workdir_max_files: this.settings.workdir_max_files,
163 + workdir_max_folders: this.settings.workdir_max_folders,
164 + workdir_max_lines: this.settings.workdir_max_lines,
165 + workdir_gitignore: this.settings.workdir_gitignore,
166 + });
167 + this.workdirFileStructureTestOutput = response?.data || "";
168 + window.openModal("settings/agent/workdir-file-structure-test.html");
169 + } catch (e) {
170 + console.error("Error testing workdir file structure:", e);
171 + toast("Error testing workdir file structure", "error");
172 + }
173 + },
174 +
175 // Field helpers for external components
176 // Handle button field clicks (opens sub-modals)
177 async handleFieldButton(field) {
webui/public/folder.svg
+7 -1
@@ -1 +1,7 @@
1 -<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 23 16.3"><path d="M21.9 5.3h-.7V3.8c0-.6-.5-1.1-1.1-1.1h-8.9L9.9.7C9.6.2 9.3 0 8.8 0H3c-.6 0-1.1.5-1.1 1.2v4.2h-.8c-.3 0-.6.1-.9.4-.1.1-.2.5-.2.8l1 8.8c.1.6.6 1 1.1 1h18.6c.6 0 1.1-.4 1.1-1l1-8.8c0-.3-.1-.7-.3-.9 0-.3-.3-.4-.6-.4M2.8 1.1c0-.1.1-.1.2-.1h5.8c.1 0 .1 0 .2.1l1.4 2.3c.1.1.2.2.4.2H20c.1 0 .2.1.2.2v1.5H2.8zM21 15.2c0 .1-.1.1-.2.1H2.2c-.1 0-.2-.1-.2-.1L1 6.5v-.1l.1-.1h20.7c.1 0 .1 0 .1.1v.1z" style="fill:#a0a0a0"/></svg>
\ No newline at end of file
1 +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg width="100%" height="100%" viewBox="0 0 23 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
4 + <g transform="matrix(0.993712,0,0,0.982491,0.0736794,0.107034)">
5 + <path d="M21.9,5.3L21.2,5.3L21.2,3.8C21.2,3.2 20.7,2.7 20.1,2.7L11.2,2.7L9.9,0.7C9.6,0.2 9.3,0 8.8,0L3,0C2.4,0 1.9,0.5 1.9,1.2L1.9,5.4L1.1,5.4C0.8,5.4 0.5,5.5 0.2,5.8C0.1,5.9 -0,6.3 -0,6.6L1,15.4C1.1,16 1.6,16.4 2.1,16.4L20.7,16.4C21.3,16.4 21.8,16 21.8,15.4L22.647,6.598C22.647,6.298 22.733,5.777 22.561,5.567C22.367,5.33 22.2,5.3 21.9,5.3M2.487,0.716C2.487,0.616 2.587,0.616 2.687,0.616L8.8,0.623C8.9,0.623 8.9,0.623 9,0.723L10.41,3.107C10.51,3.207 10.61,3.307 10.81,3.307L20.452,3.307C20.552,3.307 20.652,3.407 20.652,3.507L20.643,5.3L2.487,5.293L2.487,0.716ZM21.339,15.663C21.339,15.763 21.239,15.763 21.139,15.763L1.888,15.821C1.788,15.821 1.688,15.721 1.688,15.721L0.662,6.176L0.662,6.076L0.762,5.976L22.114,5.918C22.214,5.918 22.214,5.918 22.214,6.018L22.214,6.118L21.339,15.663Z" style="fill:rgb(160,160,160);fill-rule:nonzero;"/>
6 + </g>
7 +</svg>