frdel commited on
Commit
1f528d5
·
1 Parent(s): c3bf03e

project finalizing, openrouter embeddings

Browse files
.gitignore CHANGED
@@ -5,10 +5,9 @@
5
  *.py[cod]
6
  **/.conda/
7
 
8
- #Ignore cursor rules
9
  .cursor/
10
  .windsurf/
11
- .cursorindexingignore
12
 
13
  # ignore test files in root dir
14
  /*.test.py
@@ -43,11 +42,7 @@ instruments/**
43
 
44
  # Global rule to include .gitkeep files anywhere
45
  !**/.gitkeep
 
 
46
  agent_history.gif
47
 
48
- # specify, specstory extension data
49
- .serena
50
- .specify
51
- .specstory
52
- specs/*/.reviews
53
- specs/*/reviews
 
5
  *.py[cod]
6
  **/.conda/
7
 
8
+ #Ignore IDE files
9
  .cursor/
10
  .windsurf/
 
11
 
12
  # ignore test files in root dir
13
  /*.test.py
 
42
 
43
  # Global rule to include .gitkeep files anywhere
44
  !**/.gitkeep
45
+
46
+ # for browser-use
47
  agent_history.gif
48
 
 
 
 
 
 
 
conf/model_providers.yaml CHANGED
@@ -107,6 +107,15 @@ embedding:
107
  azure:
108
  name: OpenAI Azure
109
  litellm_provider: azure
 
 
 
 
 
 
 
 
 
110
  other:
111
  name: Other OpenAI compatible
112
  litellm_provider: openai
 
107
  azure:
108
  name: OpenAI Azure
109
  litellm_provider: azure
110
+ # TODO: OpenRouter not yet supported by LiteLLM, replace with native litellm_provider openrouter and remove api_base when ready
111
+ openrouter:
112
+ name: OpenRouter
113
+ litellm_provider: openai
114
+ kwargs:
115
+ api_base: https://openrouter.ai/api/v1
116
+ extra_headers:
117
+ "HTTP-Referer": "https://agent-zero.ai/"
118
+ "X-Title": "Agent Zero"
119
  other:
120
  name: Other OpenAI compatible
121
  litellm_provider: openai
conf/projects.default.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A0 project meta folder
2
+ .a0proj/
3
+
4
+ # Python environments & cache
5
+ venv/
6
+ **/__pycache__/
7
+
8
+ # Node.js dependencies
9
+ **/node_modules/
10
+ **/.npm/
11
+
12
+ # Version control metadata
13
+ **/.git/
prompts/agent.extras.project.file_structure.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # File structure of project {{project_name}}
2
+ - this is filtered overview not full scan
3
+ - list yourself if needed
4
+ - maximum depth: {{max_depth}}
5
+ - ignored:
6
+ {{gitignore}}
7
+
8
+ ## file tree
9
+ {{file_structure}}
prompts/agent.system.projects.main.md CHANGED
@@ -1,5 +1,5 @@
1
  # Projects
2
  - user can create and activate projects
3
- - projects have work folder and instructions
4
  - when activated agent works in project follows project instructions
5
  - agent cannot manipulate or switch projects
 
1
  # Projects
2
  - user can create and activate projects
3
+ - projects have work folder in /usr/projects/<name> and instructions and config in /usr/projects/<name>/.a0proj
4
  - when activated agent works in project follows project instructions
5
  - agent cannot manipulate or switch projects
python/api/projects.py CHANGED
@@ -25,6 +25,8 @@ class Projects(ApiHandler):
25
  data = self.activate_project(ctxid, input.get("name", None))
26
  elif action == "deactivate":
27
  data = self.deactivate_project(ctxid)
 
 
28
  else:
29
  raise Exception("Invalid action")
30
 
@@ -75,4 +77,15 @@ class Projects(ApiHandler):
75
  def deactivate_project(self, context_id: str|None):
76
  if not context_id:
77
  raise Exception("Context ID is required")
78
- return projects.deactivate_project(context_id)
 
 
 
 
 
 
 
 
 
 
 
 
25
  data = self.activate_project(ctxid, input.get("name", None))
26
  elif action == "deactivate":
27
  data = self.deactivate_project(ctxid)
28
+ elif action == "file_structure":
29
+ data = self.get_file_structure(input.get("name", None), input.get("settings"))
30
  else:
31
  raise Exception("Invalid action")
32
 
 
77
  def deactivate_project(self, context_id: str|None):
