Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,12 +308,28 @@ Multi-level confidence scheme allows users to control which providers see which
6. GitHub Actions builds release binaries for all platforms
7. Binaries uploaded to GitHub Releases

## Localization

The app's texts are localized in two steps, and the developer always does the first one.

1. The developer starts the app, which runs the I18N collector, and runs the localization assistant
in the app for German and US English. Agents never write these initial translations themselves:
they neither add nor regenerate entries in `app/MindWork AI Studio/Assistants/I18N/allTexts.lua`,
`app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua`,
or `app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua`.
When new or changed texts are waiting for translation, remind the developer to start the app and
run the localization.
2. Afterward, agents always review the German translation. Compare the new and changed values of the
de-de `plugin.lua` with `main`, check them against the wording already established there, and
correct or improve them directly in that file. `allTexts.lua` and the en-us `plugin.lua` stay as
the assistant wrote them.

## Important Development Notes

- **File changes require Write/Edit tools** - Never use bash commands like `cat <<EOF` or `echo >`
- **End of file formatting** - Do not append an extra empty line at the end of files.
- **No automated formatting for Rust or .NET files** - Never run automated formatters on Rust files (`.rs`) or .NET files (`.cs`, `.razor`, `.csproj`, etc.). Only make the minimal manual formatting changes required for the specific edit.
- **I18N resources are generated** - Do not manually edit `app/MindWork AI Studio/Assistants/I18N/allTexts.lua`, `app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua`, or `app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua`. These files are updated automatically by the I18N process.
- **I18N resources are generated** - The developer produces the translations by running the localization assistant in the app; agents only review and correct the German values afterward. See "Localization" above.
- **Spaces in paths** - Always quote paths with spaces in bash commands
- **Agent-run builds** - Never start `.NET` or Rust builds in the agent's own shell; it is sandboxed. Use the `rider` and `rustrover` MCP servers instead, which build in the IDE outside that sandbox. See "Running builds from an agent" above.
- **Debug environment** - Reads `startup.env` file with IPC credentials
Expand Down
45 changes: 45 additions & 0 deletions app/MindWork AI Studio/Assistants/I18N/allTexts.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3268,6 +3268,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347088452"] = "Result"
-- Do you really want to remove this message?
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Do you really want to remove this message?"

-- Do you really want to roll back this chat to this AI response? All later messages and their attachments will be permanently removed.
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347725178"] = "Do you really want to roll back this chat to this AI response? All later messages and their attachments will be permanently removed."

-- Yes, remove the AI response and edit it
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Yes, remove the AI response and edit it"

Expand Down Expand Up @@ -3319,12 +3322,18 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2822776450"] = "Export
-- Number of attachments
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Number of attachments"

-- Roll back to this response
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3132525321"] = "Roll back to this response"

-- Cannot render content of type {0} yet.
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Cannot render content of type {0} yet."

-- Edit
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3267849393"] = "Edit"

-- Roll Back Chat
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3304283125"] = "Roll Back Chat"

-- Unknown
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3424652889"] = "Unknown"

Expand All @@ -3337,6 +3346,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3816336467"] = "Blocked
-- Do you really want to regenerate this message?
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3878878761"] = "Do you really want to regenerate this message?"

-- Yes, roll back the chat
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3951371697"] = "Yes, roll back the chat"

-- Remove Message
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove Message"

Expand Down Expand Up @@ -3610,6 +3622,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2377171085"] = "Italic"
-- The media transcription was canceled.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T241403726"] = "The media transcription was canceled."

-- Copy this chat & continue in the copy.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2634509198"] = "Copy this chat & continue in the copy."

-- Profile usage is disabled according to your chat template settings.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2670286472"] = "Profile usage is disabled according to your chat template settings."

Expand Down Expand Up @@ -5296,9 +5311,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T588743762"] = "An error o
-- The transcription result is empty.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T974954792"] = "The transcription result is empty."

