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
+}