78
  if not context_id:
79
  raise Exception("Context ID is required")
80
+ return projects.deactivate_project(context_id)
81
+
82
+ def get_file_structure(self, name: str|None, settings: dict|None):
83
+ if not name:
84
+ raise Exception("Project name is required")
85
+ # project data
86
+ basic_data = projects.load_basic_project_data(name)
87
+ # override file structure settings
88
+ if settings:
89
+ basic_data["file_structure"] = settings # type: ignore
90
+ # get structure
91
+ return projects.get_file_structure(name, basic_data)
python/extensions/message_loop_prompts_after/_75_include_project_extras.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/helpers/file_tree.py CHANGED
@@ -28,11 +28,11 @@ def file_tree(
28
  max_depth: int = 0,
29
  max_lines: int = 0,
30
  folders_first: bool = True,
31
- max_folders: int | None = None,
32
- max_files: int | None = None,
33
- sort: tuple[str, str] = (SORT_BY_MODIFIED, SORT_DESC),
34
  ignore: str | None = None,
35
- output_mode: str = OUTPUT_MODE_STRING,
36
  ) -> str | list[dict]:
37
  """Render a directory tree relative to the repository base path.
38
 
 
28
  max_depth: int = 0,
29
  max_lines: int = 0,
30
  folders_first: bool = True,
31
+ max_folders: int = 0,
32
+ max_files: int = 0,
33
+ sort: tuple[Literal["name", "created", "modified"], Literal["asc", "desc"]] = ("modified", "desc"),
34
  ignore: str | None = None,
35
+ output_mode: Literal["string", "flat", "nested"] = OUTPUT_MODE_STRING,
36
  ) -> str | list[dict]:
37
  """Render a directory tree relative to the repository base path.
38
 
python/helpers/projects.py CHANGED
@@ -1,7 +1,7 @@
1
  import os
2
  from typing import Literal, TypedDict, TYPE_CHECKING
3
 
4
- from python.helpers import files, dirty_json, persist_chat
5
  from python.helpers.print_style import PrintStyle
6
 
7
 
@@ -17,6 +17,15 @@ PROJECT_HEADER_FILE = "project.json"
17
  CONTEXT_DATA_KEY_PROJECT = "project"
18
 
19
 
 
 
 
 
 
 
 
 
 
20
  class BasicProjectData(TypedDict):
21
  title: str
22
  description: str
@@ -25,6 +34,7 @@ class BasicProjectData(TypedDict):
25
  memory: Literal[
26
  "own", "global"
27
  ] # in the future we can add cutom and point to another existing folder
 
28
 
29
 
30
  class EditProjectData(BasicProjectData):
@@ -73,6 +83,21 @@ def load_project_header(name: str):
73
  return header
74
 
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  def _normalizeBasicData(data: BasicProjectData):
77
  return BasicProjectData(
78
  title=data.get("title", ""),
@@ -80,6 +105,10 @@ def _normalizeBasicData(data: BasicProjectData):
80
  instructions=data.get("instructions", ""),
81
  color=data.get("color", ""),
82
  memory=data.get("memory", "own"),
 
 
 
 
83
  )
84
 
85
 
@@ -95,6 +124,10 @@ def _normalizeEditData(data: EditProjectData):
95
  knowledge_files_count=data.get("knowledge_files_count", 0),
96
  secrets=data.get("secrets", ""),
97
  memory=data.get("memory", "own"),
 
 
 
 
98
  )
99
 
100
 
@@ -324,3 +357,26 @@ def get_knowledge_files_count(name: str):
324
  get_project_meta_folder(name, PROJECT_KNOWLEDGE_DIR)
325
  )
326
  return len(files.list_files_in_dir_recursively(knowledge_folder))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  from typing import Literal, TypedDict, TYPE_CHECKING
3
 
4
+ from python.helpers import files, dirty_json, persist_chat, file_tree
5
  from python.helpers.print_style import PrintStyle
6
 
7
 
 
17
  CONTEXT_DATA_KEY_PROJECT = "project"
18
 
19
 
20
+ class FileStructureInjectionSettings(TypedDict):
21
+ enabled: bool
22
+ max_depth: int
23
+ max_files: int
24
+ max_folders: int
25
+ max_lines: int
26
+ gitignore: str
27
+
28
+
29
  class BasicProjectData(TypedDict):
30
  title: str
31
  description: str
 
34
  memory: Literal[
35
  "own", "global"
36
  ] # in the future we can add cutom and point to another existing folder