-- Do you want to copy this chat? Your unsaved changes move into the copy, and the original chat keeps the state it was last saved with.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1047391993"] = "Do you want to copy this chat? Your unsaved changes move into the copy, and the original chat keeps the state it was last saved with."

-- Move chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1133040906"] = "Move chat"

-- Copy Chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1192756314"] = "Copy Chat"

-- Loading chats...
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1364857726"] = "Loading chats..."

Expand Down Expand Up @@ -5365,6 +5386,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3249036008"] = "There is alr
-- Please enter a workspace name.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Please enter a workspace name."

-- Copy chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3337233722"] = "Copy chat"

-- Rename
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Rename"

Expand All @@ -5380,6 +5404,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3555709365"] = "Load Chat"
-- Add Workspace
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3672981145"] = "Add Workspace"

-- Do you want to copy this chat? The copy is opened afterwards, so all unsaved changes of the chat you have open right now will be lost.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3699436634"] = "Do you want to copy this chat? The copy is opened afterwards, so all unsaved changes of the chat you have open right now will be lost."

-- Chat Name
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3891063690"] = "Chat Name"

Expand Down Expand Up @@ -12949,14 +12976,32 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T818893091"] =
-- Are you sure you want to delete the chat '{0}' in the workspace '{1}'?
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1016188706"] = "Are you sure you want to delete the chat '{0}' in the workspace '{1}'?"

-- Copy Chat
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1192756314"] = "Copy Chat"

-- Unnamed workspace
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unnamed workspace"

-- Copy
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1703884388"] = "Copy"

-- Delete Chat
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Delete Chat"

-- Please enter a chat name.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2301651387"] = "Please enter a chat name."

-- Are you sure you want to delete the temporary chat '{0}'?
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3043761007"] = "Are you sure you want to delete the temporary chat '{0}'?"

-- Unnamed chat
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3310482275"] = "Unnamed chat"

-- Please enter a name for the copy of your chat '{0}':
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3323676840"] = "Please enter a name for the copy of your chat '{0}':"

-- Copy of {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3365678931"] = "Copy of {0}"

-- Chat Name
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3891063690"] = "Chat Name"
42 changes: 42 additions & 0 deletions app/MindWork AI Studio/Chat/ChatThread.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,48 @@ public void Remove(IContent content, bool removeForRegenerate = false)
this.Blocks.Remove(block);
}

/// <summary>
/// Rolls this chat thread back, so that the given content becomes the last block of the conversation.
/// </summary>
/// <remarks>
/// Every later block is removed in conversation order, which is the order of the time stamps and
/// the order in which the chat shows the blocks. That includes the blocks hidden from the user,
/// such as the prompts an assistant sends into a chat: the user can neither see nor remove them,
/// so leaving them behind would continue the chat with messages nobody knows about. Hidden blocks
/// before the content stay, e.g. the example conversation of a chat template. The managed
/// transcripts of the removed blocks are deleted with them.<br/><br/>
///
/// The augmented data and the AI-selected data sources are reset, too. Both describe the last
/// retrieval, not a certain message, so after a rollback nobody knows whether they belong to a
/// kept or to a removed one. The next message with active data sources retrieves anew; without
/// active data sources, the chat continues without this context. The data source options stay,
/// because they are the user's choice rather than the result of a message.<br/><br/>
///
/// What stays as well is everything the thread ratchets for security reasons, namely the data
/// security and the required provider confidence. Both only ever tighten, because the data which
/// raised them was seen by this thread. Removing the message that brought it in does not unsee
/// it, so the chat keeps demanding the same of every provider which continues it.
/// </remarks>
/// <param name="content">The content to keep as the last block.</param>
/// <returns>True when one or more later blocks were removed. False when the content is unknown or already the last block; the thread stays unchanged then.</returns>
public bool RollBackTo(IContent content)
{
var sortedBlocks = this.Blocks.OrderBy(x => x.Time).ToList();
var blockIndex = sortedBlocks.FindIndex(block => ReferenceEquals(block.Content, content));
if (blockIndex < 0 || blockIndex == sortedBlocks.Count - 1)
return false;

foreach (var block in sortedBlocks.Skip(blockIndex + 1))
{
DeleteManagedAttachments(block);
this.Blocks.Remove(block);
}

this.AugmentedData = string.Empty;
this.AISelectedDataSources = [];
return true;
}

