Improved the dialog for moving chats into workspaces (#796)
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-06-06 10:06:41 +02:00
1 parent 0b41f5eb96
commit 102b344557
13 files changed
+489 -37

No files matched your search

@@ -12,6 +12,8 @@ namespace AIStudio.Tools;
public static class WorkspaceBehaviour
{
public readonly record struct TryCreateWorkspaceResult(bool Success, WorkspaceTreeWorkspace Workspace);
private sealed class WorkspaceChatCacheEntry
{
public Guid WorkspaceId { get; init; }
@@ -76,9 +78,9 @@ public static class WorkspaceBehaviour
private static readonly TimeSpan PREFETCH_DELAY_DURATION = TimeSpan.FromMilliseconds(45);
private static string WorkspaceRootDirectory => Path.Join(SettingsManager.DataDirectory, "workspaces");
private static readonly string WORKSPACE_ROOT_DIRECTORY = Path.Join(SettingsManager.DataDirectory, "workspaces");
private static string TemporaryChatsRootDirectory => Path.Join(SettingsManager.DataDirectory, "tempChats");
private static readonly string TEMPORARY_CHATS_ROOT_DIRECTORY = Path.Join(SettingsManager.DataDirectory, "tempChats");
private static SemaphoreSlim GetChatSemaphore(Guid workspaceId, Guid chatId)
{
@@ -156,9 +158,9 @@ public static class WorkspaceBehaviour
private static async Task<List<WorkspaceChatCacheEntry>> ReadTemporaryChatsCoreAsync()
{
var chats = new List<WorkspaceChatCacheEntry>();
Directory.CreateDirectory(TemporaryChatsRootDirectory);
Directory.CreateDirectory(TEMPORARY_CHATS_ROOT_DIRECTORY);
foreach (var tempChatPath in Directory.EnumerateDirectories(TemporaryChatsRootDirectory))
foreach (var tempChatPath in Directory.EnumerateDirectories(TEMPORARY_CHATS_ROOT_DIRECTORY))
{
if (!Guid.TryParse(Path.GetFileName(tempChatPath), out var chatId))
continue;
@@ -188,8 +190,8 @@ public static class WorkspaceBehaviour
WORKSPACE_TREE_CACHE.Workspaces.Clear();
WORKSPACE_TREE_CACHE.WorkspaceOrder.Clear();
Directory.CreateDirectory(WorkspaceRootDirectory);
foreach (var workspacePath in Directory.EnumerateDirectories(WorkspaceRootDirectory))
Directory.CreateDirectory(WORKSPACE_ROOT_DIRECTORY);
foreach (var workspacePath in Directory.EnumerateDirectories(WORKSPACE_ROOT_DIRECTORY))
{
if (!Guid.TryParse(Path.GetFileName(workspacePath), out var workspaceId))
continue;
@@ -259,6 +261,13 @@ public static class WorkspaceBehaviour
return false;
}
private static bool WorkspaceNameExistsCore(string workspaceName, Guid excludedWorkspaceId = default)
{
return WORKSPACE_TREE_CACHE.Workspaces.Values.Any(workspace =>
workspace.WorkspaceId != excludedWorkspaceId &&
string.Equals(workspace.WorkspaceName.Trim(), workspaceName, StringComparison.OrdinalIgnoreCase));
}
private static async Task<bool> ThreadContainsTermsAsync(WorkspaceTreeChat chat, IReadOnlyList<string> terms, CancellationToken token)
{
var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(chat.WorkspaceId, chat.ChatId, nameof(ThreadContainsTermsAsync));
@@ -587,6 +596,100 @@ public static class WorkspaceBehaviour
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
}
}
public static string NormalizeWorkspaceName(string workspaceName) => workspaceName.Trim();
public static async Task<bool> IsWorkspaceNameExistingAsync(string workspaceName, Guid excludedWorkspaceId = default)
{
var normalizedWorkspaceName = NormalizeWorkspaceName(workspaceName);
if (string.IsNullOrWhiteSpace(normalizedWorkspaceName))
return false;
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
try
{
await EnsureTreeShellLoadedCoreAsync();
return WorkspaceNameExistsCore(normalizedWorkspaceName, excludedWorkspaceId);
}
finally
{
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
}
}
public static async Task<TryCreateWorkspaceResult> TryCreateWorkspaceAsync(string workspaceName)
{
var normalizedWorkspaceName = NormalizeWorkspaceName(workspaceName);
if (string.IsNullOrWhiteSpace(normalizedWorkspaceName))
return new(false, default);
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
try
{
await EnsureTreeShellLoadedCoreAsync();
if (WorkspaceNameExistsCore(normalizedWorkspaceName))
return new(false, default);
var workspaceId = Guid.NewGuid();
var workspacePath = Path.Join(WORKSPACE_ROOT_DIRECTORY, workspaceId.ToString());
Directory.CreateDirectory(workspacePath);
var workspaceNamePath = Path.Join(workspacePath, "name");
await File.WriteAllTextAsync(workspaceNamePath, normalizedWorkspaceName, Encoding.UTF8);
var workspace = new WorkspaceCacheEntry
{
WorkspaceId = workspaceId,
WorkspacePath = workspacePath,
WorkspaceName = normalizedWorkspaceName,
Chats = [],
ChatsLoaded = false,
};
WORKSPACE_TREE_CACHE.Workspaces[workspaceId] = workspace;
WORKSPACE_TREE_CACHE.WorkspaceOrder.Add(workspaceId);
return new(true, ToPublicWorkspace(workspace));
}
finally
{
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
}
}
public static async Task<bool> RenameWorkspaceAsync(Guid workspaceId, string workspaceName)
{
var normalizedWorkspaceName = NormalizeWorkspaceName(workspaceName);
if (string.IsNullOrWhiteSpace(normalizedWorkspaceName))
return false;
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
try
{
await EnsureTreeShellLoadedCoreAsync();
if (!WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace))
return false;
var workspaceNamePath = Path.Join(workspace.WorkspacePath, "name");
if (string.Equals(workspace.WorkspaceName.Trim(), normalizedWorkspaceName, StringComparison.OrdinalIgnoreCase))
{
await File.WriteAllTextAsync(workspaceNamePath, normalizedWorkspaceName, Encoding.UTF8);
workspace.WorkspaceName = normalizedWorkspaceName;
return true;
}
if (WorkspaceNameExistsCore(normalizedWorkspaceName, workspaceId))
return false;
await File.WriteAllTextAsync(workspaceNamePath, normalizedWorkspaceName, Encoding.UTF8);
workspace.WorkspaceName = normalizedWorkspaceName;
return true;
}
finally
{
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
}
}
public static bool IsChatExisting(LoadChat loadChat)
{
@@ -668,7 +771,7 @@ public static class WorkspaceBehaviour
// Not in cache — read from disk and update cache in the same semaphore scope
// to avoid a second semaphore acquisition via UpdateWorkspaceNameInCacheAsync:
var workspacePath = Path.Join(WorkspaceRootDirectory, workspaceId.ToString());
var workspacePath = Path.Join(WORKSPACE_ROOT_DIRECTORY, workspaceId.ToString());
var workspaceNamePath = Path.Join(workspacePath, "name");
string workspaceName;
@@ -756,7 +859,7 @@ public static class WorkspaceBehaviour
private static async Task EnsureWorkspace(Guid workspaceId, string workspaceName)
{
var workspacePath = Path.Join(WorkspaceRootDirectory, workspaceId.ToString());
var workspacePath = Path.Join(WORKSPACE_ROOT_DIRECTORY, workspaceId.ToString());
var workspaceNamePath = Path.Join(workspacePath, "name");
if (!Path.Exists(workspacePath))
@@ -786,4 +889,4 @@ public static class WorkspaceBehaviour
public static async Task EnsureBiasWorkspace() => await EnsureWorkspace(KnownWorkspaces.BIAS_WORKSPACE_ID, "Bias of the Day");
public static async Task EnsureERIServerWorkspace() => await EnsureWorkspace(KnownWorkspaces.ERI_SERVER_WORKSPACE_ID, "ERI Servers");
}
}