37
+ file_structure: FileStructureInjectionSettings
38
 
39
 
40
  class EditProjectData(BasicProjectData):
 
83
  return header
84
 
85
 
86
+ def _default_file_structure_settings():
87
+ try:
88
+ gitignore = files.read_file("conf/projects.default.gitignore")
89
+ except Exception:
90
+ gitignore = ""
91
+ return FileStructureInjectionSettings(
92
+ enabled=True,
93
+ max_depth=5,
94
+ max_files=20,
95
+ max_folders=20,
96
+ max_lines=250,
97
+ gitignore=gitignore,
98
+ )
99
+
100
+
101
  def _normalizeBasicData(data: BasicProjectData):
102
  return BasicProjectData(
103
  title=data.get("title", ""),
 
105
  instructions=data.get("instructions", ""),
106
  color=data.get("color", ""),
107
  memory=data.get("memory", "own"),
108
+ file_structure=data.get(
109
+ "file_structure",
110
+ _default_file_structure_settings(),
111
+ ),
112
  )
113
 
114
 
 
124
  knowledge_files_count=data.get("knowledge_files_count", 0),
125
  secrets=data.get("secrets", ""),
126
  memory=data.get("memory", "own"),
127
+ file_structure=data.get(
128
+ "file_structure",
129
+ _default_file_structure_settings(),
130
+ ),
131
  )
132
 
133
 
 
357
  get_project_meta_folder(name, PROJECT_KNOWLEDGE_DIR)
358
  )
359
  return len(files.list_files_in_dir_recursively(knowledge_folder))