private static void DeleteManagedAttachments(ContentBlock block)
{
if (block.Content is not ContentText textContent)
Expand Down
6 changes: 6 additions & 0 deletions app/MindWork AI Studio/Chat/ContentBlockComponent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" Disabled="@(!this.RegenerateEnabled())" OnClick="@this.RegenerateBlock"/>
</MudTooltip>
}
@if (!this.IsLastContentBlock && this.Role is ChatRole.AI && this.RollbackFunc is not null)
{
<MudTooltip Text="@T("Roll back to this response")" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Restore" Color="Color.Default" Disabled="@(!this.RollbackEnabled())" OnClick="@this.RollbackBlock"/>
</MudTooltip>
}
@if (this.RemoveBlockFunc is not null)
{
<MudTooltip Text="@T("Removes this block")" Placement="Placement.Bottom">
Expand Down
21 changes: 21 additions & 0 deletions app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ public partial class ContentBlockComponent : MSGComponentBase

[Parameter]
public Func<IContent, Task>? RegenerateFunc { get; set; }

[Parameter]
public Func<IContent, Task>? RollbackFunc { get; set; }

[Parameter]
public Func<IContent, Task>? EditLastBlockFunc { get; set; }
Expand All @@ -86,6 +89,9 @@ public partial class ContentBlockComponent : MSGComponentBase
[Parameter]
public Func<bool> RegenerateEnabled { get; set; } = () => false;

[Parameter]
public Func<bool> RollbackEnabled { get; set; } = () => false;

/// <summary>
/// What the export offers, used both as the label of the export button and as the title of
/// the save dialog.
Expand Down Expand Up @@ -780,6 +786,21 @@ private async Task RegenerateBlock()
if (regenerate.HasValue && regenerate.Value)
await this.RegenerateFunc(this.Content);
}

private async Task RollbackBlock()
{
if (this.RollbackFunc is null || this.Role is not ChatRole.AI || !this.RollbackEnabled())
return;

var rollback = await this.DialogService.ShowMessageBox(
T("Roll Back Chat"),
T("Do you really want to roll back this chat to this AI response? All later messages and their attachments will be permanently removed."),
T("Yes, roll back the chat"),
T("No, keep it"));

if (rollback.HasValue && rollback.Value)
await this.RollbackFunc(this.Content);
}

private async Task EditLastBlock()
{
Expand Down
6 changes: 6 additions & 0 deletions app/MindWork AI Studio/Components/ChatComponent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
IsSecondToLastBlock="@isSecondLastBlock"
RegenerateFunc="@this.RegenerateBlock"
RegenerateEnabled="@(() => this.IsProviderSelected && this.ChatThread.IsLLMProviderAllowed(this.Provider))"
RollbackFunc="@this.RollbackBlock"
RollbackEnabled="@(() => !this.IsCurrentChatStreaming)"
EditLastBlockFunc="@this.EditLastBlock"
EditLastUserBlockFunc="@this.EditLastUserBlock"/>
}
Expand Down Expand Up @@ -101,6 +103,10 @@
<MudTooltip Text="@T("Move the chat to a workspace, or to another if it is already in one.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved || this.IsCurrentChatStreaming)" OnClick="@this.MoveChatToWorkspace"/>
</MudTooltip>

<MudTooltip Text="@T("Copy this chat & continue in the copy.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Disabled="@(!this.CanThreadBeCopied)" OnClick="@this.CopyCurrentChat"/>
</MudTooltip>
}

