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
1 change: 1 addition & 0 deletions src/MandoCode.Desktop.Tests/ResponseStreamerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public event Action<FunctionExecutionResult>? OnFunctionCompleted { add { } remo
public void AppendUserNote(string text) => throw new NotSupportedException();
public int TryRestoreHistoryJson(string json) => throw new NotSupportedException();
public Task EnterLearnModeAsync() => throw new NotSupportedException();
public Task<bool> CompactHistoryAsync() => throw new NotSupportedException();
public Task ClearHistoryAsync() => throw new NotSupportedException();
public Task<IReadOnlyList<ChatMessage>> GetHistoryAsync() => throw new NotSupportedException();
}
Expand Down
67 changes: 67 additions & 0 deletions src/MandoCode.Desktop/Assets/web/transcript/transcript.css
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,73 @@
.red { color: var(--red); }
.token-summary { text-align: right; font-size: 12px; }

/* Live plan status is operationally important, not ordinary transcript narration. Give it a
high-opacity themed surface so a bright or busy chat wallpaper cannot wash out the active
step. The panel/border/accent variables keep it native to every appearance; backdrop blur
preserves contrast while still letting a little of the selected image show through. */
.plan-activity { display: flex; align-items: flex-start; gap: 10px;
max-width: 720px; padding: 10px 12px; border: 1px solid var(--border);
border-left: 3px solid var(--accent); border-radius: 10px;
background: var(--panel);
background: color-mix(in srgb, var(--panel) 82%, transparent);
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
box-shadow: 0 5px 18px rgba(0, 0, 0, 0.20); }
.plan-activity-dot { flex: 0 0 auto; width: 8px; height: 8px; margin-top: 6px;
border-radius: 50%; background: var(--accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
.plan-activity-title { color: var(--fg); font-weight: 700; line-height: 1.25; }
.plan-activity-emoji { display: inline-block; margin-right: 6px; font-family: "Segoe UI Emoji", "Segoe UI Symbol", sans-serif; }
.plan-activity-detail { color: var(--fg); margin-top: 2px; font-size: 13px; line-height: 1.35; }
.plan-activity.success { border-left-color: var(--green); }
.plan-activity.success .plan-activity-dot { background: var(--green);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 22%, transparent); }
.plan-activity.warning { border-left-color: var(--gold); }
.plan-activity.warning .plan-activity-dot { background: var(--gold);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 22%, transparent); }
.plan-activity.error { border-left-color: var(--red); }
.plan-activity.error .plan-activity-dot { background: var(--red);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 22%, transparent); }
.checkpoint-card { background: var(--panel);
background: color-mix(in srgb, var(--panel) 82%, transparent);
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
box-shadow: 0 5px 18px rgba(0, 0, 0, 0.20); }
.assistant.plan-step-result { background: var(--panel);
background: color-mix(in srgb, var(--panel) 82%, transparent);
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
border: 1px solid var(--border); border-radius: 10px; padding: 6px 12px 8px 12px;
box-shadow: 0 5px 18px rgba(0, 0, 0, 0.20); }

