Spaces:
Paused
Paused
frdel commited on
Commit ·
1f528d5
1
Parent(s): c3bf03e
project finalizing, openrouter embeddings
Browse files- .gitignore +3 -8
- conf/model_providers.yaml +9 -0
- conf/projects.default.gitignore +13 -0
- prompts/agent.extras.project.file_structure.md +9 -0
- prompts/agent.system.projects.main.md +1 -1
- python/api/projects.py +14 -1
- python/extensions/message_loop_prompts_after/_75_include_project_extras.py +47 -0
- python/helpers/file_tree.py +4 -4
- python/helpers/projects.py +57 -1
- python/helpers/strings.py +1 -0
- requirements.dev.txt +3 -0
- requirements2.txt +1 -1
- webui/components/chat/speech/speech-store.js +11 -6
- webui/components/notifications/notification-toast-stack.html +2 -0
- webui/components/projects/project-edit-file-structure.html +102 -0
- webui/components/projects/project-edit.html +8 -0
- webui/components/projects/project-file-structure-test.html +25 -0
- webui/components/projects/project-selector.html +2 -1
- webui/components/projects/projects-store.js +27 -0
- webui/components/sidebar/chats/chats-store.js +4 -0
- webui/js/shortcuts.js +18 -6
.gitignore
CHANGED
|
@@ -5,10 +5,9 @@
|
|
| 5 |
*.py[cod]
|
| 6 |
**/.conda/
|
| 7 |
|
| 8 |
-
#Ignore
|
| 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
|
| 32 |
-
max_files: int
|
| 33 |
-
sort: tuple[
|
| 34 |
ignore: str | None = None,
|
| 35 |
-
output_mode:
|
| 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.
|
| 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(
|
|
|
|
|
|
|
| 98 |
return;
|
| 99 |
}
|
| 100 |
|
|
@@ -368,11 +371,13 @@ const model = {
|
|
| 368 |
|
| 369 |
// Show a prompt to user to enable audio
|
| 370 |
showAudioPermissionPrompt() {
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 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">
|
|
|
|
| 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 |
-
|
| 17 |
-
|
| 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 |
+
}
|