frontend file browsers, css colors, litellm update, reqs split

frdel committed Nov 9, 2025 at 20:56 UTC 70814791932bf47548402a3b1798e02167b612d7
23 files changed +532 -69
docker/run/fs/ins/install_A0.sh
+2
@@ -36,6 +36,8 @@ fi
36
37 # Install remaining A0 python packages
38 uv pip install -r /git/agent-zero/requirements.txt
39 +# override for packages that have unnecessarily strict dependencies
40 +uv pip install -r /git/agent-zero/requirements2.txt
41
42 # install playwright
43 bash /ins/install_playwright.sh "$@"
python/api/chat_files_path_get.py new
+23
@@ -0,0 +1,23 @@
1 +from python.helpers.api import ApiHandler, Request, Response
2 +from python.helpers import files, memory, notification, projects, notification, runtime
3 +import os
4 +from werkzeug.utils import secure_filename
5 +
6 +
7 +class GetChatFilesPath(ApiHandler):
8 + async def process(self, input: dict, request: Request) -> dict | Response:
9 + ctxid = input.get("ctxid", "")
10 + if not ctxid:
11 + raise Exception("No context id provided")
12 + context = self.use_context(ctxid)
13 +
14 + project_name = projects.get_context_project_name(context)
15 + if project_name:
16 + folder = files.normalize_a0_path(projects.get_project_folder(project_name))
17 + else:
18 + folder = "/root" # root in container
19 +
20 + return {
21 + "ok": True,
22 + "path": folder,
23 + }
\ No newline at end of file
python/api/knowledge_path_get.py new
+25
@@ -0,0 +1,25 @@
1 +from python.helpers.api import ApiHandler, Request, Response
2 +from python.helpers import files, memory, notification, projects, notification
3 +import os
4 +from werkzeug.utils import secure_filename
5 +
6 +
7 +class GetKnowledgePath(ApiHandler):
8 + async def process(self, input: dict, request: Request) -> dict | Response:
9 + ctxid = input.get("ctxid", "")
10 + if not ctxid:
11 + raise Exception("No context id provided")
12 + context = self.use_context(ctxid)
13 +
14 + project_name = projects.get_context_project_name(context)
15 + if project_name:
16 + knowledge_folder = projects.get_project_meta_folder(project_name, "knowledge")
17 + else:
18 + knowledge_folder = memory.get_custom_knowledge_subdir_abs(context.agent0)
19 +
20 + knowledge_folder = files.normalize_a0_path(knowledge_folder)
21 +
22 + return {
23 + "ok": True,
24 + "path": knowledge_folder,
25 + }
\ No newline at end of file
python/api/knowledge_reindex.py new
+21
@@ -0,0 +1,21 @@
1 +from python.helpers.api import ApiHandler, Request, Response
2 +from python.helpers import files, memory, notification, projects, notification
3 +import os
4 +from werkzeug.utils import secure_filename
5 +
6 +
7 +class ReindexKnowledge(ApiHandler):
8 + async def process(self, input: dict, request: Request) -> dict | Response:
9 + ctxid = input.get("ctxid", "")
10 + if not ctxid:
11 + raise Exception("No context id provided")
12 + context = self.use_context(ctxid)
13 +
14 + # reload memory to re-import knowledge
15 + await memory.Memory.reload(context.agent0)
16 + context.log.set_initial_progress()
17 +
18 + return {
19 + "ok": True,
20 + "message": "Knowledge re-indexed",
21 + }
python/api/poll.py
+4 -1
@@ -20,7 +20,10 @@ class Poll(ApiHandler):
20
21 # context instance - get or create only if ctxid is provided
22 if ctxid:
23 - context = self.use_context(ctxid, create_if_not_exists=False)
23 + try:
24 + context = self.use_context(ctxid, create_if_not_exists=False)
25 + except Exception as e:
26 + context = None
27 else:
28 context = None
29
python/helpers/api.py
+3
@@ -95,3 +95,6 @@ class ApiHandler:
95 if create_if_not_exists:
96 context = AgentContext(config=initialize_agent(), id=ctxid, set_current=True)
97 return context
98 + else:
99 + raise Exception(f"Context {ctxid} not found")
100 +
python/helpers/files.py
+14
@@ -530,3 +530,17 @@ def read_text_files_in_dir(
530 except Exception:
531 continue
532 return result
533 +
534 +def list_files_in_dir_recursively(relative_path: str) -> list[str]:
535 + abs_path = get_abs_path(relative_path)
536 + if not os.path.exists(abs_path):
537 + return []
538 + result = []
539 + for root, dirs, files in os.walk(abs_path):
540 + for file in files:
541 + file_path = os.path.join(root, file)
542 + # Return relative path from the base directory
543 + rel_path = os.path.relpath(file_path, abs_path)
544 + result.append(rel_path)
545 + return result
546 +
\ No newline at end of file
python/helpers/knowledge_import.py
+2 -1
@@ -36,6 +36,7 @@ def load_knowledge(
36 index: Dict[str, KnowledgeImport],
37 metadata: dict[str, Any] = {},
38 filename_pattern: str = "**/*",
39 + recursive: bool = True,
40 ) -> Dict[str, KnowledgeImport]:
41 """
42 Load knowledge files from a directory with change detection and metadata enhancement.
@@ -96,7 +97,7 @@ def load_knowledge(
97
98 # Fetch all files in the directory with specified extensions
99 try:
99 - kn_files = glob.glob(os.path.join(knowledge_dir, filename_pattern), recursive=True)
100 + kn_files = glob.glob(os.path.join(knowledge_dir, filename_pattern), recursive=recursive)
101 kn_files = [f for f in kn_files if os.path.isfile(f) and not os.path.basename(f).startswith('.')]
102 except Exception as e:
103 PrintStyle(font_color="red").print(f"Error scanning knowledge directory {knowledge_dir}: {e}")
python/helpers/memory.py
+62 -14
@@ -77,10 +77,11 @@ class Memory:
77 )
78 Memory.index[memory_subdir] = db
79 wrap = Memory(db, memory_subdir=memory_subdir)
80 - if agent.config.knowledge_subdirs:
81 - await wrap.preload_knowledge(
82 - log_item, agent.config.knowledge_subdirs, memory_subdir
83 - )
80 + knowledge_subdirs = get_knowledge_subdirs_by_memory_subdir(
81 + memory_subdir, agent.config.knowledge_subdirs or []
82 + )
83 + if knowledge_subdirs:
84 + await wrap.preload_knowledge(log_item, knowledge_subdirs, memory_subdir)
85 return wrap
86 else:
87 return Memory(
@@ -106,16 +107,20 @@ class Memory:
107 in_memory=False,
108 )
109 wrap = Memory(db, memory_subdir=memory_subdir)
109 - if preload_knowledge and agent_config.knowledge_subdirs:
110 - await wrap.preload_knowledge(
111 - log_item, agent_config.knowledge_subdirs, memory_subdir
110 + if preload_knowledge:
111 + knowledge_subdirs = get_knowledge_subdirs_by_memory_subdir(
112 + memory_subdir, agent_config.knowledge_subdirs or []
113 )
114 + if knowledge_subdirs:
115 + await wrap.preload_knowledge(
116 + log_item, knowledge_subdirs, memory_subdir
117 + )
118 Memory.index[memory_subdir] = db
119 return Memory(db=Memory.index[memory_subdir], memory_subdir=memory_subdir)
120
121 @staticmethod
122 async def reload(agent: Agent):
118 - memory_subdir = agent.config.memory_subdir or "default"
123 + memory_subdir = get_agent_memory_subdir(agent)
124 if Memory.index.get(memory_subdir):
125 del Memory.index[memory_subdir]
126 return await Memory.get(agent)
@@ -298,12 +303,24 @@ class Memory:
303 ):
304 # load knowledge folders, subfolders by area
305 for kn_dir in kn_dirs:
306 + # everything in the root of the knowledge goes to main
307 + index = knowledge_import.load_knowledge(
308 + log_item,
309 + abs_knowledge_dir(kn_dir),
310 + index,
311 + {"area": Memory.Area.MAIN},
312 + filename_pattern="*",
313 + recursive=False,
314 + )
315 + # subdirectories go to their folders
316 for area in Memory.Area:
317 index = knowledge_import.load_knowledge(
318 log_item,
304 - files.get_abs_path("knowledge", kn_dir, area.value),
319 + # files.get_abs_path("knowledge", kn_dir, area.value),
320 + abs_knowledge_dir(kn_dir, area.value),
321 index,
322 {"area": area.value},
323 + recursive=True,
324 )
325
326 # load instruments descriptions
@@ -313,6 +330,7 @@ class Memory:
330 index,
331 {"area": Memory.Area.INSTRUMENTS.value},
332 filename_pattern="**/*.md",
333 + recursive=True,
334 )
335
336 return index
@@ -484,6 +502,18 @@ def abs_db_dir(memory_subdir: str) -> str:
502 return files.get_abs_path("memory", memory_subdir)
503
504
505 +def abs_knowledge_dir(knowledge_subdir: str, *sub_dirs: str) -> str:
506 + # patch for projects, this way we don't need to re-work the structure of knowledge subdirs
507 + if knowledge_subdir.startswith("projects/"):
508 + from python.helpers.projects import get_project_meta_folder
509 +
510 + return files.get_abs_path(
511 + get_project_meta_folder(knowledge_subdir[9:]), "knowledge", *sub_dirs
512 + )
513 + # standard subdirs
514 + return files.get_abs_path("knowledge", knowledge_subdir, *sub_dirs)
515 +
516 +
517 def get_memory_subdir_abs(agent: Agent) -> str:
518 subdir = get_agent_memory_subdir(agent)
519 return abs_db_dir(subdir)
@@ -496,7 +526,9 @@ def get_agent_memory_subdir(agent: Agent) -> str:
526
527 def get_context_memory_subdir(context: AgentContext) -> str:
528 # if project is active, use project memory subdir
499 - from python.helpers.projects import get_context_memory_subdir as get_project_memory_subdir
529 + from python.helpers.projects import (
530 + get_context_memory_subdir as get_project_memory_subdir,
531 + )
532
533 memory_subdir = get_project_memory_subdir(context)
534 if memory_subdir:
@@ -505,23 +537,39 @@ def get_context_memory_subdir(context: AgentContext) -> str:
537 # no project, regular memory subdir
538 return context.config.memory_subdir or "default"
539
540 +
541 def get_existing_memory_subdirs() -> list[str]:
542 try:
510 - from python.helpers.projects import get_project_meta_folder, get_projects_parent_folder
543 + from python.helpers.projects import (
544 + get_project_meta_folder,
545 + get_projects_parent_folder,
546 + )
547 +
548 # Get subdirectories from memory folder
549 subdirs = files.get_subdirectories("memory", exclude="embeddings")
550
551 project_subdirs = files.get_subdirectories(get_projects_parent_folder())
552 for project_subdir in project_subdirs:
516 - if files.exists(get_project_meta_folder(project_subdir), "memory", "index.faiss"):
553 + if files.exists(
554 + get_project_meta_folder(project_subdir), "memory", "index.faiss"
555 + ):
556 subdirs.append(f"projects/{project_subdir}")
557
558 # Ensure 'default' is always available
559 if "default" not in subdirs:
560 subdirs.insert(0, "default")
522 -
561 +
562 return subdirs
563 except Exception as e:
564 PrintStyle.error(f"Failed to get memory subdirectories: {str(e)}")
565 return ["default"]
527 -
\ No newline at end of file
566 +
567 +
568 +def get_knowledge_subdirs_by_memory_subdir(
569 + memory_subdir: str, default: list[str]
570 +) -> list[str]:
571 + if memory_subdir.startswith("projects/"):
572 + from python.helpers.projects import get_project_meta_folder
573 +
574 + default.append(get_project_meta_folder(memory_subdir[9:], "knowledge"))
575 + return default
python/helpers/playwright.py
+10 -3
@@ -1,4 +1,6 @@
1
2 +import os
3 +import sys
4 from pathlib import Path
5 import subprocess
6 from python.helpers import files
@@ -9,8 +11,14 @@ from python.helpers import files
11
12 def get_playwright_binary():
13 pw_cache = Path(get_playwright_cache_dir())
12 - headless_shell = next(pw_cache.glob("chromium_headless_shell-*/chrome-*/headless_shell"), None)
13 - return headless_shell
14 + for pattern in (
15 + "chromium_headless_shell-*/chrome-*/headless_shell",
16 + "chromium_headless_shell-*/chrome-*/headless_shell.exe",
17 + ):
18 + binary = next(pw_cache.glob(pattern), None)
19 + if binary:
20 + return binary
21 + return None
22
23 def get_playwright_cache_dir():
24 return files.get_abs_path("tmp/playwright")
@@ -19,7 +27,6 @@ def ensure_playwright_binary():
27 bin = get_playwright_binary()
28 if not bin:
29 cache = get_playwright_cache_dir()
22 - import os
30 env = os.environ.copy()
31 env["PLAYWRIGHT_BROWSERS_PATH"] = cache
32 subprocess.check_call(
python/helpers/projects.py
+51 -17
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
11 PROJECTS_PARENT_DIR = "usr/projects"
12 PROJECT_META_DIR = ".a0proj"
13 PROJECT_INSTRUCTIONS_DIR = "instructions"
14 +PROJECT_KNOWLEDGE_DIR = "knowledge"
15 PROJECT_HEADER_FILE = "project.json"
16
17 CONTEXT_DATA_KEY_PROJECT = "project"
@@ -21,13 +22,15 @@ class BasicProjectData(TypedDict):
22 description: str
23 instructions: str
24 color: str
24 - memory: Literal["own", "global"] # in the future we can add cutom and point to another existing folder
25 -
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):
31 name: str
32 instruction_files_count: int
33 + knowledge_files_count: int
34 variables: str
35 secrets: str
36
@@ -39,8 +42,9 @@ def get_projects_parent_folder():
42 def get_project_folder(name: str):
43 return files.get_abs_path(get_projects_parent_folder(), name)
44
42 -def get_project_meta_folder(name: str):
43 - return files.get_abs_path(get_project_folder(name), PROJECT_META_DIR)
45 +
46 +def get_project_meta_folder(name: str, *sub_dirs: str):
47 + return files.get_abs_path(get_project_folder(name), PROJECT_META_DIR, *sub_dirs)
48
49
50 def delete_project(name: str):
@@ -54,9 +58,7 @@ def create_project(name: str, data: BasicProjectData):
58 abs_path = files.create_dir_safe(
59 files.get_abs_path(PROJECTS_PARENT_DIR, name), rename_format="{name}_{number}"
60 )
57 - files.create_dir(
58 - files.get_abs_path(abs_path, PROJECT_META_DIR, PROJECT_INSTRUCTIONS_DIR)
59 - )
61 + create_project_meta_folders(name)
62 data = _normalizeBasicData(data)
63 save_project_header(name, data)
64 return name
@@ -80,6 +82,7 @@ def _normalizeBasicData(data: BasicProjectData):
82 memory=data.get("memory", "own"),
83 )
84
85 +
86 def _normalizeEditData(data: EditProjectData):
87 return EditProjectData(
88 name=data.get("name", ""),
@@ -89,16 +92,19 @@ def _normalizeEditData(data: EditProjectData):
92 variables=data.get("variables", ""),
93 color=data.get("color", ""),
94 instruction_files_count=data.get("instruction_files_count", 0),
95 + knowledge_files_count=data.get("knowledge_files_count", 0),
96 secrets=data.get("secrets", ""),
97 memory=data.get("memory", "own"),
98 )
99
100 +
101 def _edit_data_to_basic_data(data: EditProjectData):
102 return _normalizeBasicData(data)
103
104 +
105 def _basic_data_to_edit_data(data: BasicProjectData):
100 - return _normalizeEditData(data) # type: ignore
101 -
106 + return _normalizeEditData(data) # type: ignore
107 +
108
109 def update_project(name: str, data: EditProjectData):
110 # merge with current state
@@ -117,6 +123,7 @@ def update_project(name: str, data: EditProjectData):
123 reactivate_project_in_chats(name)
124 return name
125
126 +
127 def load_basic_project_data(name: str) -> BasicProjectData:
128 data = BasicProjectData(**load_project_header(name))
129 normalized = _normalizeBasicData(data)
@@ -130,10 +137,12 @@ def load_edit_project_data(name: str) -> EditProjectData:
137 ) # for additional info
138 variables = load_project_variables(name)
139 secrets = load_project_secrets_masked(name)
140 + knowledge_files_count = get_knowledge_files_count(name)
141 data = EditProjectData(
142 **data,
143 name=name,
144 instruction_files_count=len(additional_instructions),
145 + knowledge_files_count=knowledge_files_count,
146 variables=variables,
147 secrets=secrets,
148 )
@@ -254,34 +263,38 @@ def get_additional_instructions_files(name: str):
263 )
264 return files.read_text_files_in_dir(instructions_folder)
265
266 +
267 def get_context_project_name(context: "AgentContext") -> str | None:
268 return context.get_data(CONTEXT_DATA_KEY_PROJECT)
269
270 +
271 def load_project_variables(name: str):
272 try:
262 - abs_path = files.get_abs_path(
263 - get_project_meta_folder(name), "variables.env"
264 - )
273 + abs_path = files.get_abs_path(get_project_meta_folder(name), "variables.env")
274 return files.read_file(abs_path)
275 except Exception:
276 return ""
277
278 +
279 def save_project_variables(name: str, variables: str):
270 - abs_path = files.get_abs_path(
271 - get_project_meta_folder(name), "variables.env"
272 - )
280 + abs_path = files.get_abs_path(get_project_meta_folder(name), "variables.env")
281 files.write_file(abs_path, variables)
282
275 -def load_project_secrets_masked(name:str, merge_with_global=False):
283 +
284 +def load_project_secrets_masked(name: str, merge_with_global=False):
285 from python.helpers import secrets
286 +
287 mgr = secrets.get_project_secrets_manager(name, merge_with_global)
288 return mgr.get_masked_secrets()
289
290 +
291 def save_project_secrets(name: str, secrets: str):
292 from python.helpers.secrets import get_project_secrets_manager
293 +
294 secrets_manager = get_project_secrets_manager(name)
295 secrets_manager.save_secrets_with_merge(secrets)
296
297 +
298 def get_context_memory_subdir(context: "AgentContext") -> str | None:
299 # if a project is active and has memory isolation set, return the project memory subdir
300 project_name = get_context_project_name(context)
@@ -289,4 +302,25 @@ def get_context_memory_subdir(context: "AgentContext") -> str | None:
302 project_data = load_basic_project_data(project_name)
303 if project_data["memory"] == "own":
304 return "projects/" + project_name
292 - return None # no memory override
\ No newline at end of file
305 + return None # no memory override
306 +
307 +
308 +def create_project_meta_folders(name: str):
309 + # create instructions folder
310 + files.create_dir(get_project_meta_folder(name, PROJECT_INSTRUCTIONS_DIR))
311 +
312 + # create knowledge folders
313 + files.create_dir(get_project_meta_folder(name, PROJECT_KNOWLEDGE_DIR))
314 + from python.helpers import memory
315 +
316 + for memory_type in memory.Memory.Area:
317 + files.create_dir(
318 + get_project_meta_folder(name, PROJECT_KNOWLEDGE_DIR, memory_type.value)
319 + )
320 +
321 +
322 +def get_knowledge_files_count(name: str):
323 + knowledge_folder = files.get_abs_path(
324 + get_project_meta_folder(name, PROJECT_KNOWLEDGE_DIR)
325 + )
326 + return len(files.list_files_in_dir_recursively(knowledge_folder))
requirements.txt
-1
@@ -33,7 +33,6 @@ unstructured-client==0.31.0
33 webcolors==24.6.0
34 nest-asyncio==1.6.0
35 crontab==1.0.1
36 -litellm==1.75.0
36 markdownify==1.1.0
37 pymupdf==1.25.3
38 pytesseract==0.3.13
requirements2.txt new
+2
@@ -0,0 +1,2 @@
1 +litellm==1.79.0
2 +openai==1.99.5
\ No newline at end of file
webui/components/chat/input/bottom-actions.html
+1 -2
@@ -4,7 +4,6 @@
4 import { store } from "/components/chat/input/input-store.js";
5 import { store as historyStore } from "/components/modals/history/history-store.js";
6 import { store as contextStore } from "/components/modals/context/context-store.js";
7 - import { store as fileBrowserStore } from "/components/modals/file-browser/file-browser-store.js";
7 </script>
8 </head>
9 <body>
@@ -32,7 +31,7 @@
31 <p>Import knowledge</p>
32 </button>
33
35 - <button class="text-button" id="work_dir_browser" @click="$store.fileBrowser.open()">
34 + <button class="text-button" id="work_dir_browser" @click="$store.chatInput.browseFiles()">
35 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 123.37 92.59">
36 <path d="m5.72,11.5l-3.93,8.73h119.77s-3.96-8.73-3.96-8.73h-60.03c-1.59,0-2.88-1.29-2.88-2.88V1.75H13.72v6.87c0,1.59-1.29,2.88-2.88,2.88h-5.12Z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="7"></path>
37 <path d="m6.38,20.23H1.75l7.03,67.03c.11,1.07.55,2.02,1.2,2.69.55.55,1.28.89,2.11.89h97.1c.82,0,1.51-.33,2.05-.87.68-.68,1.13-1.67,1.28-2.79l9.1-66.94H6.38Z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="8"></path>
webui/components/chat/input/input-store.js
+69 -3
@@ -1,4 +1,6 @@
1 import { createStore } from "/js/AlpineStore.js";
2 +import * as shortcuts from "/js/shortcuts.js";
3 +import { store as fileBrowserStore } from "/components/modals/file-browser/file-browser-store.js";
4
5 const model = {
6 paused: false,
@@ -28,7 +30,8 @@ const model = {
30 this.paused = paused;
31 try {
32 const context = globalThis.getContext?.();
31 - if (!globalThis.sendJsonData) throw new Error("sendJsonData not available");
33 + if (!globalThis.sendJsonData)
34 + throw new Error("sendJsonData not available");
35 await globalThis.sendJsonData("/pause", { paused, context });
36 } catch (e) {
37 this.paused = prev;
@@ -50,6 +53,55 @@ const model = {
53 },
54
55 async loadKnowledge() {
56 + try {
57 + const resp = await shortcuts.callJsonApi("/knowledge_path_get", {
58 + ctxid: shortcuts.getCurrentContextId(),
59 + });
60 + if (!resp.ok) throw new Error("Error getting knowledge path");
61 + const path = resp.path;
62 +
63 + // open file browser and wait for it to close
64 + await fileBrowserStore.open(path);
65 +
66 + // progress notification
67 + shortcuts.frontendNotification({
68 + type: shortcuts.NotificationType.PROGRESS,
69 + message: "Loading knowledge...",
70 + priority: shortcuts.NotificationPriority.NORMAL,
71 + displayTime: 999,
72 + group: "knowledge_load",
73 + frontendOnly: true,
74 + });
75 +
76 + // then reindex knowledge
77 + await globalThis.sendJsonData("/knowledge_reindex", {
78 + ctxid: shortcuts.getCurrentContextId(),
79 + });
80 +
81 + // finished notification
82 + shortcuts.frontendNotification({
83 + type: shortcuts.NotificationType.SUCCESS,
84 + message: "Knowledge loaded successfully",
85 + priority: shortcuts.NotificationPriority.NORMAL,
86 + displayTime: 2,
87 + group: "knowledge_load",
88 + frontendOnly: true,
89 + });
90 + } catch (e) {
91 + // error notification
92 + shortcuts.frontendNotification({
93 + type: shortcuts.NotificationType.ERROR,
94 + message: "Error loading knowledge",
95 + priority: shortcuts.NotificationPriority.NORMAL,
96 + displayTime: 5,
97 + group: "knowledge_load",
98 + frontendOnly: true,
99 + });
100 + }
101 + },
102 +
103 + // previous implementation without projects
104 + async _loadKnowledge() {
105 const input = document.createElement("input");
106 input.type = "file";
107 input.accept = ".txt,.pdf,.csv,.html,.json,.md";
@@ -70,7 +122,8 @@ const model = {
122 });
123
124 if (!response.ok) {
73 - if (globalThis.toast) globalThis.toast(await response.text(), "error");
125 + if (globalThis.toast)
126 + globalThis.toast(await response.text(), "error");
127 } else {
128 const data = await response.json();
129 if (globalThis.toast) {
@@ -89,9 +142,22 @@ const model = {
142
143 input.click();
144 },
145 +
146 + async browseFiles(path) {
147 + if (!path) {
148 + try {
149 + const resp = await shortcuts.callJsonApi("/chat_files_path_get", {
150 + ctxid: shortcuts.getCurrentContextId(),
151 + });
152 + if (resp.ok) path = resp.path;
153 + } catch (_e) {
154 + console.error("Error getting chat files path", _e);
155 + }
156 + }
157 + await fileBrowserStore.open(path);
158 + },
159 };
160
161 const store = createStore("chatInput", model);
162
163 export { store };
97 -
webui/components/notifications/notification-store.js
+47
@@ -733,6 +733,50 @@ const model = {
733 frontendOnly
734 );
735 },
736 +
737 + async frontendProgress(
738 + message,
739 + title = "Progress",
740 + display_time = 3,
741 + group = "",
742 + priority = defaultPriority,
743 + frontendOnly = false
744 + ) {
745 + return await this.addFrontendToast(
746 + NotificationType.PROGRESS,
747 + message,
748 + title,
749 + display_time,
750 + group,
751 + priority,
752 + frontendOnly
753 + );
754 + },
755 +
756 + // NEW: Enhanced frontend toast with object parameters and type annotations
757 + /**
758 + * Adds a frontend toast notification with object parameters.
759 + * @param {Object} options - The options for the toast notification.
760 + * @param {string} options.type - The type of notification (e.g., info, success, error).
761 + * @param {string} options.message - The message content of the notification.
762 + * @param {string} [options.title=''] - The title of the notification.
763 + * @param {number} [options.displayTime=5] - The display duration in seconds.
764 + * @param {string} [options.group=''] - The group identifier for the notification.
765 + * @param {string} [options.priority='medium'] - The priority of the notification.
766 + * @param {boolean} [options.frontendOnly=false] - Whether to show only on frontend.
767 + * @returns {Promise<string>} The ID of the added notification.
768 + */
769 + async frontendNotification({
770 + type,
771 + message,
772 + title = '',
773 + displayTime = 5,
774 + group = '',
775 + priority = defaultPriority,
776 + frontendOnly = false
777 + }) {
778 + return await this.addFrontendToast(type, message, title, displayTime, group, priority, frontendOnly);
779 + },
780 };
781
782 // Create and export the store
@@ -744,12 +788,14 @@ const toastFrontendInfo = store.frontendInfo.bind(store);
788 const toastFrontendSuccess = store.frontendSuccess.bind(store);
789 const toastFrontendWarning = store.frontendWarning.bind(store);
790 const toastFrontendError = store.frontendError.bind(store);
791 +const toastFrontendProgress = store.frontendProgress.bind(store);
792
793 export {
794 toastFrontendInfo,
795 toastFrontendSuccess,
796 toastFrontendWarning,
797 toastFrontendError,
798 + toastFrontendProgress,
799 };
800
801 // add toasts to global for backward compatibility with older scripts
@@ -757,3 +803,4 @@ globalThis.toastFrontendInfo = toastFrontendInfo;
803 globalThis.toastFrontendSuccess = toastFrontendSuccess;
804 globalThis.toastFrontendWarning = toastFrontendWarning;
805 globalThis.toastFrontendError = toastFrontendError;
806 +globalThis.toastFrontendProgress = toastFrontendProgress;
webui/components/projects/project-edit-memory.html
+22 -1
@@ -17,7 +17,9 @@
17 <label class="projects-form-label">Project-specific memory</label>
18 <span class="projects-form-description">When turned on, the agent's memory while working on this
19 project will be isolated from the global memory. The memory will be stored in <strong><span
20 - x-text="$store.projects.getSelectedAbsPath('.a0proj','memory')"></span></strong>.</span>
20 + x-text="$store.projects.getSelectedAbsPath('.a0proj','memory')"></span></strong>.
21 + <br>
22 + Project-specific knowledge files can be used only when project-specific memory is turned on.</span>
23 </div>
24 <div class="projects-setting-control">
25 <label class="toggle">
@@ -27,6 +29,22 @@
29 </div>
30 </div>
31
32 + <div class="projects-form-group" x-show="$store.projects.selectedProject._ownMemory">
33 + <label class="projects-form-label">Knowledge files</label>
34 + <div class="projects-input-with-button-wrapper">
35 + <span class="projects-form-description">Additional knowledge files in <strong><span
36 + x-text="$store.projects.getSelectedAbsPath('.a0proj','knowledge')"></span></strong>.
37 + Knowledge files are imported into memory and are used when relevant based on the context of the conversation.</span>
38 + <span class="projects-form-description">Currently there are <strong><span
39 + x-text="$store.projects.selectedProject.knowledge_files_count || 0"></span></strong>
40 + knowledge files.</span>
41 + <button class="button icon-button" x-on:click="$store.projects.browseKnowledgeFiles()">
42 + <span class="icon material-symbols-outlined">folder</span>
43 + <span>Browse</span>
44 + </button>
45 + </div>
46 + </div>
47 +
48 <style>
49 .projects-setting-row {
50 display: flex;
@@ -34,15 +52,18 @@
52 justify-content: space-between;
53 gap: 2rem;
54 }
55 +
56 .projects-setting-text {
57 flex-grow: 1;
58 flex-shrink: 1;
59 min-width: 0;
60 }
61 +
62 .projects-setting-control {
63 flex-grow: 0;
64 flex-shrink: 0;
65 }
66 +
67 .projects-form-label {
68 display: block;
69 }
webui/components/projects/project-selector.html
+89 -20
@@ -8,38 +8,107 @@
8 </head>
9
10 <body>
11 - <div x-data>
11 + <div x-data="{ open: false }" @click.away="open = false" class="project-dropdown-container">
12 <template x-if="$store.projects && $store.chats && $store.chats.selectedContext">
13 <div>
14 <template x-if="$store.chats.selectedContext.project?.name">
15 - <button @click="$store.projects.openProjectsModal()" type="button" class="button project-button">
15 + <button @click="open = !open" type="button" class="button project-dropdown-button">
16 <span class="project-color-ball" :style="$store.chats.selectedContext.project.color ? { backgroundColor: $store.chats.selectedContext.project.color } : { border: '1px solid var(--color-border)' }"></span>
17 - <span x-text="$store.chats.selectedContext.project.name"></span>
17 + <span x-text="$store.chats.selectedContext.project.title"></span>
18 + <span class="icon material-symbols-outlined">arrow_drop_down</span>
19 </button>
20 </template>
21 <template x-if="!$store.chats.selectedContext.project?.name">
21 - <button @click="$store.projects.openProjectsModal()" type="button" class="button project-button">
22 - No project</button>
22 + <button @click="open = !open" type="button" class="button project-dropdown-button">
23 + No project
24 + <span class="icon material-symbols-outlined">arrow_drop_down</span>
25 + </button>
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 +
33 + <hr x-show=" $store.projects.projectList.length > 0" class="project-dropdown-divider">
34 + <template x-for="project in $store.projects.projectList" :key="project.name">
35 + <a href="#" @click.prevent="$store.projects.activateProject(project.name); open = false;" class="project-dropdown-item">
36 + <span class="project-color-ball" :style="project.color ? { backgroundColor: project.color } : { border: '1px solid var(--color-border)' }"></span>
37 + <span class="project-selector-item-text" x-text="project.title"></span>
38 + </a>
39 + </template>
40 + </div>
41 </div>
42 </template>
43 </div>
44 </body>
45
46 <style>
30 - .project-button {
31 - display: inline-flex;
32 - align-items: center;
33 - gap: 0.3em;
34 - padding-left: 0.5em;
35 - padding-right: 0.5em;
36 - }
37 - .project-color-ball {
38 - width: 0.6em;
39 - height: 0.6em;
40 - border-radius: 50%;
41 - display: inline-block;
42 - box-sizing: border-box;
43 - }
47 + .project-dropdown-container {
48 + position: relative;
49 + display: inline-block;
50 + }
51 +
52 + .project-dropdown-button {
53 + display: inline-flex;
54 + align-items: center;
55 + gap: 0.3em;
56 + padding-left: 0.5em;
57 + padding-right: 0.5em;
58 + }
59 +
60 + .project-color-ball {
61 + width: 0.6em;
62 + height: 0.6em;
63 + border-radius: 50%;
64 + display: inline-block;
65 + box-sizing: border-box;
66 + flex-shrink: 0;
67 + }
68 +
69 + .project-dropdown-menu {
70 + position: absolute;
71 + top: 100%;
72 + right: 0;
73 + z-index: 1000;
74 + background-color: var(--color-panel);
75 + border: 1px solid var(--color-border);
76 + border-radius: 4px;
77 + padding: 0.5em 0;
78 + min-width: 10rem;
79 + max-width: 20em;
80 + max-height: 70vh;
81 + overflow-y: auto;
82 + margin-top: 0.25rem;
83 + }
84 +
85 + .project-dropdown-item {
86 + display: flex;
87 + align-items: center;
88 + margin: 0.25em 0.5em 0.25em 0.5em;
89 + padding: 0.5em 1em;
90 + color: var(--color-text-primary);
91 + border-radius: var(--spacing-xs);
92 + text-decoration: none;
93 + white-space: nowrap;
94 + overflow: hidden;
95 + text-overflow: ellipsis;
96 + }
97 +
98 + .project-dropdown-item:hover {
99 + background-color: var(--color-background-hover);
100 + }
101 +
102 + .project-dropdown-divider {
103 + height: 1px;
104 + margin: 0.5rem 0;
105 + overflow: hidden;
106 + background-color: var(--color-border);
107 + border: 0;
108 + }
109 +
110 + .project-selector-item-text {
111 + margin-left: 0.5em;
112 + }
113 </style>
45 -</html>
\ No newline at end of file
114 +</html>
webui/components/projects/projects-store.js
+51
@@ -4,6 +4,7 @@ import * as modals from "/js/modals.js";
4 import * as notifications from "/components/notifications/notification-store.js";
5 import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
6 import { store as browserStore } from "/components/modals/file-browser/file-browser-store.js";
7 +import * as shortcuts from "/js/shortcuts.js";
8
9 const listModal = "projects/project-list.html";
10 const createModal = "projects/project-create.html";
@@ -322,6 +323,56 @@ const model = {
323 }
324 },
325
326 + async browseKnowledgeFiles() {
327 + await this.browseSelected(".a0proj", "knowledge");
328 + // refresh and reindex project
329 + try {
330 + // progress notification
331 + shortcuts.frontendNotification({
332 + type: shortcuts.NotificationType.PROGRESS,
333 + message: "Loading knowledge...",
334 + priority: shortcuts.NotificationPriority.NORMAL,
335 + displayTime: 999,
336 + group: "knowledge_load",
337 + frontendOnly: true,
338 + });
339 +
340 + // call reindex knowledge
341 + const reindexCall = api.callJsonApi("/knowledge_reindex", {
342 + ctxid: shortcuts.getCurrentContextId(),
343 + });
344 +
345 + const newData = await this._createEditProjectData(
346 + this.selectedProject.name
347 + );
348 + this.selectedProject.knowledge_files_count =
349 + newData.knowledge_files_count;
350 +
351 + // wait for reindex to finish
352 + await reindexCall;
353 +
354 + // finished notification
355 + shortcuts.frontendNotification({
356 + type: shortcuts.NotificationType.SUCCESS,
357 + message: "Knowledge loaded successfully",
358 + priority: shortcuts.NotificationPriority.NORMAL,
359 + displayTime: 2,
360 + group: "knowledge_load",
361 + frontendOnly: true,
362 + });
363 + } catch (error) {
364 + // error notification
365 + shortcuts.frontendNotification({
366 + type: shortcuts.NotificationType.ERROR,
367 + message: "Error loading knowledge",
368 + priority: shortcuts.NotificationPriority.NORMAL,
369 + displayTime: 5,
370 + group: "knowledge_load",
371 + frontendOnly: true,
372 + });
373 + }
374 + },
375 +
376 getSelectedAbsPath(...relPath) {
377 return ["/a0/usr/projects", this.selectedProject.name, ...relPath]
378 .join("/")
webui/components/sidebar/chats/chats-list.html
+6 -4
@@ -14,9 +14,11 @@
14 <ul class="config-list chats-config-list" x-show="$store.chats.contexts.length > 0">
15 <template x-for="context in $store.chats.contexts" :key="context.id">
16 <li>
17 - <div :class="{'chat-container': true, 'chat-selected': context.id === $store.chats.selected}" @click="$store.chats.selectChat(context.id)">
17 + <div :class="{'chat-container': true, 'chat-selected': context.id === $store.chats.selected}"
18 + @click="$store.chats.selectChat(context.id)">
19 <div class="chat-list-button">
19 - <span class="project-color-ball" :style="context.project?.color ? { backgroundColor: context.project.color } : { border: '1px solid var(--color-border)' }"></span>
20 + <span class="project-color-ball"
21 + :style="context.project?.color ? { backgroundColor: context.project.color } : { border: '1px solid var(--color-border)' }"></span>
22 <span class="chat-name" :title="context.name ? context.name : 'Chat #' + context.no"
23 x-text="context.name ? context.name : 'Chat #' + context.no"></span>
24 </div>
@@ -80,7 +82,7 @@
82 }
83
84 .chat-container:hover {
83 - background-color: rgba(255, 255, 255, 0.03);
85 + background-color: var(--color-background-hover);
86 }
87
88 .chat-list-button {
@@ -149,7 +151,7 @@
151
152 /* Selected chat accent */
153 .chat-selected {
152 - background-color: rgba(var(--color-border-rgb, 50, 50, 50), 0.5);
154 + background-color: var(--color-background-hover);
155 }
156
157 .chat-selected .chat-name {
webui/components/sidebar/tasks/tasks-list.html
+2 -2
@@ -149,7 +149,7 @@
149 }
150
151 .task-container:hover {
152 - background-color: rgba(255, 255, 255, 0.03);
152 + background-color: var(--color-background-hover);
153 }
154
155
@@ -157,7 +157,7 @@
157
158 /* Selected task accent */
159 .task-selected {
160 - background-color: rgba(var(--color-border-rgb, 50, 50, 50), 0.5);
160 + background-color: var(--color-background-hover);
161 }
162
163 .task-selected .task-name {
webui/index.css
+2
@@ -49,6 +49,7 @@
49 --color-border: var(--color-border-dark);
50 --color-input: var(--color-input-dark);
51 --color-input-focus: var(--color-input-focus-dark);
52 + --color-background-hover: color-mix(in srgb, var(--color-border) 50%, transparent);
53
54 /* Spacing variables */
55 --spacing-xxs: 0.15rem;
@@ -88,6 +89,7 @@
89 --color-border: var(--color-border-light);
90 --color-input: var(--color-input-light);
91 --color-input-focus: var(--color-input-focus-light);
92 + --color-background-hover: color-mix(in srgb, var(--color-border) 50%, transparent);
93 }
94
95 /* Reset and Base Styles */
webui/js/shortcuts.js new
+24
@@ -0,0 +1,24 @@
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
10 +
11 +// api
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 +}