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>