<AttachDocuments Name="File Attachments" DocumentPaths="@this.ComposerState.FileAttachments" DocumentPathsChanged="@this.ComposerAttachmentsChanged" CatchAllDocuments="true" UseSmallForm="true" ShowMediaStatus="false" Provider="@this.Provider" OwnerChat="@this.ChatThread" EnsureOwnerChatAsync="@this.EnsureMediaImportChatAsync" Disabled="@this.MediaTranscriptionService.IsBusy(this.CurrentMediaImportOwner)"/>
Expand Down
65 changes: 63 additions & 2 deletions app/MindWork AI Studio/Components/ChatComponent.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,9 @@ private string InputLabel
}

private bool CanThreadBeSaved => this.ChatThread is not null && this.ChatThread.Blocks.Any(b => !b.HideFromUser);


private bool CanThreadBeCopied => this.CanThreadBeSaved && !this.IsCurrentChatStreaming && !this.MediaTranscriptionService.IsBusy(this.CurrentMediaImportOwner);

private string TooltipAddChatToWorkspace => string.Format(T("Start new chat in workspace '{0}'"), this.currentWorkspaceName);

private string UserInputStyle => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty;
Expand Down Expand Up @@ -1344,7 +1346,48 @@ private async Task MoveChatToWorkspace()

await this.SyncWorkspaceHeaderWithChatThreadAsync();
}


/// <summary>
/// Copies the open chat and continues in the copy.
/// </summary>
/// <remarks>
/// Copied is the chat as it stands on the screen, not the state which was stored last. That is
/// why nothing is asked about unsaved changes here, unlike everywhere else a chat is left
/// behind: none of them are lost, they move into the copy, while the original keeps what was
/// stored.<br/><br/>
///
/// The same holds for the message being written. The copy is marked as loaded right away,
/// because the parameter update which follows would otherwise take it for another chat being
/// opened and clear the composer. Only the transcripts attached to that message are exchanged:
/// those of the original live in its directory, and the copy got copies of its own.
/// </remarks>
private async Task CopyCurrentChat()
{
if (this.ChatThread is null || !this.CanThreadBeCopied)
return;

var sourceChat = this.ChatThread;
var copy = this.Workspaces is null
? await WorkspaceBehaviour.CopyChatAsync(this.DialogService, sourceChat)
: await this.Workspaces.CopyChatAsync(sourceChat);

if (copy is null)
return;

var transcriptsOfTheOriginal = sourceChat.PendingMediaTranscripts.Select(transcript => transcript.FilePath).ToHashSet(StringComparer.Ordinal);
this.ComposerState.FileAttachments.RemoveWhere(attachment => transcriptsOfTheOriginal.Contains(attachment.FilePath));
foreach (var transcript in copy.PendingMediaTranscripts)
this.ComposerState.FileAttachments.Add(transcript);

this.ChatThread = copy;
this.hasUnsavedChanges = false;

await this.SyncForegroundChatAsync();
this.MarkCurrentChatAsLoadedParameter();
await this.SyncWorkspaceHeaderWithChatThreadAsync();
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
}

private async Task LoadedChatChanged(bool notifyParent = true)
{
this.hasUnsavedChanges = false;
Expand Down Expand Up @@ -1457,6 +1500,24 @@ private async Task RegenerateBlock(IContent aiBlock)

await this.SendMessage(reuseLastUserPrompt: true);
}

private async Task RollbackBlock(IContent aiBlock)
{
if (this.ChatThread is null || this.IsCurrentChatStreaming)
return;

// Which parts of the thread a rollback resets and which it keeps is documented at RollBackTo:
if (!this.ChatThread.RollBackTo(aiBlock))
return;

// The rollback reset the AI-selected data sources, which the data source selection still shows:
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
this.hasUnsavedChanges = true;
await this.SaveThread();
this.tokenTracker?.Nudge();
this.StateHasChanged();
await this.inputField.FocusAsync();
}

private Task EditLastUserBlock(IContent block)
{
Expand Down
Loading
Loading