+ @if (PlatformHelper.IsDesktop && (settings.Mode == ConnectionMode.Embedded || (settings.Mode == ConnectionMode.Persistent && serverAlive)))
+ {
+
+
Share via DevTunnel
@if (!devTunnelAvailable)
{
@@ -139,12 +149,11 @@
}
}
- }
@if (settings.Mode == ConnectionMode.Remote)
{
-
Remote Server
+
Remote Server
Connect to a Copilot server running on another machine (via DevTunnel URL).
@@ -212,6 +221,12 @@
}
+
+ @if (!string.IsNullOrEmpty(statusMessage))
+ {
+
@(statusClass == "success" ? "✓ " : statusClass == "error" ? "✗ " : "")@statusMessage
+ }
+ }
@code {
@@ -225,6 +240,25 @@
private bool tunnelBusy;
private bool showToken;
private string? qrCodeDataUri;
+ private CancellationTokenSource? _statusCts;
+
+ private void ShowStatus(string message, string cls, int dismissMs = 3000)
+ {
+ _statusCts?.Cancel();
+ statusMessage = message;
+ statusClass = cls;
+ StateHasChanged();
+ if (dismissMs > 0)
+ {
+ _statusCts = new CancellationTokenSource();
+ var token = _statusCts.Token;
+ _ = Task.Delay(dismissMs, token).ContinueWith(_ =>
+ {
+ if (!token.IsCancellationRequested)
+ InvokeAsync(() => { statusMessage = null; StateHasChanged(); });
+ }, TaskScheduler.Default);
+ }
+ }
protected override async Task OnInitializedAsync()
{
@@ -335,9 +369,7 @@
if (!string.IsNullOrEmpty(url))
settings.RemoteUrl = url;
- statusMessage = "QR code scanned!";
- statusClass = "success";
- StateHasChanged();
+ ShowStatus("QR code scanned!", "success");
}
private async Task TunnelLogin()
@@ -372,9 +404,7 @@
if (DevTunnelService.TunnelUrl != null)
{
await Microsoft.Maui.ApplicationModel.DataTransfer.Clipboard.SetTextAsync(DevTunnelService.TunnelUrl);
- statusMessage = "URL copied!";
- statusClass = "success";
- StateHasChanged();
+ ShowStatus("URL copied!", "success");
}
}
@@ -383,9 +413,7 @@
if (DevTunnelService.AccessToken != null)
{
await Microsoft.Maui.ApplicationModel.DataTransfer.Clipboard.SetTextAsync(DevTunnelService.AccessToken);
- statusMessage = "Token copied!";
- statusClass = "success";
- StateHasChanged();
+ ShowStatus("Token copied!", "success");
}
}
@@ -398,12 +426,8 @@
serverAlive = success;
starting = false;
- if (success)
- statusMessage = $"Server started on port {settings.Port}";
- else
- statusMessage = "Failed to start server";
- statusClass = success ? "success" : "error";
- StateHasChanged();
+ ShowStatus(success ? $"Server started on port {settings.Port}" : "Failed to start server",
+ success ? "success" : "error");
}
private void StopServer()
@@ -412,45 +436,35 @@
DevTunnelService.Stop();
ServerManager.StopServer();
serverAlive = false;
- statusMessage = "Server stopped";
- statusClass = "";
- StateHasChanged();
+ ShowStatus("Server stopped", "success");
}
private async Task SaveAndApply()
{
if (settings.Mode == ConnectionMode.Persistent && !serverAlive)
{
- statusMessage = "Start the persistent server first";
- statusClass = "error";
- StateHasChanged();
+ ShowStatus("Start the persistent server first", "error", 5000);
return;
}
if (settings.Mode == ConnectionMode.Remote && string.IsNullOrWhiteSpace(settings.RemoteUrl))
{
- statusMessage = "Enter a remote server URL";
- statusClass = "error";
- StateHasChanged();
+ ShowStatus("Enter a remote server URL", "error", 5000);
return;
}
settings.Save();
- statusMessage = "Settings saved. Reconnecting...";
- statusClass = "";
- StateHasChanged();
+ ShowStatus("Settings saved. Reconnecting...", "", 0);
try
{
await CopilotService.ReconnectAsync(settings);
- statusMessage = "Connected!";
- statusClass = "success";
+ ShowStatus("Connected!", "success");
+ Nav.NavigateTo("/");
}
catch (Exception ex)
{
- statusMessage = $"Connection failed: {ex.Message}";
- statusClass = "error";
+ ShowStatus($"Connection failed: {ex.Message}", "error", 8000);
}
- StateHasChanged();
}
}
diff --git a/Components/Pages/Settings.razor.css b/Components/Pages/Settings.razor.css
index 543b47855b..2d9b952ae7 100644
--- a/Components/Pages/Settings.razor.css
+++ b/Components/Pages/Settings.razor.css
@@ -7,52 +7,61 @@
}
.settings-header h2 {
- margin: 0 0 1.5rem 0;
- font-size: 1.6rem;
+ margin: 0;
+ font-size: var(--type-large-title);
}
-.settings-section {
- background: rgba(255,255,255,0.05);
- border: 1px solid rgba(255,255,255,0.1);
- border-radius: 12px;
- padding: 1.25rem;
- margin-bottom: 1rem;
+.header-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-bottom: 1.5rem;
}
-.settings-section h3 {
- margin: 0 0 1rem 0;
- font-size: 1.15rem;
- color: rgba(255,255,255,0.9);
+.connection-badge {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.35rem 0.85rem;
+ border-radius: 20px;
+ font-size: var(--type-callout);
+ font-weight: 500;
+ white-space: nowrap;
}
-.settings-section.info {
- background: rgba(59, 130, 246, 0.08);
- border-color: rgba(59, 130, 246, 0.2);
+.connection-badge.connected {
+ background: rgba(74, 222, 128, 0.12);
+ border: 1px solid rgba(74, 222, 128, 0.3);
+ color: #4ade80;
}
-.settings-section.info p {
- color: rgba(255,255,255,0.6);
- margin: 0.5rem 0;
+.connection-badge.disconnected {
+ background: rgba(248, 113, 113, 0.12);
+ border: 1px solid rgba(248, 113, 113, 0.3);
+ color: #f87171;
}
-.settings-section.info code {
- display: block;
- background: rgba(0,0,0,0.3);
- padding: 0.5rem 0.75rem;
- border-radius: 6px;
- font-family: monospace;
- color: #60a5fa;
- margin: 0.5rem 0;
+.badge-dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: currentColor;
+ box-shadow: 0 0 6px currentColor;
}
-.settings-section.info ul {
- color: rgba(255,255,255,0.6);
- padding-left: 1.25rem;
- margin: 0.5rem 0 0;
+.settings-section {
+ background: rgba(255,255,255,0.05);
+ border: 1px solid rgba(255,255,255,0.1);
+ border-radius: 12px;
+ padding: 1.25rem;
+ margin-bottom: 1rem;
}
-.settings-section.info li {
- margin: 0.25rem 0;
+.settings-section h3 {
+ margin: 0 0 1rem 0;
+ font-size: var(--type-title2);
+ color: rgba(255,255,255,0.9);
}
.mode-cards {
@@ -86,13 +95,13 @@
}
.mode-title {
- font-size: 1.1rem;
+ font-size: var(--type-title2);
font-weight: 600;
margin-bottom: 0.3rem;
}
.mode-desc {
- font-size: 0.85rem;
+ font-size: var(--type-body);
color: rgba(255,255,255,0.5);
}
@@ -106,7 +115,7 @@
.form-row label {
min-width: 60px;
color: rgba(255,255,255,0.7);
- font-size: 0.95rem;
+ font-size: var(--type-body);
}
.form-input {
@@ -117,7 +126,7 @@
border-radius: 6px;
background: rgba(255,255,255,0.08);
color: #a0b4cc;
- font-size: 0.95rem;
+ font-size: var(--type-body);
}
.form-input:focus {
@@ -154,7 +163,7 @@
background: transparent;
color: rgba(255,255,255,0.6);
cursor: pointer;
- font-size: 0.85rem;
+ font-size: var(--type-body);
}
.check-btn:hover:not(:disabled) {
@@ -173,7 +182,7 @@
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
- font-size: 0.95rem;
+ font-size: var(--type-body);
cursor: pointer;
font-weight: 500;
}
@@ -202,43 +211,69 @@
}
.action-hint {
- font-size: 0.8rem;
+ font-size: var(--type-callout);
color: rgba(255,255,255,0.3);
font-family: monospace;
}
-.save-row {
+.save-section {
display: flex;
align-items: center;
gap: 1rem;
+ margin-bottom: 1rem;
}
.save-btn {
- padding: 0.6rem 1.5rem;
- border: none;
+ padding: 0.55rem 1.4rem;
+ border: 1px solid rgba(59, 130, 246, 0.5);
border-radius: 8px;
- background: #3b82f6;
- color: #a0b4cc;
- font-size: 1rem;
+ background: rgba(59, 130, 246, 0.15);
+ color: #60a5fa;
+ font-size: var(--type-body);
font-weight: 500;
cursor: pointer;
+ transition: all 0.2s ease;
}
.save-btn:hover {
- background: #2563eb;
+ background: rgba(59, 130, 246, 0.25);
+ border-color: #3b82f6;
}
-.save-status {
- font-size: 0.9rem;
- color: rgba(255,255,255,0.6);
+.status-toast {
+ position: fixed;
+ bottom: 2rem;
+ left: 50%;
+ transform: translateX(-50%);
+ padding: 0.5rem 1.2rem;
+ border-radius: 20px;
+ font-size: var(--type-body);
+ font-weight: 500;
+ color: #c8d6e5;
+ background: rgba(30, 35, 50, 0.95);
+ border: 1px solid rgba(255,255,255,0.1);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ box-shadow: 0 4px 20px rgba(0,0,0,0.4);
+ z-index: 1000;
+ animation: toast-in 0.3s ease;
+ pointer-events: none;
+ white-space: nowrap;
}
-.save-status.success {
+.status-toast.success {
color: #48bb78;
+ border-color: rgba(72, 187, 120, 0.3);
}
-.save-status.error {
+.status-toast.error {
color: #ef4444;
+ border-color: rgba(239, 68, 68, 0.3);
+}
+
+@keyframes toast-in {
+ from { opacity: 0; transform: translateX(-50%) translateY(10px); }
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
.server-controls {
@@ -264,7 +299,7 @@
}
.pid-label {
- font-size: 0.8rem;
+ font-size: var(--type-callout);
color: rgba(255,255,255,0.4);
font-family: monospace;
}
@@ -286,7 +321,7 @@
border-radius: 6px;
background: rgba(255,255,255,0.08);
color: #a0b4cc;
- font-size: 0.95rem;
+ font-size: var(--type-body);
}
.port-input input:focus {
@@ -303,18 +338,20 @@
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
- font-size: 0.95rem;
+ font-size: var(--type-body);
cursor: pointer;
font-weight: 500;
}
.start-btn {
- background: #48bb78;
- color: #a0b4cc;
+ background: transparent;
+ border: 1px solid rgba(72, 187, 120, 0.5);
+ color: #48bb78;
}
.start-btn:hover:not(:disabled) {
- background: #38a169;
+ background: rgba(72, 187, 120, 0.15);
+ border-color: #48bb78;
}
.start-btn:disabled {
@@ -323,12 +360,14 @@
}
.stop-btn {
- background: rgba(239, 68, 68, 0.8);
- color: #a0b4cc;
+ background: transparent;
+ border: 1px solid rgba(239, 68, 68, 0.5);
+ color: #f87171;
}
.stop-btn:hover {
- background: #ef4444;
+ background: rgba(239, 68, 68, 0.15);
+ border-color: #ef4444;
}
.tunnel-warning {
@@ -341,7 +380,7 @@
.tunnel-warning p {
margin: 0.25rem 0;
color: rgba(255,255,255,0.7);
- font-size: 0.9rem;
+ font-size: var(--type-body);
}
.tunnel-warning code {
@@ -353,7 +392,7 @@
}
.tunnel-warning .hint {
- font-size: 0.8rem;
+ font-size: var(--type-callout);
color: rgba(255,255,255,0.4);
}
@@ -365,7 +404,7 @@
.tunnel-error {
color: #ef4444;
- font-size: 0.85rem;
+ font-size: var(--type-body);
margin: 0;
}
@@ -379,9 +418,10 @@
display: flex;
align-items: center;
gap: 0.5rem;
- padding: 0.5rem 0.75rem;
- background: rgba(0,0,0,0.3);
- border-radius: 6px;
+ padding: 0.6rem 0.85rem;
+ background: rgba(0,0,0,0.25);
+ border: 1px solid rgba(255,255,255,0.08);
+ border-radius: 8px;
}
.tunnel-url code {
@@ -393,16 +433,18 @@
.tunnel-token {
display: flex;
- align-items: center;
+ align-items: flex-start;
gap: 0.5rem;
- padding: 0.5rem 0.75rem;
- background: rgba(0,0,0,0.3);
- border-radius: 6px;
+ padding: 0.6rem 0.85rem;
+ background: rgba(0,0,0,0.25);
+ border: 1px solid rgba(255,255,255,0.08);
+ border-radius: 8px;
+ flex-wrap: wrap;
}
.tunnel-token label {
color: rgba(255,255,255,0.6);
- font-size: 0.85rem;
+ font-size: var(--type-body);
white-space: nowrap;
}
@@ -410,10 +452,9 @@
flex: 1;
font-family: monospace;
color: rgba(255,255,255,0.4);
- font-size: 0.75rem;
- overflow: hidden;
- text-overflow: ellipsis;
+ font-size: var(--type-caption1);
word-break: break-all;
+ white-space: normal;
transition: filter 0.2s ease;
}
@@ -428,16 +469,19 @@
.copy-btn {
background: transparent;
- border: 1px solid rgba(255,255,255,0.2);
- border-radius: 4px;
- padding: 0.2rem 0.5rem;
+ border: 1px solid rgba(255,255,255,0.15);
+ border-radius: 6px;
+ padding: 0.3rem 0.5rem;
cursor: pointer;
- font-size: 0.9rem;
- color: rgba(255,255,255,0.6);
+ font-size: var(--type-body);
+ color: rgba(255,255,255,0.5);
+ transition: all 0.15s ease;
}
.copy-btn:hover {
- border-color: rgba(255,255,255,0.4);
+ border-color: rgba(255,255,255,0.35);
+ color: rgba(255,255,255,0.8);
+ background: rgba(255,255,255,0.05);
}
.qr-code {
@@ -455,7 +499,7 @@
}
.qr-hint {
- font-size: 0.8rem;
+ font-size: var(--type-callout);
color: rgba(255,255,255,0.4);
margin: 0.5rem 0 0;
}
@@ -473,7 +517,7 @@
.mode-hint {
color: rgba(255,255,255,0.5);
- font-size: 0.9rem;
+ font-size: var(--type-body);
margin: 0;
}
@@ -495,7 +539,7 @@
.tunnel-status-text {
color: rgba(255,255,255,0.5);
- font-size: 0.9rem;
+ font-size: var(--type-body);
margin: 0;
}
@@ -506,7 +550,7 @@
border-radius: 10px;
background: rgba(59, 130, 246, 0.08);
color: #60a5fa;
- font-size: 1.1rem;
+ font-size: var(--type-title2);
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
@@ -554,3 +598,113 @@
color: #4ea8d1;
line-height: 1.3;
}
+
+/* === Mobile: compact settings layout === */
+@media (max-width: 640px) {
+ .settings-page {
+ padding: 0.75rem;
+ }
+
+ .settings-header h2 {
+ font-size: var(--type-title2);
+ }
+
+ .header-row {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.5rem;
+ margin-bottom: 0.75rem;
+ }
+
+ .connection-badge {
+ font-size: var(--type-footnote);
+ padding: 0.25rem 0.6rem;
+ }
+
+ .settings-section {
+ padding: 0.75rem;
+ margin-bottom: 0.6rem;
+ border-radius: 10px;
+ }
+
+ .settings-section h3 {
+ font-size: var(--type-body);
+ margin-bottom: 0.6rem;
+ }
+
+ .mode-cards {
+ grid-template-columns: 1fr 1fr;
+ gap: 0.5rem;
+ }
+
+ .mode-card {
+ padding: 0.6rem;
+ border-radius: 8px;
+ }
+
+ .mode-icon {
+ font-size: 1.2rem;
+ margin-bottom: 0.25rem;
+ }
+
+ .mode-icon svg {
+ width: 20px;
+ height: 20px;
+ }
+
+ .mode-title {
+ font-size: var(--type-callout);
+ margin-bottom: 0.15rem;
+ }
+
+ .mode-desc {
+ font-size: var(--type-footnote);
+ line-height: 1.3;
+ }
+
+ .mode-hint {
+ font-size: var(--type-callout);
+ }
+
+ .scan-btn {
+ padding: 0.6rem;
+ font-size: var(--type-body);
+ border-radius: 8px;
+ }
+
+ .url-input {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 0.25rem;
+ }
+
+ .url-input label {
+ font-size: var(--type-callout);
+ }
+
+ .form-input {
+ font-size: var(--type-callout);
+ padding: 0.35rem 0.5rem;
+ }
+
+ .save-section {
+ padding: 0.75rem;
+ }
+
+ .save-btn {
+ font-size: var(--type-body);
+ padding: 0.5rem 1.25rem;
+ width: 100%;
+ }
+
+ .server-status {
+ padding: 0.35rem 0.5rem;
+ font-size: var(--type-callout);
+ gap: 0.5rem;
+ }
+
+ .start-btn, .stop-btn {
+ font-size: var(--type-callout);
+ padding: 0.4rem 0.75rem;
+ }
+}
diff --git a/MainPage.xaml b/MainPage.xaml
index 3f2a335e26..761e8e13ae 100644
--- a/MainPage.xaml
+++ b/MainPage.xaml
@@ -1,6 +1,8 @@
diff --git a/MainPage.xaml.cs b/MainPage.xaml.cs
index 1ee0ddb469..4f3ccddd4e 100644
--- a/MainPage.xaml.cs
+++ b/MainPage.xaml.cs
@@ -18,9 +18,21 @@ public MainPage()
#if ANDROID
blazorWebView.BlazorWebViewInitialized += OnBlazorWebViewInitialized;
+#elif IOS
+ blazorWebView.BlazorWebViewInitialized += OnBlazorWebViewInitializediOS;
#endif
}
+#if IOS
+ private void OnBlazorWebViewInitializediOS(object? sender, BlazorWebViewInitializedEventArgs e)
+ {
+ var wkWebView = e.WebView;
+ wkWebView.Opaque = false;
+ wkWebView.BackgroundColor = UIKit.UIColor.FromRGB(0x1a, 0x1a, 0x2e);
+ wkWebView.ScrollView.BackgroundColor = UIKit.UIColor.FromRGB(0x1a, 0x1a, 0x2e);
+ }
+#endif
+
#if ANDROID
private void OnBlazorWebViewInitialized(object? sender, BlazorWebViewInitializedEventArgs e)
{
diff --git a/Models/ConnectionSettings.cs b/Models/ConnectionSettings.cs
index 9a1656ad96..f965c9aa1c 100644
--- a/Models/ConnectionSettings.cs
+++ b/Models/ConnectionSettings.cs
@@ -7,7 +7,8 @@ public enum ConnectionMode
{
Embedded, // SDK spawns copilot via stdio (dies with app)
Persistent, // App spawns detached copilot server; survives app restarts
- Remote // Connect to a remote server via URL (e.g. DevTunnel)
+ Remote, // Connect to a remote server via URL (e.g. DevTunnel)
+ Demo // Local mock mode for testing chat UI without a real connection
}
public class ConnectionSettings
diff --git a/Platforms/iOS/Info.plist b/Platforms/iOS/Info.plist
index 2910079de2..edb6bb174e 100644
--- a/Platforms/iOS/Info.plist
+++ b/Platforms/iOS/Info.plist
@@ -30,5 +30,9 @@
Assets.xcassets/appicon.appiconset
NSCameraUsageDescription
AutoPilot uses the camera to scan QR codes for connecting to remote servers.
+ UIStatusBarStyle
+ UIStatusBarStyleLightContent
+ UIViewControllerBasedStatusBarAppearance
+
diff --git a/Resources/AppIcon/iOSAssets.car b/Resources/AppIcon/iOSAssets.car
new file mode 100644
index 0000000000..b230f6670f
Binary files /dev/null and b/Resources/AppIcon/iOSAssets.car differ
diff --git a/Resources/Splash/splash.png b/Resources/Splash/splash.png
new file mode 100644
index 0000000000..725865aacd
Binary files /dev/null and b/Resources/Splash/splash.png differ
diff --git a/Services/CopilotService.cs b/Services/CopilotService.cs
index 277daad233..f15547eeb3 100644
--- a/Services/CopilotService.cs
+++ b/Services/CopilotService.cs
@@ -12,6 +12,7 @@ public class CopilotService : IAsyncDisposable
private readonly ChatDatabase _chatDb;
private readonly ServerManager _serverManager;
private readonly WsBridgeClient _bridgeClient;
+ private readonly DemoService _demoService;
private CopilotClient? _client;
private string? _activeSessionName;
private SynchronizationContext? _syncContext;
@@ -86,6 +87,7 @@ private static string FindProjectDir()
public bool IsInitialized { get; private set; }
public bool NeedsConfiguration { get; private set; }
public bool IsRemoteMode { get; private set; }
+ public bool IsDemoMode { get; private set; }
public string? ActiveSessionName => _activeSessionName;
public ChatDatabase ChatDb => _chatDb;
public ConnectionMode CurrentMode { get; private set; } = ConnectionMode.Embedded;
@@ -95,6 +97,7 @@ public CopilotService(ChatDatabase chatDb, ServerManager serverManager, WsBridge
_chatDb = chatDb;
_serverManager = serverManager;
_bridgeClient = bridgeClient;
+ _demoService = new DemoService();
}
// Debug info
@@ -170,6 +173,13 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default)
return;
}
+ // Demo mode: local mock responses, no network needed
+ if (settings.Mode == ConnectionMode.Demo)
+ {
+ InitializeDemo();
+ return;
+ }
+
#if ANDROID
// Android can't run Copilot CLI locally — must connect to remote server
settings.Mode = ConnectionMode.Persistent;
@@ -308,6 +318,58 @@ private async Task InitializeRemoteAsync(ConnectionSettings settings, Cancellati
OnStateChanged?.Invoke();
}
+ ///
+ /// Initialize in Demo mode: wire up DemoService events for local mock responses.
+ ///
+ private void InitializeDemo()
+ {
+ Debug("Demo mode: initializing with mock responses");
+
+ _demoService.OnStateChanged += () => InvokeOnUI(() => OnStateChanged?.Invoke());
+ _demoService.OnContentReceived += (s, c) =>
+ {
+ // Accumulate response in SessionState for history
+ if (_sessions.TryGetValue(s, out var state))
+ state.CurrentResponse.Append(c);
+ InvokeOnUI(() => OnContentReceived?.Invoke(s, c));
+ };
+ _demoService.OnToolStarted += (s, tool, id) =>
+ {
+ if (_sessions.TryGetValue(s, out var state))
+ {
+ FlushCurrentResponse(state);
+ state.Info.History.Add(ChatMessage.ToolCallMessage(tool, id));
+ }
+ InvokeOnUI(() => OnToolStarted?.Invoke(s, tool, id, null));
+ };
+ _demoService.OnToolCompleted += (s, id, result, success) =>
+ {
+ if (_sessions.TryGetValue(s, out var state))
+ {
+ var toolMsg = state.Info.History.LastOrDefault(m => m.ToolCallId == id);
+ if (toolMsg != null) { toolMsg.IsComplete = true; toolMsg.IsSuccess = success; toolMsg.Content = result; }
+ }
+ InvokeOnUI(() => OnToolCompleted?.Invoke(s, id, result, success));
+ };
+ _demoService.OnIntentChanged += (s, i) => InvokeOnUI(() => OnIntentChanged?.Invoke(s, i));
+ _demoService.OnTurnStart += (s) => InvokeOnUI(() => OnTurnStart?.Invoke(s));
+ _demoService.OnTurnEnd += (s) =>
+ {
+ // Flush accumulated response into history (mirrors CompleteResponse)
+ if (_sessions.TryGetValue(s, out var state))
+ {
+ CompleteResponse(state);
+ }
+ InvokeOnUI(() => OnTurnEnd?.Invoke(s));
+ };
+
+ IsInitialized = true;
+ IsDemoMode = true;
+ NeedsConfiguration = false;
+ Debug("Demo mode initialized");
+ OnStateChanged?.Invoke();
+ }
+
///
/// Sync remote session list from WsBridgeClient into our local _sessions dictionary.
///
@@ -339,10 +401,12 @@ private void SyncRemoteSessions()
Info = info
};
}
- // Update processing state
+ // Update processing state — don't overwrite local 'true' with remote 'false'
+ // (race: we sent a message but server hasn't started processing yet)
if (_sessions.TryGetValue(rs.Name, out var state))
{
- state.Info.IsProcessing = rs.IsProcessing;
+ if (rs.IsProcessing)
+ state.Info.IsProcessing = true;
state.Info.MessageCount = rs.MessageCount;
}
}
@@ -356,13 +420,17 @@ private void SyncRemoteSessions()
}
// Sync history from WsBridgeClient cache
+ // Don't overwrite if local history has messages not yet reflected by server
foreach (var (name, messages) in _bridgeClient.SessionHistories)
{
if (_sessions.TryGetValue(name, out var s))
{
- Debug($"SyncRemoteSessions: Syncing {messages.Count} messages for '{name}'");
- s.Info.History.Clear();
- s.Info.History.AddRange(messages);
+ if (messages.Count >= s.Info.History.Count)
+ {
+ Debug($"SyncRemoteSessions: Syncing {messages.Count} messages for '{name}'");
+ s.Info.History.Clear();
+ s.Info.History.AddRange(messages);
+ }
}
}
@@ -400,9 +468,17 @@ public async Task ReconnectAsync(ConnectionSettings settings, CancellationToken
IsInitialized = false;
IsRemoteMode = false;
+ IsDemoMode = false;
CurrentMode = settings.Mode;
OnStateChanged?.Invoke();
+ // Demo mode: local mock responses
+ if (settings.Mode == ConnectionMode.Demo)
+ {
+ InitializeDemo();
+ return;
+ }
+
// Remote mode uses WsBridgeClient state-sync
if (settings.Mode == ConnectionMode.Remote && !string.IsNullOrWhiteSpace(settings.RemoteUrl))
{
@@ -881,6 +957,17 @@ public async Task ResumeSessionAsync(string sessionId, string
public async Task CreateSessionAsync(string name, string? model = null, string? workingDirectory = null, CancellationToken cancellationToken = default)
{
+ // In demo mode, create a local mock session
+ if (IsDemoMode)
+ {
+ var demoInfo = _demoService.CreateSession(name, model);
+ var demoState = new SessionState { Session = null!, Info = demoInfo };
+ _sessions[name] = demoState;
+ _activeSessionName ??= name;
+ OnStateChanged?.Invoke();
+ return demoInfo;
+ }
+
// In remote mode, delegate to WsBridgeClient
if (IsRemoteMode)
{
@@ -1028,7 +1115,9 @@ void Invoke(Action action)
var toolInput = ExtractToolInput(toolStart.Data);
if (!FilteredTools.Contains(startToolName))
{
- // Add to session history
+ // Flush any accumulated assistant text before adding tool message
+ FlushCurrentResponse(state);
+
var toolMsg = ChatMessage.ToolCallMessage(startToolName, startCallId, toolInput);
state.Info.History.Add(toolMsg);
@@ -1199,6 +1288,70 @@ private static string FormatToolResult(object? result)
return null;
}
+ private void TryAttachImages(MessageOptions options, List imagePaths)
+ {
+ try
+ {
+ var sdkAssembly = typeof(MessageOptions).Assembly;
+ var attachItemType = sdkAssembly.GetType("GitHub.Copilot.SDK.UserMessageDataAttachmentsItem");
+ var fileType = sdkAssembly.GetType("GitHub.Copilot.SDK.UserMessageDataAttachmentsItemFile");
+ if (attachItemType == null || fileType == null)
+ {
+ Debug("SDK attachment types not found, falling back to path-in-prompt");
+ return;
+ }
+
+ var items = new System.Collections.Generic.List