- @if (gitDiffViewerVisible()) {
+ @if (fileManagerContext() === 'app' && gitDiffViewerVisible()) {
();
agentId = input.required();
chatVisible = input(true);
+ /** Files API root: workspace (`app`) or provider agent config (`config`). */
+ fileManagerContext = input('app');
// Internal state
selectedFilePath = signal(null);
@@ -119,23 +123,26 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
// Refresh debounce subject to prevent multiple rapid refreshes
private readonly refreshTrigger$ = new Subject();
private isRefreshing = false;
+ private previousFileManagerContext: FileManagerContext | null = null;
// Convert signals to observables
private readonly selectedFilePath$ = toObservable(this.selectedFilePath);
private readonly clientId$ = toObservable(this.clientId);
private readonly agentId$ = toObservable(this.agentId);
+ private readonly fileManagerContext$ = toObservable(this.fileManagerContext);
// Computed observables
readonly selectedFileContent$: Observable = combineLatest([
this.selectedFilePath$,
this.clientId$,
this.agentId$,
+ this.fileManagerContext$,
]).pipe(
- switchMap(([filePath, clientId, agentId]) => {
+ switchMap(([filePath, clientId, agentId, fileCtx]) => {
if (!filePath || !clientId || !agentId) {
return of(null);
}
- return this.filesFacade.getFileContent$(clientId, agentId, filePath);
+ return this.filesFacade.getFileContent$(clientId, agentId, filePath, fileCtx);
}),
);
@@ -143,15 +150,16 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
this.selectedFilePath$,
this.clientId$,
this.agentId$,
+ this.fileManagerContext$,
toObservable(this.lastLoadedFilePath),
]).pipe(
- switchMap(([filePath, clientId, agentId, lastLoadedFilePath]) => {
+ switchMap(([filePath, clientId, agentId, fileCtx, lastLoadedFilePath]) => {
if (!filePath || !clientId || !agentId) {
return of(false);
}
return this.filesFacade
- .isReadingFile$(clientId, agentId, filePath)
+ .isReadingFile$(clientId, agentId, filePath, fileCtx)
.pipe(map((isLoading) => isLoading && lastLoadedFilePath !== filePath));
}),
);
@@ -160,12 +168,13 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
this.selectedFilePath$,
this.clientId$,
this.agentId$,
+ this.fileManagerContext$,
]).pipe(
- switchMap(([filePath, clientId, agentId]) => {
+ switchMap(([filePath, clientId, agentId, fileCtx]) => {
if (!filePath || !clientId || !agentId) {
return of(false);
}
- return this.filesFacade.isWritingFile$(clientId, agentId, filePath);
+ return this.filesFacade.isWritingFile$(clientId, agentId, filePath, fileCtx);
}),
);
@@ -180,23 +189,46 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
});
// Open tabs
- readonly openTabs$: Observable = combineLatest([this.clientId$, this.agentId$]).pipe(
- switchMap(([clientId, agentId]) => {
+ readonly openTabs$: Observable = combineLatest([
+ this.clientId$,
+ this.agentId$,
+ this.fileManagerContext$,
+ ]).pipe(
+ switchMap(([clientId, agentId, fileCtx]) => {
if (!clientId || !agentId) {
return of([]);
}
- return this.filesFacade.getOpenTabs$(clientId, agentId);
+ return this.filesFacade.getOpenTabs$(clientId, agentId, fileCtx);
}),
);
private resizeObserver?: ResizeObserver;
+ private listParams(path: string): ListDirectoryParams {
+ const c = this.fileManagerContext();
+ return c === 'app' ? { path } : { path, context: c };
+ }
+
constructor() {
+ effect(() => {
+ const ctx = this.fileManagerContext();
+ if (this.previousFileManagerContext !== null && this.previousFileManagerContext !== ctx) {
+ this.selectedFilePath.set(null);
+ this.lastLoadedFilePath.set(null);
+ this.dirtyFiles.set(new Set());
+ this.expandedPaths.set(new Set());
+ this.gitManagerVisible.set(false);
+ this.gitDiffViewerVisible.set(false);
+ this.gitDiffFilePath.set(null);
+ }
+ this.previousFileManagerContext = ctx;
+ });
+
// Load file when selected
effect(() => {
const filePath = this.selectedFilePath();
if (filePath && this.clientId() && this.agentId()) {
- this.filesFacade.readFile(this.clientId(), this.agentId(), filePath);
+ this.filesFacade.readFile(this.clientId(), this.agentId(), filePath, this.fileManagerContext());
}
});
@@ -231,7 +263,12 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
const overflowed = this.overflowedTabs();
const isOverflowed = overflowed.some((tab) => tab.filePath === selectedPath);
if (isOverflowed) {
- this.filesFacade.moveTabToFront(this.clientId(), this.agentId(), selectedPath);
+ this.filesFacade.moveTabToFront(
+ this.clientId(),
+ this.agentId(),
+ selectedPath,
+ this.fileManagerContext(),
+ );
// Recalculate after moving
setTimeout(() => this.calculateVisibleTabs(), 50);
}
@@ -262,9 +299,13 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
const currentSelectedPath = this.selectedFilePath();
const clientId = this.clientId();
const agentId = this.agentId();
+ const actionCtx = action.context ?? 'app';
+ if (actionCtx !== this.fileManagerContext()) {
+ return;
+ }
- // Reload git status after file move
- if (clientId === action.clientId && agentId === action.agentId) {
+ // Reload git status after file move (workspace only)
+ if (this.fileManagerContext() === 'app' && clientId === action.clientId && agentId === action.agentId) {
setTimeout(() => {
this.vcsFacade.loadStatus(clientId, agentId);
}, 500);
@@ -288,7 +329,7 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
// Load the file content at the new location
// The effect will automatically load it when selectedFilePath changes
- this.filesFacade.readFile(clientId, agentId, action.destinationPath);
+ this.filesFacade.readFile(clientId, agentId, action.destinationPath, this.fileManagerContext());
}
});
@@ -420,7 +461,7 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
// Open tab when file is selected
// The effect will automatically move it to front if it ends up in overflow
if (this.clientId() && this.agentId()) {
- this.filesFacade.openFileTab(this.clientId(), this.agentId(), filePath);
+ this.filesFacade.openFileTab(this.clientId(), this.agentId(), filePath, this.fileManagerContext());
}
}
@@ -496,7 +537,7 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
encoding: 'utf-8',
};
- this.filesFacade.writeFile(clientId, agentId, filePath, writeDto);
+ this.filesFacade.writeFile(clientId, agentId, filePath, writeDto, this.fileManagerContext());
// Mark as not dirty and sync editorContent after successful save
// Also emit file update notification to other clients after successful save
@@ -531,10 +572,12 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
return newSaved;
});
- // Reload git status after file save
- setTimeout(() => {
- this.vcsFacade.loadStatus(clientId, agentId);
- }, 300);
+ // Reload git status after file save (workspace only)
+ if (this.fileManagerContext() === 'app') {
+ setTimeout(() => {
+ this.vcsFacade.loadStatus(clientId, agentId);
+ }, 300);
+ }
// Emit file update notification to other clients after successful save
// agentId is required for routing the event to the correct agent
@@ -595,15 +638,23 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
};
const fullPath = event.path === '.' ? event.name : `${event.path}/${event.name}`;
- this.filesFacade.createFileOrDirectory(this.clientId(), this.agentId(), fullPath, createDto);
+ this.filesFacade.createFileOrDirectory(
+ this.clientId(),
+ this.agentId(),
+ fullPath,
+ createDto,
+ this.fileManagerContext(),
+ );
// Refresh directory listing
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: event.path });
+ this.filesFacade.listDirectory(this.clientId(), this.agentId(), this.listParams(event.path));
- // Reload git status after file creation
- setTimeout(() => {
- this.vcsFacade.loadStatus(this.clientId(), this.agentId());
- }, 500);
+ // Reload git status after file creation (workspace only)
+ if (this.fileManagerContext() === 'app') {
+ setTimeout(() => {
+ this.vcsFacade.loadStatus(this.clientId(), this.agentId());
+ }, 500);
+ }
// If it's a file, select it
if (event.type === 'file') {
@@ -622,7 +673,7 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
onFileDelete(filePath: string): void {
// Confirmation is handled by the file-tree component's Bootstrap modal
- this.filesFacade.deleteFileOrDirectory(this.clientId(), this.agentId(), filePath);
+ this.filesFacade.deleteFileOrDirectory(this.clientId(), this.agentId(), filePath, this.fileManagerContext());
// If deleted file was selected, clear selection
if (this.selectedFilePath() === filePath) {
@@ -635,12 +686,14 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
}
// Refresh root directory listing
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: '.' });
+ this.filesFacade.listDirectory(this.clientId(), this.agentId(), this.listParams('.'));
- // Reload git status after file deletion
- setTimeout(() => {
- this.vcsFacade.loadStatus(this.clientId(), this.agentId());
- }, 500);
+ // Reload git status after file deletion (workspace only)
+ if (this.fileManagerContext() === 'app') {
+ setTimeout(() => {
+ this.vcsFacade.loadStatus(this.clientId(), this.agentId());
+ }, 500);
+ }
}
onDirectoryExpand(path: string | Event): void {
@@ -714,17 +767,17 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
if (path === '.') {
// Small delay for root directory to avoid cancellation
setTimeout(() => {
- this.filesFacade.listDirectory(clientId, agentId, { path: '.' });
+ this.filesFacade.listDirectory(clientId, agentId, this.listParams('.'));
}, 50);
} else {
- this.filesFacade.listDirectory(clientId, agentId, { path });
+ this.filesFacade.listDirectory(clientId, agentId, this.listParams(path));
}
});
// If root is not in expanded paths, reload it anyway (it's always needed)
if (!currentExpandedPaths.has('.')) {
setTimeout(() => {
- this.filesFacade.listDirectory(clientId, agentId, { path: '.' });
+ this.filesFacade.listDirectory(clientId, agentId, this.listParams('.'));
}, 50);
}
@@ -736,7 +789,7 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
// Reload currently selected file if it exists
if (currentSelectedFile) {
- this.filesFacade.readFile(clientId, agentId, currentSelectedFile);
+ this.filesFacade.readFile(clientId, agentId, currentSelectedFile, this.fileManagerContext());
}
}
@@ -754,7 +807,7 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
// Move the tab to the front before selecting it
if (this.clientId() && this.agentId()) {
- this.filesFacade.moveTabToFront(this.clientId(), this.agentId(), filePath);
+ this.filesFacade.moveTabToFront(this.clientId(), this.agentId(), filePath, this.fileManagerContext());
}
this.onTabClick(filePath);
@@ -781,7 +834,7 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
event.preventDefault();
event.stopPropagation();
if (this.clientId() && this.agentId()) {
- this.filesFacade.pinFileTab(this.clientId(), this.agentId(), filePath);
+ this.filesFacade.pinFileTab(this.clientId(), this.agentId(), filePath, this.fileManagerContext());
}
}
@@ -789,7 +842,7 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
event.preventDefault();
event.stopPropagation();
if (this.clientId() && this.agentId()) {
- this.filesFacade.closeFileTab(this.clientId(), this.agentId(), filePath);
+ this.filesFacade.closeFileTab(this.clientId(), this.agentId(), filePath, this.fileManagerContext());
// If the closed tab was selected, select the first remaining tab or clear selection
if (this.selectedFilePath() === filePath) {
this.openTabs$.pipe(take(1), takeUntilDestroyed(this.destroyRef)).subscribe((tabs) => {
@@ -821,7 +874,8 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
// Build the URL
const baseUrl = window.location.origin;
- const editorPath = `/clients/${clientId}/agents/${agentId}/editor`;
+ const segment = this.fileManagerContext() === 'config' ? 'config' : 'editor';
+ const editorPath = `/clients/${clientId}/agents/${agentId}/${segment}`;
const queryParams = new URLSearchParams();
queryParams.set('standalone', 'true');
queryParams.set('file', encodeURIComponent(filePath));
@@ -874,7 +928,7 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
// Wait a bit to ensure the new window has opened before closing the tab
setTimeout(() => {
if (this.clientId() && this.agentId()) {
- this.filesFacade.closeFileTab(this.clientId(), this.agentId(), filePath);
+ this.filesFacade.closeFileTab(this.clientId(), this.agentId(), filePath, this.fileManagerContext());
// If the closed tab was selected, select the first remaining tab or clear selection
if (this.selectedFilePath() === filePath) {
this.openTabs$.pipe(take(1), takeUntilDestroyed(this.destroyRef)).subscribe((tabs) => {
@@ -969,7 +1023,7 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
const isOverflowed = overflowed.some((tab) => tab.filePath === selectedPath);
if (isOverflowed) {
// Move selected tab to front and recalculate
- this.filesFacade.moveTabToFront(this.clientId(), this.agentId(), selectedPath);
+ this.filesFacade.moveTabToFront(this.clientId(), this.agentId(), selectedPath, this.fileManagerContext());
// The tab order change will trigger a recalculation via the effect
// But we also need to recalculate immediately to show the change
setTimeout(() => this.calculateVisibleTabs(), 50);
@@ -1003,6 +1057,9 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
}
onToggleGitManager(): void {
+ if (this.fileManagerContext() !== 'app') {
+ return;
+ }
const wasVisible = this.gitManagerVisible();
this.gitManagerVisible.update((visible) => !visible);
@@ -1021,6 +1078,9 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
}
onShowGitDiff(filePath: string): void {
+ if (this.fileManagerContext() !== 'app') {
+ return;
+ }
this.gitDiffFilePath.set(filePath);
this.gitDiffViewerVisible.set(true);
}
@@ -1040,6 +1100,10 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
* - If file is not dirty: automatically reloads the file from server
*/
private handleFileUpdateNotification(notification: FileUpdateNotificationData): void {
+ if (this.fileManagerContext() !== 'app') {
+ return;
+ }
+
const currentFilePath = this.selectedFilePath();
const currentSocketId = getSocketInstance()?.id;
const clientId = this.clientId();
@@ -1080,7 +1144,7 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
}
} else {
// File is not dirty - automatically reload from server (no need to disable autosave)
- this.filesFacade.readFile(clientId, agentId, notification.filePath);
+ this.filesFacade.readFile(clientId, agentId, notification.filePath, this.fileManagerContext());
}
}
}
@@ -1138,11 +1202,11 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
}
// Clear cached content first so Monaco wrapper receives a fresh emission and updates correctly
- this.filesFacade.clearFileContent(clientId, agentId, filePath);
+ this.filesFacade.clearFileContent(clientId, agentId, filePath, this.fileManagerContext());
// Reset lastLoadedFilePath so the content effect will run when new content arrives
this.lastLoadedFilePath.set(null);
// Reload the file from server
- this.filesFacade.readFile(clientId, agentId, filePath);
+ this.filesFacade.readFile(clientId, agentId, filePath, this.fileManagerContext());
// Clear dirty state for this file
this.dirtyFiles.update((dirty) => {
@@ -1165,10 +1229,12 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
this.showFileUpdateModal.set(false);
this.fileUpdateNotification.set(null);
- // Reload git status after accepting file update
- setTimeout(() => {
- this.vcsFacade.loadStatus(clientId, agentId);
- }, 500);
+ // Reload git status after accepting file update (workspace only)
+ if (this.fileManagerContext() === 'app') {
+ setTimeout(() => {
+ this.vcsFacade.loadStatus(clientId, agentId);
+ }, 500);
+ }
}
/**
@@ -1291,7 +1357,7 @@ export class FileEditorComponent implements OnDestroy, AfterViewInit {
ngOnDestroy(): void {
// Clear all open tabs when component is destroyed
if (this.clientId() && this.agentId()) {
- this.filesFacade.clearOpenTabs(this.clientId(), this.agentId());
+ this.filesFacade.clearOpenTabs(this.clientId(), this.agentId(), this.fileManagerContext());
}
// Clean up ResizeObserver
if (this.resizeObserver) {
diff --git a/libs/domains/framework/frontend/feature-agent-console/src/lib/file-editor/file-tree/file-tree.component.html b/libs/domains/framework/frontend/feature-agent-console/src/lib/file-editor/file-tree/file-tree.component.html
index 7607ddc0..077f55bc 100644
--- a/libs/domains/framework/frontend/feature-agent-console/src/lib/file-editor/file-tree/file-tree.component.html
+++ b/libs/domains/framework/frontend/feature-agent-console/src/lib/file-editor/file-tree/file-tree.component.html
@@ -132,57 +132,59 @@ Files
}
- @if (clientRepositoryName$ | async; as repoName) {
-
-
-
-
- {{ repoName }}
-
- @if (currentBranch$ | async; as branch) {
-
-
-
- {{ branch }}
-
- @if (statusIndicator$ | async; as indicator) {
-
+
+
+
+ {{ repoName }}
+
+ @if (currentBranch$ | async; as branch) {
+
+
- @if (showStatusIndicatorSpinner$ | async) {
-
- Loading...
-
- }
-
- }
-
- }
+
+
{{ branch }}
+
+ @if (statusIndicator$ | async; as indicator) {
+
+ @if (showStatusIndicatorSpinner$ | async) {
+
+ Loading...
+
+ }
+
+ }
+
+ }
+
-
- }
+ }
-
-
+
+
+ }
diff --git a/libs/domains/framework/frontend/feature-agent-console/src/lib/file-editor/file-tree/file-tree.component.ts b/libs/domains/framework/frontend/feature-agent-console/src/lib/file-editor/file-tree/file-tree.component.ts
index 26e26065..5a4810d1 100644
--- a/libs/domains/framework/frontend/feature-agent-console/src/lib/file-editor/file-tree/file-tree.component.ts
+++ b/libs/domains/framework/frontend/feature-agent-console/src/lib/file-editor/file-tree/file-tree.component.ts
@@ -18,7 +18,9 @@ import {
ClientsFacade,
FilesFacade,
VcsFacade,
+ type FileManagerContext,
type FileNodeDto,
+ type ListDirectoryParams,
} from '@forepath/framework/frontend/data-access-agent-console';
import { combineLatest, filter, map, Observable, of, Subscription, switchMap, take } from 'rxjs';
import { GitBranchModalComponent } from '../git-branch-modal/git-branch-modal.component';
@@ -68,6 +70,8 @@ export class FileTreeComponent implements OnInit {
expandedPaths = input
>(new Set());
selectedPath = input(null);
gitManagerVisible = input(false);
+ /** Files API root: workspace (`app`) or provider agent config (`config`). */
+ fileManagerContext = input('app');
// Outputs
fileSelect = output();
@@ -96,15 +100,24 @@ export class FileTreeComponent implements OnInit {
private hoverTimeout: ReturnType | null = null;
private expandedDirectorySubscriptions = new Map();
+ private listParams(path: string): ListDirectoryParams {
+ const c = this.fileManagerContext();
+ return c === 'app' ? { path } : { path, context: c };
+ }
+
+ private listDirectoryRel(path: string): void {
+ this.filesFacade.listDirectory(this.clientId(), this.agentId(), this.listParams(path));
+ }
+
// Computed observables for directory listings - convert computed signals to observables
private readonly rootDirectorySignal = computed(() => {
const clientId = this.clientId();
const agentId = this.agentId();
+ const context = this.fileManagerContext();
if (!clientId || !agentId) {
return null;
}
- // Return a placeholder - we'll use toObservable to convert the signal
- return { clientId, agentId };
+ return { clientId, agentId, context };
});
readonly rootDirectory$: Observable = toObservable(this.rootDirectorySignal).pipe(
@@ -112,17 +125,18 @@ export class FileTreeComponent implements OnInit {
if (!config) {
return of(null);
}
- return this.filesFacade.getDirectoryListing$(config.clientId, config.agentId, '.');
+ return this.filesFacade.getDirectoryListing$(config.clientId, config.agentId, '.', config.context);
}),
);
private readonly rootLoadingSignal = computed(() => {
const clientId = this.clientId();
const agentId = this.agentId();
+ const context = this.fileManagerContext();
if (!clientId || !agentId) {
return false;
}
- return { clientId, agentId };
+ return { clientId, agentId, context };
});
readonly rootLoading$: Observable = toObservable(this.rootLoadingSignal).pipe(
@@ -132,8 +146,8 @@ export class FileTreeComponent implements OnInit {
}
// Only show loading if we don't have cached data (silent refresh)
return combineLatest([
- this.filesFacade.isListingDirectory$(config.clientId, config.agentId, '.'),
- this.filesFacade.getDirectoryListing$(config.clientId, config.agentId, '.'),
+ this.filesFacade.isListingDirectory$(config.clientId, config.agentId, '.', config.context),
+ this.filesFacade.getDirectoryListing$(config.clientId, config.agentId, '.', config.context),
]).pipe(
map(([isLoading, cachedData]) => {
// Show loading only if loading AND no cached data exists
@@ -204,12 +218,12 @@ export class FileTreeComponent implements OnInit {
// Helper to get directory listing observable
getDirectoryListing$(path: string): Observable {
- return this.filesFacade.getDirectoryListing$(this.clientId(), this.agentId(), path);
+ return this.filesFacade.getDirectoryListing$(this.clientId(), this.agentId(), path, this.fileManagerContext());
}
// Helper to get directory loading observable
getDirectoryLoading$(path: string): Observable {
- return this.filesFacade.isListingDirectory$(this.clientId(), this.agentId(), path);
+ return this.filesFacade.isListingDirectory$(this.clientId(), this.agentId(), path, this.fileManagerContext());
}
constructor() {
@@ -227,9 +241,10 @@ export class FileTreeComponent implements OnInit {
this.hasLoadedContent.set(false);
this.hasHadOperation.set(false);
this.isReloadingAfterOperation.set(false);
- this.filesFacade.listDirectory(clientId, agentId, { path: '.' });
- // Load git status
- this.vcsFacade.loadStatus(clientId, agentId);
+ this.filesFacade.listDirectory(clientId, agentId, this.listParams('.'));
+ if (this.fileManagerContext() === 'app') {
+ this.vcsFacade.loadStatus(clientId, agentId);
+ }
}
});
@@ -384,7 +399,7 @@ export class FileTreeComponent implements OnInit {
if (!hasCachedData) {
// Only show loading if we don't have cached data (silent refresh)
node.loading = true;
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: node.path });
+ this.listDirectoryRel(node.path);
// Subscribe to directory listing
this.getDirectoryListing$(node.path)
.pipe(
@@ -401,7 +416,7 @@ export class FileTreeComponent implements OnInit {
});
} else {
// We have cached data, but still reload to get fresh data (silent)
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: node.path });
+ this.listDirectoryRel(node.path);
}
this.directoryExpand.emit(node.path);
}
@@ -691,21 +706,27 @@ export class FileTreeComponent implements OnInit {
}
// Use move functionality
- this.filesFacade.moveFileOrDirectory(this.clientId(), this.agentId(), dragged.path, {
- destination: destinationPath,
- });
+ this.filesFacade.moveFileOrDirectory(
+ this.clientId(),
+ this.agentId(),
+ dragged.path,
+ {
+ destination: destinationPath,
+ },
+ this.fileManagerContext(),
+ );
this.draggedItem.set(null);
// Refresh source parent directory
const sourceParentPath = this.getParentPath(dragged.path);
setTimeout(() => {
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: sourceParentPath });
+ this.listDirectoryRel(sourceParentPath);
}, 100);
// Refresh destination directory
setTimeout(() => {
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: node.path });
+ this.listDirectoryRel(node.path);
}, 200);
// Expand target path in the tree
@@ -802,20 +823,26 @@ export class FileTreeComponent implements OnInit {
}
// Use move functionality
- this.filesFacade.moveFileOrDirectory(this.clientId(), this.agentId(), dragged.path, {
- destination: destinationPath,
- });
+ this.filesFacade.moveFileOrDirectory(
+ this.clientId(),
+ this.agentId(),
+ dragged.path,
+ {
+ destination: destinationPath,
+ },
+ this.fileManagerContext(),
+ );
this.draggedItem.set(null);
// Refresh source parent directory
setTimeout(() => {
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: sourceParentPath });
+ this.listDirectoryRel(sourceParentPath);
}, 100);
// Refresh root directory
setTimeout(() => {
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: '.' });
+ this.listDirectoryRel('.');
}, 200);
}
@@ -847,7 +874,7 @@ export class FileTreeComponent implements OnInit {
// Load directory if not cached
const hasCachedData = this.treeCache().has(path);
if (!hasCachedData) {
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path });
+ this.listDirectoryRel(path);
}
}
this.hoverTimeout = null;
@@ -891,7 +918,7 @@ export class FileTreeComponent implements OnInit {
// Wait a bit for the file/directory to be created, then refresh the parent directory listing
setTimeout(() => {
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: creating.path });
+ this.listDirectoryRel(creating.path);
}, 100);
this.creatingItem.set(null);
@@ -951,7 +978,7 @@ export class FileTreeComponent implements OnInit {
this.removeFromCache(item.path);
// Refresh the parent directory listing to update the tree
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: parentPath });
+ this.listDirectoryRel(parentPath);
}
}
@@ -969,9 +996,15 @@ export class FileTreeComponent implements OnInit {
const destinationPath = parentPath === '.' ? newName : `${parentPath}/${newName}`;
// Use move functionality to rename
- this.filesFacade.moveFileOrDirectory(this.clientId(), this.agentId(), item.path, {
- destination: destinationPath,
- });
+ this.filesFacade.moveFileOrDirectory(
+ this.clientId(),
+ this.agentId(),
+ item.path,
+ {
+ destination: destinationPath,
+ },
+ this.fileManagerContext(),
+ );
this.hideModal(this.renameFileModal);
this.itemToRename.set(null);
@@ -979,7 +1012,7 @@ export class FileTreeComponent implements OnInit {
// Refresh the parent directory listing to update the tree
setTimeout(() => {
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: parentPath });
+ this.listDirectoryRel(parentPath);
}, 100);
}
@@ -1002,9 +1035,15 @@ export class FileTreeComponent implements OnInit {
}
// Use move functionality
- this.filesFacade.moveFileOrDirectory(this.clientId(), this.agentId(), item.path, {
- destination: fullDestinationPath,
- });
+ this.filesFacade.moveFileOrDirectory(
+ this.clientId(),
+ this.agentId(),
+ item.path,
+ {
+ destination: fullDestinationPath,
+ },
+ this.fileManagerContext(),
+ );
this.hideModal(this.moveFileModal);
this.itemToMove.set(null);
@@ -1013,13 +1052,13 @@ export class FileTreeComponent implements OnInit {
// Refresh source parent directory
const sourceParentPath = this.getParentPath(item.path);
setTimeout(() => {
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: sourceParentPath });
+ this.listDirectoryRel(sourceParentPath);
}, 100);
// Refresh destination directory and expand target path in the tree
const destinationParentPath = this.getParentPath(fullDestinationPath);
setTimeout(() => {
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: destinationParentPath });
+ this.listDirectoryRel(destinationParentPath);
}, 200);
// Expand target path in the tree
@@ -1063,7 +1102,7 @@ export class FileTreeComponent implements OnInit {
// Load directory if not cached
const hasCachedData = this.treeCache().has(path);
if (!hasCachedData) {
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path });
+ this.listDirectoryRel(path);
}
}
}, index * 100); // 100ms delay between each expansion
@@ -1384,7 +1423,7 @@ export class FileTreeComponent implements OnInit {
const pathsArray = Array.from(pathsToRefresh);
pathsArray.forEach((path, index) => {
setTimeout(() => {
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path });
+ this.listDirectoryRel(path);
}, index * 50); // 50ms delay between each call
});
}
@@ -1432,7 +1471,7 @@ export class FileTreeComponent implements OnInit {
pathsArray.forEach((path, index) => {
setTimeout(() => {
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path });
+ this.listDirectoryRel(path);
}, index * 50); // 50ms delay between each call
});
}
@@ -1471,10 +1510,16 @@ export class FileTreeComponent implements OnInit {
const fullPath = targetPath === '.' ? file.name : `${targetPath}/${file.name}`;
// Create file with content using createFileOrDirectory
- this.filesFacade.createFileOrDirectory(this.clientId(), this.agentId(), fullPath, {
- type: 'file',
- content: base64Content,
- });
+ this.filesFacade.createFileOrDirectory(
+ this.clientId(),
+ this.agentId(),
+ fullPath,
+ {
+ type: 'file',
+ content: base64Content,
+ },
+ this.fileManagerContext(),
+ );
uploadedCount++;
@@ -1486,19 +1531,21 @@ export class FileTreeComponent implements OnInit {
// Load directory if not cached
const hasCachedData = this.treeCache().has(targetPath);
if (!hasCachedData) {
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: targetPath });
+ this.listDirectoryRel(targetPath);
}
}
// Refresh directory listing
setTimeout(() => {
- this.filesFacade.listDirectory(this.clientId(), this.agentId(), { path: targetPath });
+ this.listDirectoryRel(targetPath);
}, 100);
- // Reload git status after file upload
- setTimeout(() => {
- this.vcsFacade.loadStatus(this.clientId(), this.agentId());
- }, 500);
+ // Reload git status after file upload (workspace app tree only)
+ if (this.fileManagerContext() === 'app') {
+ setTimeout(() => {
+ this.vcsFacade.loadStatus(this.clientId(), this.agentId());
+ }, 500);
+ }
// Select the last uploaded file
if (fileArray.length === 1) {
diff --git a/libs/domains/framework/frontend/feature-agent-console/src/lib/guards/config-editor.guard.spec.ts b/libs/domains/framework/frontend/feature-agent-console/src/lib/guards/config-editor.guard.spec.ts
new file mode 100644
index 00000000..3724a8b1
--- /dev/null
+++ b/libs/domains/framework/frontend/feature-agent-console/src/lib/guards/config-editor.guard.spec.ts
@@ -0,0 +1,103 @@
+import { Injector, runInInjectionContext } from '@angular/core';
+import { TestBed } from '@angular/core/testing';
+import { ActivatedRoute, type ActivatedRouteSnapshot, convertToParamMap, Router, type UrlTree } from '@angular/router';
+// eslint-disable-next-line @nx/enforce-module-boundaries
+import { ClientsFacade } from '../../../../data-access-agent-console/src/lib/state/clients/clients.facade';
+import { firstValueFrom, isObservable, of, type Observable } from 'rxjs';
+import { configEditorGuard } from './config-editor.guard';
+
+describe('configEditorGuard', () => {
+ const mockParentRoute = { path: 'parent' };
+
+ let mockRouter: { createUrlTree: jest.Mock };
+ let mockActivatedRoute: { parent: typeof mockParentRoute };
+ let clientsFacadeStub: {
+ getClientById$: jest.Mock;
+ loadClient: jest.Mock;
+ setActiveClient: jest.Mock;
+ };
+
+ const createInjector = (): Injector => {
+ TestBed.resetTestingModule();
+ TestBed.configureTestingModule({
+ providers: [
+ { provide: ClientsFacade, useValue: clientsFacadeStub },
+ { provide: Router, useValue: mockRouter },
+ { provide: ActivatedRoute, useValue: mockActivatedRoute },
+ ],
+ });
+ return TestBed.inject(Injector);
+ };
+
+ async function runGuard(route: ActivatedRouteSnapshot): Promise {
+ const injector = createInjector();
+ const raw = runInInjectionContext(injector, () => configEditorGuard(route, {} as never));
+ if (isObservable(raw)) {
+ return firstValueFrom(raw as Observable);
+ }
+ return raw as boolean | UrlTree;
+ }
+
+ beforeEach(() => {
+ mockRouter = {
+ createUrlTree: jest.fn(),
+ };
+ mockActivatedRoute = { parent: mockParentRoute };
+ clientsFacadeStub = {
+ getClientById$: jest.fn(),
+ loadClient: jest.fn(),
+ setActiveClient: jest.fn(),
+ };
+ jest.clearAllMocks();
+ });
+
+ it('allows activation when the user can manage workspace configuration', async () => {
+ clientsFacadeStub.getClientById$.mockReturnValue(
+ of({
+ id: 'c1',
+ canManageWorkspaceConfiguration: true,
+ } as never),
+ );
+ const route = {
+ paramMap: convertToParamMap({ clientId: 'c1', agentId: 'a1' }),
+ } as ActivatedRouteSnapshot;
+
+ const result = await runGuard(route);
+
+ expect(clientsFacadeStub.setActiveClient).toHaveBeenCalledWith('c1');
+ expect(clientsFacadeStub.loadClient).toHaveBeenCalledWith('c1');
+ expect(result).toBe(true);
+ expect(mockRouter.createUrlTree).not.toHaveBeenCalled();
+ });
+
+ it('redirects to agent chat when the user cannot manage workspace configuration', async () => {
+ const urlTree = { toString: () => '/clients/c1/agents/a1' } as UrlTree;
+ mockRouter.createUrlTree.mockReturnValue(urlTree);
+ clientsFacadeStub.getClientById$.mockReturnValue(
+ of({
+ id: 'c1',
+ canManageWorkspaceConfiguration: false,
+ } as never),
+ );
+ const route = {
+ paramMap: convertToParamMap({ clientId: 'c1', agentId: 'a1' }),
+ } as ActivatedRouteSnapshot;
+
+ const result = await runGuard(route);
+
+ expect(result).toBe(urlTree);
+ expect(mockRouter.createUrlTree).toHaveBeenCalledWith(['clients', 'c1', 'agents', 'a1']);
+ });
+
+ it('redirects to /clients when client or agent id is missing', async () => {
+ const urlTree = { toString: () => '/clients' } as UrlTree;
+ mockRouter.createUrlTree.mockReturnValue(urlTree);
+ const route = { paramMap: convertToParamMap({ clientId: 'c1' }) } as ActivatedRouteSnapshot;
+
+ const result = await runGuard(route);
+
+ expect(result).toBe(urlTree);
+ expect(mockRouter.createUrlTree).toHaveBeenCalledWith(['clients']);
+ expect(clientsFacadeStub.loadClient).not.toHaveBeenCalled();
+ });
+});
diff --git a/libs/domains/framework/frontend/feature-agent-console/src/lib/guards/config-editor.guard.ts b/libs/domains/framework/frontend/feature-agent-console/src/lib/guards/config-editor.guard.ts
new file mode 100644
index 00000000..58df4541
--- /dev/null
+++ b/libs/domains/framework/frontend/feature-agent-console/src/lib/guards/config-editor.guard.ts
@@ -0,0 +1,32 @@
+import { inject } from '@angular/core';
+import { type ActivatedRouteSnapshot, Router, type CanActivateFn } from '@angular/router';
+// Avoid data-access barrel: it re-exports identity (Keycloak), which breaks lightweight Jest runs.
+// eslint-disable-next-line @nx/enforce-module-boundaries
+import { ClientsFacade } from '../../../../data-access-agent-console/src/lib/state/clients/clients.facade';
+import { filter, map, take } from 'rxjs';
+
+/**
+ * Ensures the user may open the provider config file editor for the workspace in the URL.
+ * Redirects to the agent chat route when `canManageWorkspaceConfiguration` is false.
+ */
+export const configEditorGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => {
+ const clientsFacade = inject(ClientsFacade);
+ const router = inject(Router);
+ const clientId = route.paramMap.get('clientId')?.trim();
+ const agentId = route.paramMap.get('agentId')?.trim();
+
+ if (!clientId || !agentId) {
+ return router.createUrlTree(['clients']);
+ }
+
+ clientsFacade.setActiveClient(clientId);
+ clientsFacade.loadClient(clientId);
+
+ return clientsFacade.getClientById$(clientId).pipe(
+ filter((client) => client !== null),
+ take(1),
+ map((client) =>
+ client!.canManageWorkspaceConfiguration ? true : router.createUrlTree(['clients', clientId, 'agents', agentId]),
+ ),
+ );
+};