360
+
361
+ def get_file_structure(name: str, basic_data: BasicProjectData|None=None) -> str:
362
+ project_folder = get_project_folder(name)
363
+ if basic_data is None:
364
+ basic_data = load_basic_project_data(name)
365
+
366
+ tree = str(file_tree.file_tree(
367
+ project_folder,
368
+ max_depth=basic_data["file_structure"]["max_depth"],
369
+ max_files=basic_data["file_structure"]["max_files"],
370
+ max_folders=basic_data["file_structure"]["max_folders"],
371
+ max_lines=basic_data["file_structure"]["max_lines"],
372
+ ignore=basic_data["file_structure"]["gitignore"],
373
+ output_mode=file_tree.OUTPUT_MODE_STRING
374
+ ))
375
+
376
+ # empty?
377
+ if "\n" not in tree:
378
+ tree += "\n # Empty"
379
+
380
+ return tree
381
+
382
+
python/helpers/strings.py CHANGED
@@ -168,6 +168,7 @@ def replace_file_includes(text: str, placeholder_pattern: str = r"§§include\((
168
  path = match.group(1)
169
  try:
170
  # read file content
 
171
  return files.read_file(path)
172
  except Exception:
173
  # if file not readable keep original placeholder
 
168
  path = match.group(1)
169
  try:
170
  # read file content
171
+ path = files.fix_dev_path(path)
172
  return files.read_file(path)
173
  except Exception:
174
  # if file not readable keep original placeholder
requirements.dev.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ pytest>=8.4.2
2
+ pytest-asyncio>=1.2.0
3
+ pytest-mock>=3.15.1
requirements2.txt CHANGED
@@ -1,2 +1,2 @@
1
- litellm==1.79.0
2
  openai==1.99.5
 
1
+ litellm==1.79.3
2
  openai==1.99.5
webui/components/chat/speech/speech-store.js CHANGED
@@ -2,6 +2,7 @@ import { createStore } from "/js/AlpineStore.js";
2
  import { updateChatInput, sendMessage } from "/index.js";
3
  import { sleep } from "/js/sleep.js";
4
  import { store as microphoneSettingStore } from "/components/settings/speech/microphone-setting-store.js";
 
5
 
6
  const Status = {
7
  INACTIVE: "inactive",
@@ -94,7 +95,9 @@ const model = {
94
  async init() {
95
  // Guard against multiple initializations
96
  if (this._initialized) {
97
- console.log('[Speech Store] Already initialized, skipping duplicate init()');
 
 
98
  return;
99
  }
100
 
@@ -368,11 +371,13 @@ const model = {
368
 
369
  // Show a prompt to user to enable audio
370
  showAudioPermissionPrompt() {
371
- if (window.toast) {
372
- window.toast("Click anywhere to enable audio playback", "info", 5000);
373
- } else {
374
- console.log("Please click anywhere on the page to enable audio playback");
375
- }
 
 
376
  },
377
 
378
  // Browser TTS
 
2
  import { updateChatInput, sendMessage } from "/index.js";
3
  import { sleep } from "/js/sleep.js";
4
  import { store as microphoneSettingStore } from "/components/settings/speech/microphone-setting-store.js";
5
+ import * as shortcuts from "/js/shortcuts.js";
6
 
7
  const Status = {
8
  INACTIVE: "inactive",
 
95
  async init() {
96
  // Guard against multiple initializations
97
  if (this._initialized) {
98
+ console.log(
99
+ "[Speech Store] Already initialized, skipping duplicate init()"
100
+ );
101
  return;
102
  }
103
 
 
371
 
372
  // Show a prompt to user to enable audio
373
  showAudioPermissionPrompt() {
374
+ shortcuts.frontendNotification({
375
+ type: "info",
376
+ message: "Click anywhere to enable audio playback",
377
+ displayTime: 5000,
378
+ frontendOnly: true,
379
+ });
380
+ console.log("Please click anywhere on the page to enable audio playback");
381
  },
382
 
383
  // Browser TTS
webui/components/notifications/notification-toast-stack.html CHANGED
@@ -82,6 +82,8 @@
82
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
83
  position: relative;
84
  min-height: 60px;
 
 
85
  }
86
 
87
  .toast-item:hover {
 
82
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
83
  position: relative;
84
  min-height: 60px;
85
+ max-height: 15em;
86
+ overflow-y: auto;
87
  }
88
 
89
  .toast-item:hover {
webui/components/projects/project-edit-file-structure.html ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <html>
2
+
3
+ <head>
4
+ <title>Create a new project</title>
5
+ <script type="module">
6
+ import { store } from "/components/projects/projects-store.js";
7
+ </script>
8
+ </head>
9
+
10
+ <body>
11
+ <div x-data>
12
+ <template x-if="$store.projects && $store.projects.selectedProject">
13
+ <div>
14
+ <!-- <div class="project-detail-header">
15
+ <div class="projects-project-card-title">Project details</div>
16
+ <div class="project-path" x-text="$store.projects.selectedProject.path"></div>
17
+ </div> -->
18
+
19
+
20
+ <div class="projects-setting-row" style="padding: 1rem 0;">
21
+ <div class="projects-setting-text">
22
+ <label class="projects-form-label">Inject project structure into context window</label>
23
+ <span class="projects-form-description">When turned on, the project file structure will be
24
+ injected into the context window of the agent. This is useful for agents that need to
25
+ have an overview of project folders and files at all times.</span>
26
+ </div>
27
+ <div class="projects-setting-control">
28
+ <label class="toggle">
29
+ <input type="checkbox" x-model="$store.projects.selectedProject.file_structure.enabled">
30
+ <span class="toggler"></span>
31
+ </label>
32
+ </div>
33
+ </div>
34
+
35
+ <div x-show="$store.projects.selectedProject.file_structure.enabled">
36
+
37
+
38
+ <div class="projects-form-group">
39
+ <label class="projects-form-label">Max Depth</label>
40
+ <span class="projects-form-description">Set the maximum depth for the file structure (0 =
41
+ unlimited).</span>
42
+ <input type="number" class="projects-form-input"
43
+ x-model.number="$store.projects.selectedProject.file_structure.max_depth" min="0" step="1"
44
+ placeholder="0">
45
+ </div>
46
+
47
+ <div class="projects-form-group">
48
+ <label class="projects-form-label">Max Lines</label>
49
+ <span class="projects-form-description">Maximum total lines outputted for the agent. This limits
50
+ the
51
+ space occupied in the context window (0 = unlimited).</span>
52
+ <input type="number" class="projects-form-input"
53
+ x-model.number="$store.projects.selectedProject.file_structure.max_lines" min="0" step="1"
54
+ placeholder="0">
55
+ </div>
56
+
57
+ <div class="projects-form-group">
58
+ <label class="projects-form-label">Max Folders</label>
59
+ <span class="projects-form-description">Maximum number of subfolders to display under one folder
60
+ (0 =
61
+ unlimited).</span>
62
+ <input type="number" class="projects-form-input"
63
+ x-model.number="$store.projects.selectedProject.file_structure.max_folders" min="0" step="1"
64
+ placeholder="0">
65
+ </div>
66
+
67
+ <div class="projects-form-group">
68
+ <label class="projects-form-label">Max Files</label>
69
+ <span class="projects-form-description">Maximum number of files to display under one folder (0 =
70
+ unlimited).</span>
71
+ <input type="number" class="projects-form-input"
72
+ x-model.number="$store.projects.selectedProject.file_structure.max_files" min="0" step="1"
73
+ placeholder="0">
74
+ </div>
75
+
76
+ <div class="projects-form-group">
77
+ <label class="projects-form-label">Ignored files / folders</label>
78
+ <span class="projects-form-description">Specify patterns in gitignore format. These files and
79
+ folders will be skipped during analysis, such as meta folders, cache directories, packages,
80
+ and
81
+ build artifacts.</span>
82
+ <textarea class="projects-form-textarea"
83
+ x-model="$store.projects.selectedProject.file_structure.gitignore" rows="5"
84
+ placeholder="Enter gitignore patterns (e.g., node_modules/, .git/, *.log)"></textarea>
85
+ </div>
86
+
87
+ <button class="button icon-button" x-on:click="$store.projects.testFileStructure()">
88
+ <span class="icon material-symbols-outlined">account_tree</span>
89
+ <span>Test output</span>
90
+ </button>
91
+
92
+
93
+ </div>
94
+
95
+ </div>
96
+ </template>
97
+ </div>
98
+ </body>
99
+ <style>
100
+ </style>
101
+
102
+ </html>
webui/components/projects/project-edit.html CHANGED
@@ -50,6 +50,14 @@
50
  </x-component>
51
  </div>
52
 
 
 
 
 
 
 
 
 
53
  <div class="project-detail">
54
  <div class="project-detail-header">
55
  <span class="projects-project-card-title">Secrets</span>
 
50
  </x-component>
51
  </div>
52
 
53
+ <div class="project-detail">
54
+ <div class="project-detail-header">
55
+ <span class="projects-project-card-title">File structure</span>
56
+ </div>
57
+ <x-component path="projects/project-edit-file-structure.html">
58
+ </x-component>
59
+ </div>
60
+
61
  <div class="project-detail">
62
  <div class="project-detail-header">
63
  <span class="projects-project-card-title">Secrets</span>
webui/components/projects/project-file-structure-test.html ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <html>
2
+
3
+ <head>
4
+ <title>File structure test</title>
5
+ <script type="module">
6
+ import { store } from "/components/projects/projects-store.js";
7
+ </script>
8
+ </head>
9
+
10
+ <body>
11
+ <div x-data>
12
+ <template x-if="$store.projects && $store.projects.selectedProject">
13
+ <div>
14
+
15
+ <h3>File structure test - <span x-text="$store.projects.selectedProject.name"></span></h3>
16
+ <pre x-text="$store.projects.fileStructureTestOutput"></pre>
17
+
18
+ </div>
19
+ </template>
20
+ </div>
21
+ </body>
22
+ <style>
23
+ </style>
24
+
25
+ </html>
webui/components/projects/project-selector.html CHANGED
@@ -26,7 +26,8 @@
26
  </template>
27
 
28
  <div x-show="open" x-init="$store.projects.loadProjectsList()" class="project-dropdown-menu" style="display: none;" x-transition>
29
- <a href="#" @click.prevent="$store.projects.openProjectsModal(); open = false;" class="project-dropdown-item"><span class="icon material-symbols-outlined">snippet_folder</span> <span class="project-selector-item-text">Edit Projects</span></a>
 
30
  <a x-show="$store.chats.selectedContext.project?.name" href="#" @click.prevent="$store.projects.deactivateProject(); open = false;" class="project-dropdown-item"><span class="icon material-symbols-outlined">close</span> <span class="project-selector-item-text">Deactivate</span></a>
31
  <!-- <a href="#" @click.prevent="$store.projects.openProjectsModal(); $store.projects.openCreateModal(); open = false;" class="project-dropdown-item">Create Project</a> -->
32
 
 
26
  </template>
27
 
28
  <div x-show="open" x-init="$store.projects.loadProjectsList()" class="project-dropdown-menu" style="display: none;" x-transition>
29
+ <a href="#" @click.prevent="$store.projects.openProjectsModal(); open = false;" class="project-dropdown-item"><span class="icon material-symbols-outlined">snippet_folder</span> <span class="project-selector-item-text">Projects</span></a>
30
+ <a x-show="$store.chats.selectedContext.project?.name" href="#" @click.prevent="$store.projects.editActiveProject(); open = false;" class="project-dropdown-item"><span class="icon material-symbols-outlined">edit</span> <span class="project-selector-item-text">Edit <span x-text="$store.chats.selectedContext.project?.title"></span></span></a>
31
  <a x-show="$store.chats.selectedContext.project?.name" href="#" @click.prevent="$store.projects.deactivateProject(); open = false;" class="project-dropdown-item"><span class="icon material-symbols-outlined">close</span> <span class="project-selector-item-text">Deactivate</span></a>
32
  <!-- <a href="#" @click.prevent="$store.projects.openProjectsModal(); $store.projects.openCreateModal(); open = false;" class="project-dropdown-item">Create Project</a> -->
33
 
webui/components/projects/projects-store.js CHANGED
@@ -378,6 +378,33 @@ const model = {
378
  .join("/")
379
  .replace(/\/+/g, "/");
380
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  };
382
 
383
  // convert it to alpine store
 
378
  .join("/")
379
  .replace(/\/+/g, "/");
380
  },
381
+
382
+ async editActiveProject() {
383
+ const ctx = shortcuts.getCurrentContext();
384
+ if(!ctx) return;
385
+ this.openEditModal(ctx.project.name);
386
+ },
387
+
388
+ async testFileStructure() {
389
+ try {
390
+ const response = await api.callJsonApi("projects", {
391
+ action: "file_structure",
392
+ name: this.selectedProject.name,
393
+ settings: this.selectedProject.file_structure,
394
+ });
395
+ this.fileStructureTestOutput = response.data;
396
+ shortcuts.openModal("projects/project-file-structure-test.html");
397
+ } catch (error) {
398
+ console.error("Error testing file structure:", error);
399
+ shortcuts.frontendNotification({
400
+ type: shortcuts.NotificationType.ERROR,
401
+ message: "Error testing file structure",
402
+ priority: shortcuts.NotificationPriority.NORMAL,
403
+ displayTime: 3,
404
+ frontendOnly: true,
405
+ });
406
+ }
407
+ },
408
  };
409
 
410
  // convert it to alpine store
webui/components/sidebar/chats/chats-store.js CHANGED
@@ -23,6 +23,10 @@ const model = {
23
  return this.selected;
24
  },
25
 
 
 
 
 
26
  init() {
27
  // Initialize from localStorage
28
  const lastSelectedChat = localStorage.getItem("lastSelectedChat");
 
23
  return this.selected;
24
  },
25
 
26
+ getSelectedContext(){
27
+ return this.selectedContext;
28
+ },
29
+
30
  init() {
31
  // Initialize from localStorage
32
  const lastSelectedChat = localStorage.getItem("lastSelectedChat");
webui/js/shortcuts.js CHANGED
@@ -1,9 +1,10 @@
1
  import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
2
  import { callJsonApi } from "/js/api.js";
 
3
  import {
4
  NotificationType,
5
  NotificationPriority,
6
- store as notificationStore
7
  } from "/components/notifications/notification-store.js";
8
 
9
  // shortcuts utils for convenience
@@ -12,13 +13,24 @@ import {
12
  export { callJsonApi };
13
 
14
  // notifications
15
- export {
16
- NotificationType,
17
- NotificationPriority,
18
- }
19
- export const frontendNotification = notificationStore.frontendNotification.bind(notificationStore);
20
 
21
  // chat context
22
  export function getCurrentContextId() {
23
  return chatsStore.getSelectedChatId();
24
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
2
  import { callJsonApi } from "/js/api.js";
3
+ import * as modals from "/js/modals.js";
4
  import {
5
  NotificationType,
6
  NotificationPriority,
7
+ store as notificationStore,
8
  } from "/components/notifications/notification-store.js";
9
 
10
  // shortcuts utils for convenience
 
13
  export { callJsonApi };
14
 
15
  // notifications
16
+ export { NotificationType, NotificationPriority };
17
+ export const frontendNotification =
18
+ notificationStore.frontendNotification.bind(notificationStore);
 
 
19
 
20
  // chat context
21
  export function getCurrentContextId() {
22
  return chatsStore.getSelectedChatId();
23
  }
24
+
25
+ export function getCurrentContext(){
26
+ return chatsStore.getSelectedContext();
27
+ }
28
+
29
+ // modals
30
+ export function openModal(modalPath) {
31
+ return modals.openModal(modalPath);
32
+ }
33
+
34
+ export function closeModal(modalPath = null) {
35
+ return modals.closeModal(modalPath);
36
+ }