html[data-win98] .plan-activity { background: var(--bg); color: #000; border-radius: 0;
border: 2px solid; border-left: 2px solid;
border-color: #FFFFFF #404040 #404040 #FFFFFF; box-shadow: none; backdrop-filter: none; }
html[data-win98] .plan-activity-title, html[data-win98] .plan-activity-detail { color: #000; }
html[data-win98] .plan-activity-dot { background: #1084D0; box-shadow: none; }
html[data-win98] .assistant.plan-step-result { border-radius: 0; box-shadow: none;
backdrop-filter: none; padding: 0; }
html[data-win98] .checkpoint-card { background: var(--bg); box-shadow: none;
backdrop-filter: none; }

/* System notices use a smaller sibling of the plan-activity card. They replace the old bare
colored/dim lines, which disappear against bright wallpapers at high image opacity. Pills,
command panels, and normal messages keep their existing presentation. */
.notice-card { display: flex; align-items: flex-start; gap: 8px; max-width: 720px;
padding: 7px 10px; border: 1px solid var(--border); border-left: 3px solid var(--sky);
border-radius: 8px; background: var(--panel);
background: color-mix(in srgb, var(--panel) 82%, transparent);
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); }
.notice-emoji { flex: 0 0 auto; margin-top: 1px;
font-family: "Segoe UI Emoji", "Segoe UI Symbol", sans-serif; }
.notice-text { color: var(--fg); white-space: pre-wrap; line-height: 1.4; }
.notice-card.success { border-left-color: var(--green); }
.notice-card.warn { border-left-color: var(--gold); }
.notice-card.error { border-left-color: var(--red); }
.notice-card.dim { border-left-color: var(--border); }
html[data-win98] .notice-card { background: var(--bg); border-radius: 0; box-shadow: none;
backdrop-filter: none; border: 2px solid; border-left: 2px solid;
border-color: #FFFFFF #404040 #404040 #FFFFFF; }
html[data-win98] .notice-text { color: #000; }

/* Status chips — compact pills for session/connection state. A CSS status dot
(crisp, theme-aware) replaces status emoji; state = ok | warn | err | neutral. */
/* Centered to match the tool pills: all system/status chrome sits centered, conversation stays left. */
Expand Down
1 change: 1 addition & 0 deletions src/MandoCode.Desktop/Controls/ChatTabView.Header.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public sealed partial class ChatTabView

public void UpdateHeader()
{
InputBox.PlaceholderText = $"Message Agent {Session.Title}… /commands · @file · !shell";
ModelText.Text = _controller.ModelName;
ProjectRootText.Text = _controller.ProjectRootPath;
ConnectionDot.Fill = new SolidColorBrush(
Expand Down
11 changes: 8 additions & 3 deletions src/MandoCode.Desktop/Controls/ChatTabView.Transcript.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ private async Task RestoreJournaledTranscriptAsync()
// called by MainWindow after any saved model is re-selected — a model switch clears
// history, so restoring memory here would risk it being wiped moments later).
_replayedBlockCount = blocks.Count;
await AppendRawAsync(_html.Dim("— restored from your previous session —"));
await AppendRawAsync(_html.StatusCard(
"Previous session restored",
"This transcript was restored from your previous session.",
"success"));
}
catch { /* a failed replay must never block a fresh conversation */ }
}
Expand All @@ -79,8 +82,10 @@ public async Task RestoreConversationMemoryAsync()
var restored = await Task.Run(() => Session.Ai.TryRestoreHistoryJson(historyJson));
if (restored > 0)
{
await AppendRawAsync(_html.Dim(
$"Conversation memory restored — the agent remembers this session ({restored} messages)."));
await AppendRawAsync(_html.StatusCard(
"Conversation memory restored",
$"The agent remembers this session ({restored} messages).",
"success"));
return;
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/MandoCode.Desktop/Controls/ChatTabView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@
<FontIcon Glyph="&#xE8B7;" FontSize="15"/>
</Button>
<Button x:Name="ExplorerButton" Click="ExplorerButton_Click" Padding="8,5"
ToolTipService.ToolTip="Show or hide the file explorer">
<FontIcon Glyph="&#xEC50;" FontSize="15"/>
ToolTipService.ToolTip="Show or hide the files list">
<FontIcon Glyph="&#xE8FD;" FontSize="15"/>
</Button>
</StackPanel>
</Grid>
Expand Down Expand Up @@ -175,7 +175,7 @@
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<FontIcon Glyph="&#xEC50;" FontSize="14" VerticalAlignment="Center"
<FontIcon Glyph="&#xE8FD;" FontSize="14" VerticalAlignment="Center"
Foreground="{StaticResource MandoAccentBrush}"/>
<TextBlock x:Name="ExplorerRootText" Grid.Column="1" FontWeight="SemiBold"
FontSize="13" VerticalAlignment="Center"
Expand Down Expand Up @@ -575,7 +575,7 @@
</Border>

<TextBox x:Name="InputBox" Grid.Row="1" Grid.Column="0"
PlaceholderText="Message MandoCode… /commands · @file · !shell"
PlaceholderText="Message Agent… /commands · @file · !shell"
AcceptsReturn="True" TextWrapping="Wrap" MaxHeight="140"
AllowDrop="True" DragOver="InputBox_DragOver" Drop="InputBox_Drop"
PreviewKeyDown="InputBox_PreviewKeyDown" TextChanged="InputBox_TextChanged"/>
Expand Down
7 changes: 7 additions & 0 deletions src/MandoCode.Desktop/Controls/ChatTabView.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public ChatTabView(Window owner, AgentSession session, TranscriptHtmlBuilder htm
_transcript.BlockAdded += OnTranscriptBlock;
_transcript.Cleared += OnTranscriptCleared;
Session.Busy.Changed += OnBusyChanged;
Session.TitleChanged += OnAgentTitleChanged;

_controller.StateChanged += OnControllerStateChanged;
_controller.PlanProgressChanged += OnPlanProgress;
Expand All @@ -119,6 +120,7 @@ public ChatTabView(Window owner, AgentSession session, TranscriptHtmlBuilder htm
_controller.ClipboardCopyRequested += OnClipboardCopy;
_controller.ExitRequested += OnExitRequested;
_controller.SnapshotOfferChanged += OnSnapshotOfferChanged;
_controller.HistoryCompacted += OnHistoryCompacted;

UpdateHeader();
}
Expand All @@ -127,12 +129,15 @@ public ChatTabView(Window owner, AgentSession session, TranscriptHtmlBuilder htm
private void OnTranscriptBlock(string html) => OnUi(() => AppendHtml(html));
private void OnTranscriptCleared() => OnUi(ClearTranscript);
private void OnBusyChanged(bool busy, string? activity) => OnUi(() => UpdateBusy(busy, activity));
private void OnAgentTitleChanged(string _) => OnUi(UpdateHeader);
private void OnControllerStateChanged() => OnUi(UpdateHeader);
private void OnPlanProgress(int done, int total, bool active) => OnUi(() => UpdatePlanProgress(done, total, active));
private void OnSetupNeeded() => OnUi(() => SetupRequested?.Invoke());
private void OnMcpEditorRequested(string? name) => OnUi(() => McpEditorRequested?.Invoke(name));
private void OnClipboardCopy(string text) => OnUi(() => ClipboardCopyRequested?.Invoke(text));
private void OnExitRequested() => OnUi(() => ExitRequested?.Invoke());
private void OnHistoryCompacted() => _ = Task.Run(() =>
SessionHistoryStore.Save(Session.PersistKey, Session.Ai.ExportHistoryJson()));
private void OnSnapshotOfferChanged() => OnUi(RefreshSnapshotOffer);

private void OnUi(Action action)
Expand Down Expand Up @@ -329,13 +334,15 @@ public void Shutdown()
_transcript.BlockAdded -= OnTranscriptBlock;
_transcript.Cleared -= OnTranscriptCleared;
Session.Busy.Changed -= OnBusyChanged;
Session.TitleChanged -= OnAgentTitleChanged;
_controller.StateChanged -= OnControllerStateChanged;
_controller.PlanProgressChanged -= OnPlanProgress;
_controller.SetupNeeded -= OnSetupNeeded;
_controller.McpEditorRequested -= OnMcpEditorRequested;
_controller.ClipboardCopyRequested -= OnClipboardCopy;
_controller.ExitRequested -= OnExitRequested;
_controller.SnapshotOfferChanged -= OnSnapshotOfferChanged;
_controller.HistoryCompacted -= OnHistoryCompacted;

_controller.CancelActiveRequest();

Expand Down
12 changes: 10 additions & 2 deletions src/MandoCode.Desktop/Services/AgentSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,19 @@ public sealed class AgentSession
public string Title
{
get => _title;
set { _title = value; Config.AgentName = value; }
set
{
if (_title == value) return;
_title = value;
Config.AgentName = value;
TitleChanged?.Invoke(value);
}
}
private string _title = "";

/// <summary>Raised when the user renames this tab's agent.</summary>
public event Action<string>? TitleChanged;

/// <summary>Durable identity across app launches (unlike <see cref="Id"/>, a process-local
/// counter). Names this session's transcript journal on disk; a restored tab passes its
/// saved key back in so it reattaches to its own history.</summary>
Expand Down Expand Up @@ -108,7 +117,6 @@ public AgentSession(
// agents working in the same project from overwriting each other's unfinished plans.
PlanRunners = new PlanRunnerSelector(
Config,
Planner,
new AiServicePlanStepExecutor(Ai),
PlanHandoff,
ProjectRoot,
Expand Down
1 change: 1 addition & 0 deletions src/MandoCode.Desktop/Services/AiServiceAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public Task<GeneratedPlan> GeneratePlanAsync(string request, string? revisionCon
public void AppendUserNote(string text) => _ai.AppendUserNote(text);
public int TryRestoreHistoryJson(string json) => _ai.TryRestoreHistoryJson(json);
public Task EnterLearnModeAsync() => _ai.EnterLearnModeAsync();
public Task<bool> CompactHistoryAsync() => _ai.CompactHistoryAsync();
public Task ClearHistoryAsync() => _ai.ClearHistoryAsync();
public Task<IReadOnlyList<ChatMessage>> GetHistoryAsync() => _ai.GetHistoryAsync();
}
1 change: 1 addition & 0 deletions src/MandoCode.Desktop/Services/IAiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ void SetRequestContext(string? request) { }
void AppendUserNote(string text);
int TryRestoreHistoryJson(string json);
Task EnterLearnModeAsync();
Task<bool> CompactHistoryAsync();
Task ClearHistoryAsync();
Task<IReadOnlyList<ChatMessage>> GetHistoryAsync();
}
67 changes: 61 additions & 6 deletions src/MandoCode.Desktop/Services/TranscriptHtmlBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,28 @@ public string UserEcho(string text) =>
public string AssistantCard(string markdown, string? speaker = null) =>
$"<div class=\"assistant\"><div class=\"assistant-label\">{E(speaker ?? "MandoCode")}</div><div class=\"md\">{FromMarkdown(markdown)}</div></div>";

public string Info(string text) => $"<div class=\"line info\">{E(text)}</div>";
public string Success(string text) => $"<div class=\"line success\">{E(text)}</div>";
public string Warn(string text) => $"<div class=\"line warn\">{E(text)}</div>";
public string Error(string text) => $"<div class=\"line error\">{E(text)}</div>";
public string Dim(string text) => $"<div class=\"line dim\">{E(text)}</div>";
/// <summary>Step results are part of the live-plan surface, so they retain a solid readable
/// panel even when ordinary transcript messages are configured as flat.</summary>
public string PlanStepResult(string markdown, string? speaker = null) =>
$"<div class=\"assistant plan-step-result\"><div class=\"assistant-label\">{E(speaker ?? "MandoCode")}</div><div class=\"md\">{FromMarkdown(markdown)}</div></div>";

// System notices need their own surface. A theme can intentionally place a high-opacity image
// behind a flat transcript, so color alone is not enough contrast for actions, warnings, or
// recovery guidance. Keep ordinary user/assistant messages untouched; only these notices use
// the compact card treatment.
public string Info(string text) => NoticeCard(text, "info");
public string Success(string text) => NoticeCard(text, "success");
public string Warn(string text) => NoticeCard(text, "warn");
public string Error(string text) => NoticeCard(text, "error");
public string Dim(string text) => NoticeCard(text, "dim");

/// <summary>
/// A high-contrast, live plan-status card. Plan execution and step headers must remain readable
/// even when a user has turned their chat wallpaper opacity up, so they get a themed surface
/// rather than being painted directly on the transcript background.
/// </summary>
public string PlanStarted(int totalSteps) =>
PlanActivity("Executing plan", $"Preparing {totalSteps} step{(totalSteps == 1 ? "" : "s")}");

/// <summary>True for blocks that describe LIVE session state (status chips: connection,
/// model ready, MCP counts, pending offers) rather than conversation history. Session
Expand Down Expand Up @@ -93,6 +110,18 @@ public string StatusChip(string primary, string? secondary = null, string state
return sb.ToString();
}

private static string NoticeCard(string text, string state) =>
$"<div class=\"notice-card {state}\"><span class=\"notice-emoji\">{NoticeEmoji(state)}</span>" +
$"<span class=\"notice-text\">{E(text)}</span></div>";

private static string NoticeEmoji(string state) => state switch
{
"success" => "✅",
"warn" => "⚠️",
"error" => "❌",
_ => "ℹ️",
};

/// <summary>
/// A STATIC tool-call pill (no animation — draws once, costs nothing). Replaces the plain
/// "[Function] …" / "[Done] ✓" text lines for non-file tools with something that reads as a
Expand Down Expand Up @@ -262,7 +291,33 @@ public string OperationCard(OperationDisplayEvent op)
public string CheckpointCard(PlanRunState saved) => CheckpointCardHtml.Build(saved);

public string StepStarted(int current, int total, string description) =>
$"<div class=\"line\"><span class=\"sky\">Step {current}/{total}:</span> {E(description)}</div>";
PlanActivity($"Step {current}/{total}", description);

public string StepCompleted(int current, int total) =>
PlanActivity($"Step {current}/{total} complete", "Moving to the next step", "success");

public string StepFailed(int current, int total, string message) =>
PlanActivity($"Step {current}/{total} needs attention", message, "error");

public string PlanFinished(string title, string detail, string state) =>
PlanActivity(title, detail, state);

/// <summary>A high-contrast, themed status surface for important system notices.</summary>
public string StatusCard(string title, string detail, string state = "") =>
PlanActivity(title, detail, state);

private static string PlanActivity(string title, string detail, string state = "") =>
$"<div class=\"plan-activity {state}\"><span class=\"plan-activity-dot\"></span>" +
$"<div><div class=\"plan-activity-title\"><span class=\"plan-activity-emoji\">{ActivityEmoji(state)}</span>{E(title)}</div>" +
$"<div class=\"plan-activity-detail\">{E(detail)}</div></div></div>";

private static string ActivityEmoji(string state) => state switch
{
"success" => "✅",
"warning" => "⚠️",
"error" => "❌",
_ => "▶️",
};

public string TokenSummary(string text) =>
$"<div class=\"line dim token-summary\">{E(text)}</div>";
Expand Down
Loading
Loading