diff --git a/App.xaml.cs b/App.xaml.cs index 18bfd0171e..3dee662f74 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -9,6 +9,6 @@ public App() protected override Window CreateWindow(IActivationState? activationState) { - return new Window(new MainPage()) { Title = "AutoPilot.App" }; + return new Window(new MainPage()) { Title = "" }; } } diff --git a/AutoPilot.App.csproj b/AutoPilot.App.csproj index ded97d09b9..7a041fde7c 100644 --- a/AutoPilot.App.csproj +++ b/AutoPilot.App.csproj @@ -29,7 +29,7 @@ SourceGen - AutoPilot.App + AutoPilot com.companyname.autopilot.app @@ -50,7 +50,7 @@ - + @@ -85,4 +85,14 @@ + + + + + + diff --git a/Components/ChatMessageList.razor b/Components/ChatMessageList.razor index c412ae8519..12e6427e57 100644 --- a/Components/ChatMessageList.razor +++ b/Components/ChatMessageList.razor @@ -18,7 +18,7 @@
@if (!Compact) { -
+
πŸ‘€
}
@if (Compact) @@ -38,7 +38,7 @@
@if (!Compact) { -
+
AutoPilot
}
@if (Compact) @@ -58,7 +58,7 @@ @if (Compact) {
- + πŸ’‘ @(msg.IsComplete ? "Thought" : "Thinking...")
} @@ -66,7 +66,7 @@ {
- @if (!msg.IsCollapsed) - { -
@TruncateResult(msg.Content)
- } - else - { -
@FirstLines(msg.Content, 3)
- } -
+ ↳ @LineCount(msg.Content) line@(LineCount(msg.Content) != 1 ? "s" : "") } }
+ @if (!msg.IsCollapsed && msg.IsComplete && !string.IsNullOrEmpty(msg.Content) && !IsUnusableResult(msg.Content)) + { + @if (GetImagePath(msg.Content) is string imgPath2 && FileToDataUri(imgPath2) is string dataUri2) + { +
+ @System.IO.Path.GetFileName(imgPath2) +
+ } + else + { +
+
@TruncateResult(msg.Content)
+
+ } + } } break; @@ -158,12 +139,12 @@
@if (Compact) { - + ⚠️ @msg.Content } else { - + ⚠️ @msg.Content }
@@ -177,23 +158,45 @@ } } - @* Current tool activity *@ - @if (!string.IsNullOrEmpty(CurrentToolName)) + @* Current turn tool activity feed *@ + @if (ToolActivities.Any()) + { + @if (Compact) + { + @foreach (var activity in ToolActivities.TakeLast(3)) + { +
+ πŸ”§ + @FormatToolName(activity.Name) @(activity.IsComplete ? (activity.IsSuccess ? "βœ“" : "βœ—") : "…") +
+ } + } + else + { + @foreach (var activity in ToolActivities) + { +
+ + @(GetActionLabel(activity.Name).Label) + @FormatActionDescription(activity.Name, activity.Input, activity.Result, activity.IsComplete) +
+ } + } + } + else if (!string.IsNullOrEmpty(CurrentToolName)) { @if (Compact) {
- + πŸ”§ @FormatToolName(CurrentToolName) …
} else { -
-
- @FormatToolName(CurrentToolName) - @ToolCount tool@(ToolCount != 1 ? "s" : "") -
+
+ + @(GetActionLabel(CurrentToolName).Label)
} } @@ -204,7 +207,7 @@
@if (!Compact) { -
+
AutoPilot
}
@if (Compact) @@ -217,22 +220,26 @@ } @* Activity indicator *@ - @if (IsProcessing && string.IsNullOrEmpty(StreamingContent) && string.IsNullOrEmpty(CurrentToolName)) + @if (IsProcessing && string.IsNullOrEmpty(StreamingContent) && string.IsNullOrEmpty(CurrentToolName) && !ToolActivities.Any()) { -
- @if (Compact) - { - AI - @(string.IsNullOrEmpty(ActivityText) ? "Thinking..." : ActivityText) - } - else - { -
-
-
@(string.IsNullOrEmpty(ActivityText) ? "Thinking..." : ActivityText)
-
- } -
+ @if (Compact) + { +
+ ⏳ + @(string.IsNullOrEmpty(ActivityText) ? "Thinking…" : ActivityText) +
+ } + else + { +
+ + Thinking + @if (!string.IsNullOrEmpty(ActivityText)) + { + @ActivityText + } +
+ } } }
@@ -242,6 +249,7 @@ [Parameter] public string StreamingContent { get; set; } = ""; [Parameter] public string CurrentToolName { get; set; } = ""; [Parameter] public int ToolCount { get; set; } + [Parameter] public List ToolActivities { get; set; } = new(); [Parameter] public string ActivityText { get; set; } = ""; [Parameter] public bool IsProcessing { get; set; } [Parameter] public bool Compact { get; set; } @@ -332,6 +340,115 @@ w.Length > 0 ? char.ToUpperInvariant(w[0]) + w[1..].ToLowerInvariant() : w)); } + internal static (string Label, string CssClass) GetActionLabel(string toolName) + { + return toolName switch + { + "edit" => ("Edit", "action-edit"), + "create" => ("Create", "action-create"), + "view" => ("Read", "action-read"), + "bash" => ("Run", "action-run"), + "read_bash" => ("Read", "action-read"), + "write_bash" => ("Write", "action-run"), + "stop_bash" => ("Stop", "action-stop"), + "grep" => ("Search", "action-search"), + "glob" => ("Search", "action-search"), + "web_fetch" => ("Fetch", "action-fetch"), + "web_search" => ("Search", "action-search"), + "sql" => ("Query", "action-query"), + "task" => ("Agent", "action-agent"), + "ask_user" => ("Ask", "action-ask"), + "task_complete" => ("Done", "action-done"), + "store_memory" => ("Memory", "action-memory"), + "report_intent" => ("Intent", "action-intent"), + "skill" => ("Skill", "action-agent"), + _ when toolName.StartsWith("github-mcp") => ("GitHub", "action-fetch"), + _ when toolName.StartsWith("context7") => ("Docs", "action-read"), + _ => (FormatToolName(toolName), "action-default"), + }; + } + + internal static string FormatActionDescription(string toolName, string? toolInput, string? result, bool isComplete) + { + var summary = !string.IsNullOrEmpty(toolInput) ? SummarizeToolInput(toolName, toolInput) : ""; + + if (toolName == "edit" && !string.IsNullOrEmpty(toolInput)) + { + // Try to extract path and show line changes + try + { + using var doc = System.Text.Json.JsonDocument.Parse(toolInput); + var root = doc.RootElement; + var path = ExtractJsonProp(root, "path") ?? ""; + if (!string.IsNullOrEmpty(path)) + { + // Show relative path + var fileName = path.Contains('/') ? path[(path.LastIndexOf('/') + 1)..] : path; + var dir = path.Contains('/') ? path[..path.LastIndexOf('/')] : ""; + var oldStr = ExtractJsonProp(root, "old_str"); + var newStr = ExtractJsonProp(root, "new_str"); + var added = newStr?.Split('\n').Length ?? 0; + var removed = oldStr?.Split('\n').Length ?? 0; + var delta = ""; + if (added > 0 || removed > 0) + delta = $" (+{added} -{removed})"; + return $"{fileName}{delta}"; + } + } + catch { } + } + + if (toolName == "bash" && !string.IsNullOrEmpty(summary)) + { + return $"$ {summary}"; + } + + if (toolName == "view" && !string.IsNullOrEmpty(toolInput)) + { + try + { + using var doc = System.Text.Json.JsonDocument.Parse(toolInput); + var root = doc.RootElement; + var path = ExtractJsonProp(root, "path") ?? ""; + var fileName = path.Contains('/') ? path[(path.LastIndexOf('/') + 1)..] : path; + // Check for view_range + if (root.TryGetProperty("view_range", out var range)) + { + var rangeStr = range.ToString(); + return $"{fileName} lines {rangeStr}"; + } + return fileName; + } + catch { } + } + + if (toolName == "create" && !string.IsNullOrEmpty(toolInput)) + { + try + { + using var doc = System.Text.Json.JsonDocument.Parse(toolInput); + var root = doc.RootElement; + var path = ExtractJsonProp(root, "path") ?? ""; + var fileName = path.Contains('/') ? path[(path.LastIndexOf('/') + 1)..] : path; + return fileName; + } + catch { } + } + + if (toolName == "grep" && !string.IsNullOrEmpty(toolInput)) + { + return summary; + } + + if (!string.IsNullOrEmpty(summary)) return summary; + if (isComplete && !string.IsNullOrEmpty(result) && !IsUnusableResult(result)) + { + var lines = LineCount(result); + return $"↳ {lines} line{(lines != 1 ? "s" : "")}"; + } + return ""; + } + internal static string TruncateResult(string result, int maxLength = 1500) { if (string.IsNullOrEmpty(result) || result.Length <= maxLength) return result ?? ""; @@ -391,4 +508,50 @@ } catch { return null; } } + + internal static string SummarizeToolInput(string toolName, string? input) + { + if (string.IsNullOrEmpty(input)) return ""; + try + { + using var doc = System.Text.Json.JsonDocument.Parse(input); + var root = doc.RootElement; + // Tool-specific summaries + return toolName switch + { + "bash" => ExtractJsonProp(root, "command") ?? Truncate(input, 120), + "read_bash" or "write_bash" => ExtractJsonProp(root, "shellId") is string sid ? $"shell: {sid}" : Truncate(input, 120), + "edit" or "create" => ExtractJsonProp(root, "path") ?? Truncate(input, 120), + "view" => ExtractJsonProp(root, "path") ?? Truncate(input, 120), + "grep" => $"{ExtractJsonProp(root, "pattern") ?? "?"} in {ExtractJsonProp(root, "glob") ?? ExtractJsonProp(root, "path") ?? "."}", + "glob" => ExtractJsonProp(root, "pattern") ?? Truncate(input, 120), + "web_fetch" => ExtractJsonProp(root, "url") ?? Truncate(input, 120), + "web_search" => ExtractJsonProp(root, "query") ?? Truncate(input, 120), + "task" => ExtractJsonProp(root, "description") ?? Truncate(input, 120), + "ask_user" => ExtractJsonProp(root, "question") ?? Truncate(input, 120), + "sql" => ExtractJsonProp(root, "query") ?? Truncate(input, 120), + _ => Truncate(input, 120) + }; + } + catch + { + return Truncate(input, 120); + } + } + + private static string? ExtractJsonProp(System.Text.Json.JsonElement root, string prop) + { + if (root.TryGetProperty(prop, out var val) && val.ValueKind == System.Text.Json.JsonValueKind.String) + { + var s = val.GetString(); + return string.IsNullOrEmpty(s) ? null : Truncate(s, 120); + } + return null; + } + + private static string Truncate(string s, int max) + { + if (s.Length <= max) return s; + return s[..max] + "…"; + } } diff --git a/Components/ChatMessageList.razor.css b/Components/ChatMessageList.razor.css index debbdb4f89..b60f2bd8c7 100644 --- a/Components/ChatMessageList.razor.css +++ b/Components/ChatMessageList.razor.css @@ -5,7 +5,7 @@ } .chat-empty { - color: rgba(255,255,255,0.3); + color: rgba(200,216,240,0.3); text-align: center; margin: auto; font-size: 0.95rem; @@ -29,13 +29,13 @@ } .chat-message-list.compact .chat-msg.user .chat-msg-role { - background: rgba(59, 130, 246, 0.3); - color: #93c5fd; + background: rgba(78, 168, 209, 0.3); + color: #7fc4e8; } .chat-message-list.compact .chat-msg.assistant .chat-msg-role { - background: rgba(72, 187, 120, 0.3); - color: #86efac; + background: rgba(0, 195, 91, 0.2); + color: #00c35b; } .chat-message-list.compact .chat-msg-text { @@ -44,17 +44,26 @@ } .chat-message-list.compact .chat-msg-text.thinking { - color: rgba(255,255,255,0.4); + color: #646e8a; font-style: italic; } +.chat-message-list.compact .thinking-label .dots span { + animation: dotPulse 1.4s infinite ease-in-out; + opacity: 0; +} +.chat-message-list.compact .thinking-label .dots span:nth-child(1) { animation-delay: 0s; } +.chat-message-list.compact .thinking-label .dots span:nth-child(2) { animation-delay: 0.2s; } +.chat-message-list.compact .thinking-label .dots span:nth-child(3) { animation-delay: 0.4s; } + /* === Full mode (chat page) === */ .chat-message-list.full .chat-msg { display: flex; - gap: 0.75rem; + gap: 0.5rem; flex-shrink: 0; max-width: 100%; padding: 0.75rem 1rem; + align-items: flex-end; } .chat-message-list.full .chat-msg.user { @@ -62,24 +71,24 @@ } .chat-message-list.full .chat-msg.user .chat-msg-content { - background: rgba(59, 130, 246, 0.2); - border: 1px solid rgba(59, 130, 246, 0.3); + background: rgba(78, 168, 209, 0.2); + border: 1px solid rgba(78, 168, 209, 0.3); border-radius: 12px 12px 0 12px; padding: 0.75rem 1rem; max-width: 80%; } .chat-message-list.full .chat-msg.assistant .chat-msg-content { - background: rgba(72, 187, 120, 0.1); - border: 1px solid rgba(72, 187, 120, 0.2); + background: rgba(0, 195, 91, 0.1); + border: 1px solid rgba(0, 195, 91, 0.2); border-radius: 12px 12px 12px 0; padding: 0.75rem 1rem; max-width: 80%; } .chat-message-list.full .chat-msg-avatar { - width: 32px; - height: 32px; + width: 40px; + height: 40px; border-radius: 50%; display: flex; align-items: center; @@ -89,27 +98,52 @@ .chat-message-list.full .chat-msg.user .chat-msg-avatar { order: 1; - background: rgba(59, 130, 246, 0.3); + background: rgba(78, 168, 209, 0.3); + width: 30px; + height: 30px; } .chat-message-list.full .chat-msg.assistant .chat-msg-avatar { - background: rgba(72, 187, 120, 0.3); + background: transparent; + overflow: hidden; + width: 44px; + height: 44px; +} + +.chat-message-list.full .chat-msg.assistant .chat-msg-avatar img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 50%; } .chat-message-list.full .chat-msg-text { - color: white; + color: #a0b4cc; word-break: break-word; line-height: 1.6; } .chat-message-list.full .chat-msg-text.thinking { - color: rgba(255,255,255,0.4); + color: #646e8a; font-style: italic; } +.chat-message-list.full .thinking-label .dots span { + animation: dotPulse 1.4s infinite ease-in-out; + opacity: 0; +} +.chat-message-list.full .thinking-label .dots span:nth-child(1) { animation-delay: 0s; } +.chat-message-list.full .thinking-label .dots span:nth-child(2) { animation-delay: 0.2s; } +.chat-message-list.full .thinking-label .dots span:nth-child(3) { animation-delay: 0.4s; } + +@keyframes dotPulse { + 0%, 80%, 100% { opacity: 0; } + 40% { opacity: 1; } +} + .chat-message-list.full .chat-msg-time { font-size: 0.75rem; - color: rgba(255,255,255,0.3); + color: rgba(200,216,240,0.3); margin-top: 0.25rem; } @@ -119,7 +153,7 @@ .chat-message-list.full .system-text { font-size: 0.85rem; - color: rgba(255,255,255,0.4); + color: #646e8a; font-style: italic; text-align: center; padding: 0.5rem; @@ -148,7 +182,7 @@ ::deep .reasoning-content { font-size: 0.85rem; - color: rgba(255,255,255,0.5); + color: #7a85a0; white-space: pre-wrap; line-height: 1.5; padding: 0.25rem 0; @@ -165,12 +199,12 @@ border-radius: 8px; padding: 0.5rem 0.75rem; font-size: 0.85rem; - border: 1px solid rgba(255,255,255,0.1); - background: rgba(255,255,255,0.03); + border: 1px solid rgba(78,168,209,0.15); + background: rgba(78,168,209,0.03); } ::deep .tool-card.running { border-color: rgba(251, 191, 36, 0.3); } -::deep .tool-card.success { border-color: rgba(72, 187, 120, 0.3); } +::deep .tool-card.success { border-color: rgba(0, 195, 91, 0.3); } ::deep .tool-card.error { border-color: rgba(248, 113, 113, 0.3); } ::deep .tool-header { @@ -179,18 +213,18 @@ align-items: center; } -::deep .tool-info { color: rgba(255,255,255,0.7); display: flex; align-items: center; gap: 0.35rem; } -::deep .tool-status { display: flex; align-items: center; gap: 0.35rem; color: rgba(255,255,255,0.5); } +::deep .tool-info { color: #c8d8f0; display: flex; align-items: center; gap: 0.35rem; } +::deep .tool-status { display: flex; align-items: center; gap: 0.35rem; color: #7a85a0; } ::deep .tool-result-section { margin-top: 0.5rem; - border-top: 1px solid rgba(255,255,255,0.05); + border-top: 1px solid rgba(78,168,209,0.06); padding-top: 0.5rem; } ::deep .tool-result-content { font-size: 0.8rem; - color: rgba(255,255,255,0.5); + color: #7a85a0; white-space: pre-wrap; word-break: break-word; max-height: 300px; @@ -201,7 +235,7 @@ ::deep .tool-result-toggle { background: none; border: none; - color: rgba(59, 130, 246, 0.7); + color: rgba(78, 168, 209, 0.7); font-size: 0.8rem; cursor: pointer; padding: 0; @@ -213,6 +247,107 @@ border-radius: 6px; } +/* === Action Items (Copilot CLI style) === */ +::deep .action-item { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0.1rem 1rem; + padding: 0.25rem 0.4rem; + font-size: 0.82rem; + cursor: pointer; + border-radius: 4px; + transition: background 0.15s; +} + +::deep .action-item:hover { + background: rgba(200,216,240,0.04); +} + +::deep .action-dot { + width: 7px; + height: 7px; + border-radius: 50%; + flex-shrink: 0; +} + +::deep .action-item.running .action-dot { + background: #f59e0b; + box-shadow: 0 0 6px rgba(245,158,11,0.5); + animation: pulse-dot 1.5s ease-in-out infinite; +} + +::deep .action-item.done .action-dot { background: #22c55e; } +::deep .action-item.failed .action-dot { background: #ef4444; } + +@keyframes pulse-dot { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +::deep .action-label { + font-weight: 600; + color: #c8d8f0; + white-space: nowrap; +} + +/* Action-specific label colors */ +::deep .action-edit .action-label { color: #f59e0b; } +::deep .action-create .action-label { color: #22c55e; } +::deep .action-read .action-label { color: #4ea8d1; } +::deep .action-run .action-label { color: #a78bfa; } +::deep .action-search .action-label { color: #60a5fa; } +::deep .action-fetch .action-label { color: #34d399; } +::deep .action-query .action-label { color: #f472b6; } +::deep .action-agent .action-label { color: #c084fc; } +::deep .action-ask .action-label { color: #fbbf24; } +::deep .action-stop .action-label { color: #ef4444; } +::deep .action-memory .action-label { color: #a78bfa; } +::deep .action-done .action-label { color: #22c55e; } + +::deep .action-desc { + color: rgba(200,216,240,0.55); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; + font-size: 0.75rem; +} + +::deep .action-detail-hint { + margin-left: auto; + color: rgba(200,216,240,0.3); + font-size: 0.7rem; + white-space: nowrap; + flex-shrink: 0; +} + +::deep .action-expanded-result { + margin: 0.15rem 1rem 0.3rem 2rem; + padding: 0.4rem 0.6rem; + background: rgba(15,15,35,0.5); + border-radius: 6px; + border: 1px solid rgba(78,168,209,0.08); + overflow: hidden; +} + +::deep .action-expanded-result pre { + font-size: 0.75rem; + color: rgba(200,216,240,0.5); + white-space: pre-wrap; + word-break: break-word; + max-height: 200px; + overflow-y: auto; + margin: 0; +} + +::deep .action-expanded-result img { + max-width: 100%; + max-height: 300px; + border-radius: 4px; +} + ::deep .task-complete-card { flex-shrink: 0; display: flex; @@ -220,10 +355,10 @@ gap: 0.5rem; margin: 0.25rem 1rem; padding: 0.75rem 1rem; - background: rgba(72, 187, 120, 0.1); - border: 1px solid rgba(72, 187, 120, 0.3); + background: rgba(0, 195, 91, 0.1); + border: 1px solid rgba(0, 195, 91, 0.3); border-radius: 8px; - color: #86efac; + color: #00c35b; font-size: 0.95rem; } @@ -242,17 +377,17 @@ } ::deep .markdown-body { - color: white; + color: #a0b4cc; } ::deep .markdown-body h1, ::deep .markdown-body h2, ::deep .markdown-body h3 { margin: 0.5em 0 0.25em; border: none; - color: white; + color: #a0b4cc; } ::deep .markdown-body code { - background: rgba(255,255,255,0.1); + background: rgba(78,168,209,0.15); padding: 0.15em 0.3em; border-radius: 3px; font-size: 0.9em; @@ -270,12 +405,12 @@ padding: 0; } -::deep .markdown-body a { color: #60a5fa; } +::deep .markdown-body a { color: #4ea8d1; } ::deep .markdown-body blockquote { - border-left: 3px solid rgba(255,255,255,0.2); + border-left: 3px solid rgba(78,168,209,0.25); margin: 0.5rem 0; padding: 0 0.75rem; - color: rgba(255,255,255,0.6); + color: #8e9abd; } ::deep .markdown-body ul, ::deep .markdown-body ol { @@ -289,7 +424,7 @@ } ::deep .markdown-body th, ::deep .markdown-body td { - border: 1px solid rgba(255,255,255,0.15); + border: 1px solid rgba(78,168,209,0.2); padding: 0.4rem 0.6rem; text-align: left; } diff --git a/Components/Layout/MainLayout.razor.css b/Components/Layout/MainLayout.razor.css index ffa2e807b8..2c2154fd9b 100644 --- a/Components/Layout/MainLayout.razor.css +++ b/Components/Layout/MainLayout.razor.css @@ -82,10 +82,13 @@ main { } .sidebar { - width: 280px; + width: 17.5rem; + min-width: 200px; + max-width: 360px; height: 100vh; position: sticky; top: 0; flex-shrink: 0; + overflow: hidden; } } diff --git a/Components/Layout/NavMenu.razor.css b/Components/Layout/NavMenu.razor.css index a2aeace9c3..d30a65ec02 100644 --- a/Components/Layout/NavMenu.razor.css +++ b/Components/Layout/NavMenu.razor.css @@ -3,7 +3,7 @@ cursor: pointer; width: 3.5rem; height: 2.5rem; - color: white; + color: #a0b4cc; position: absolute; top: 0.5rem; right: 1rem; @@ -73,12 +73,12 @@ .nav-item ::deep a.active { background-color: rgba(255,255,255,0.37); - color: white; + color: #a0b4cc; } .nav-item ::deep .nav-link:hover { background-color: rgba(255,255,255,0.1); - color: white; + color: #a0b4cc; } .nav-scrollable { diff --git a/Components/Layout/SessionSidebar.razor b/Components/Layout/SessionSidebar.razor index 6f596fcb4d..2770fdf0b2 100644 --- a/Components/Layout/SessionSidebar.razor +++ b/Components/Layout/SessionSidebar.razor @@ -8,13 +8,13 @@ {
- AutoPilot + AutoPilot
} @@ -27,7 +27,7 @@ else if (IsFlyoutPanel)
- +
- - +
@@ -46,7 +46,7 @@ else if (IsFlyoutPanel) - +
} @if (createError != null) @@ -64,8 +64,8 @@ else { } @@ -196,7 +194,7 @@ else {
- Saved Sessions (@filteredPersistedSessions.Count()) + Saved Sessions (@filteredPersistedSessions.Count()) @(showPersistedSessions ? "β–Ό" : "β–Ά")
@@ -209,18 +207,45 @@ else @foreach (var persisted in filteredPersistedSessions.Take(30)) { -
+ var isOpen = IsSessionOpen(persisted.SessionId); +
@(persisted.Title ?? "Untitled") - @persisted.LastModified.ToString("MMM dd h:mm tt") - @if (!string.IsNullOrEmpty(persisted.WorkingDirectory)) +
+
+ @persisted.LastModified.ToString("MMM dd h:mm tt") + @if (!string.IsNullOrEmpty(persisted.WorkingDirectory)) + { + @GetShortPath(persisted.WorkingDirectory) + } +
+ @if (isOpen) + { + ● Open + } + else if (confirmResumeId != persisted.SessionId) + { + Resume + } +
+ @if (!isOpen && confirmResumeId == persisted.SessionId) { - @GetShortPath(persisted.WorkingDirectory) +
+
+ + +
+ @if (!string.IsNullOrEmpty(resumeError)) + { +
⚠ @resumeError
+ } +
}
- Resume
} } @@ -231,10 +256,13 @@ else private string selectedModel = "claude-opus-4.6"; private string sessionFilter = ""; private bool isCreating = false; + private bool hasSessionName = false; private string? createError = null; private bool showPersistedSessions = false; private bool showDirectoryInput = false; private string? renamingSession = null; + private string? confirmResumeId = null; + private string? resumeError = null; private string currentPage = "/"; private List sessions = new(); private List persistedSessions = new(); @@ -258,13 +286,15 @@ else }; private IEnumerable filteredPersistedSessions => - string.IsNullOrWhiteSpace(sessionFilter) + (string.IsNullOrWhiteSpace(sessionFilter) ? persistedSessions : persistedSessions.Where(s => (s.Title?.Contains(sessionFilter, StringComparison.OrdinalIgnoreCase) ?? false) || (s.Preview?.Contains(sessionFilter, StringComparison.OrdinalIgnoreCase) ?? false) || (s.WorkingDirectory?.Contains(sessionFilter, StringComparison.OrdinalIgnoreCase) ?? false) || - s.SessionId.Contains(sessionFilter, StringComparison.OrdinalIgnoreCase)); + s.SessionId.Contains(sessionFilter, StringComparison.OrdinalIgnoreCase))) + .OrderByDescending(s => IsSessionOpen(s.SessionId)) + .ThenByDescending(s => s.LastModified); protected override void OnInitialized() { @@ -321,6 +351,9 @@ else persistedSessions = CopilotService.GetPersistedSessions().ToList(); } + private bool IsSessionOpen(string sessionId) => + sessions.Any(s => s.SessionId == sessionId); + private void TogglePersistedSessions() { showPersistedSessions = !showPersistedSessions; @@ -330,12 +363,18 @@ else } } + private void OnSessionNameInput(ChangeEventArgs e) + { + hasSessionName = !string.IsNullOrWhiteSpace(e.Value?.ToString()); + } + private async Task CreateSession() { // Try both desktop and flyout input IDs var name = await JS.InvokeAsync("eval", "document.getElementById('sessionNameInput')?.value || document.getElementById('flyoutSessionNameInput')?.value || ''"); if (string.IsNullOrWhiteSpace(name) || isCreating) return; + hasSessionName = false; // Read working directory if the input is visible string? workingDir = null; @@ -398,20 +437,33 @@ else #endif } + private void ConfirmResume(PersistedSessionInfo persisted) + { + resumeError = null; + confirmResumeId = confirmResumeId == persisted.SessionId ? null : persisted.SessionId; + } + + private void CancelResume() + { + confirmResumeId = null; + resumeError = null; + } + private async Task ResumeSession(PersistedSessionInfo persisted) { isCreating = true; + resumeError = null; try { - // Use the session title as the display name var title = persisted.Title ?? "Untitled"; - // Truncate and make unique var displayName = title.Length > 30 ? title[..27] + "..." : title; var sessionInfo = await CopilotService.ResumeSessionAsync(persisted.SessionId, displayName); CopilotService.SwitchSession(sessionInfo.Name); + confirmResumeId = null; } catch (Exception ex) { + resumeError = FriendlyResumeError(ex.Message); Console.WriteLine($"Error resuming session: {ex.Message}"); } finally @@ -420,6 +472,24 @@ else } } + private string FriendlyResumeError(string message) + { + if (message.Contains("not found", StringComparison.OrdinalIgnoreCase) || + message.Contains("does not exist", StringComparison.OrdinalIgnoreCase)) + return "Session no longer exists β€” it may have expired or been deleted."; + if (message.Contains("already exists", StringComparison.OrdinalIgnoreCase)) + return "A session with this name is already open. Close it first."; + if (message.Contains("not initialized", StringComparison.OrdinalIgnoreCase)) + return "Copilot is not connected yet. Wait for initialization."; + if (message.Contains("timeout", StringComparison.OrdinalIgnoreCase) || + message.Contains("timed out", StringComparison.OrdinalIgnoreCase)) + return "Connection timed out. Check your network and try again."; + if (message.Contains("unauthorized", StringComparison.OrdinalIgnoreCase) || + message.Contains("401", StringComparison.OrdinalIgnoreCase)) + return "Authentication failed. You may need to sign in again."; + return $"Resume failed: {(message.Length > 120 ? message[..120] + "…" : message)}"; + } + private async Task StartRename(string sessionName) { renamingSession = sessionName; @@ -465,6 +535,13 @@ else await OnSessionSelected.InvokeAsync(); } + private async Task SwitchToOpenSession(string sessionId) + { + var match = sessions.FirstOrDefault(s => s.SessionId == sessionId); + if (match != null) + await SelectSession(match.Name); + } + private async Task CloseSession(string name) { await CopilotService.CloseSessionAsync(name); diff --git a/Components/Layout/SessionSidebar.razor.css b/Components/Layout/SessionSidebar.razor.css index d9e6f75d33..7e65dcc103 100644 --- a/Components/Layout/SessionSidebar.razor.css +++ b/Components/Layout/SessionSidebar.razor.css @@ -2,8 +2,9 @@ display: flex; flex-direction: column; height: 100%; - background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%); - color: white; + background: linear-gradient(180deg, #161630 0%, #1a1a38 100%); + color: #a0b4cc; + overflow: hidden; } .sidebar-header { @@ -24,7 +25,7 @@ } .sidebar-header h3 { - margin: 0 0 0.5rem 0; + margin: 0 0 0.25rem 0; font-size: 1.4rem; } @@ -55,7 +56,7 @@ border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; background: rgba(255,255,255,0.1); - color: white; + color: #a0b4cc; font-size: 1rem; } @@ -64,14 +65,15 @@ border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; background: rgba(255,255,255,0.1); - color: white; + color: #a0b4cc; font-size: 0.9rem; - max-width: 160px; + min-width: 0; + max-width: 100%; } .model-select option { - background: #1a1a2e; - color: white; + background: #0f0f23; + color: #a0b4cc; } .new-session input::placeholder { @@ -88,7 +90,7 @@ border: none; border-radius: 6px; background: #3b82f6; - color: white; + color: #a0b4cc; cursor: pointer; font-size: 1rem; font-weight: bold; @@ -114,10 +116,10 @@ } .dir-toggle-btn { - padding: 0.5rem !important; + padding: 0.3rem 0.5rem !important; background: rgba(255,255,255,0.1) !important; border: 1px solid rgba(255,255,255,0.2) !important; - font-size: 0.9rem !important; + font-size: 0.75rem !important; font-weight: normal !important; } @@ -142,7 +144,7 @@ border-radius: 6px; font-size: 0.9rem !important; font-weight: normal !important; - color: white; + color: #a0b4cc; cursor: pointer; } @@ -211,6 +213,20 @@ border: 1px dashed rgba(255,255,255,0.2); } +.session-item.persisted.already-open { + opacity: 0.7; + cursor: default; + border: 1px solid rgba(0, 195, 91, 0.25); + background: rgba(0, 195, 91, 0.08); +} + +.open-badge { + font-size: 0.75rem; + color: #00c35b; + white-space: nowrap; + flex-shrink: 0; +} + .session-item.persisted:hover { opacity: 1; border-color: rgba(59, 130, 246, 0.5); @@ -221,28 +237,52 @@ flex-direction: column; gap: 0.25rem; overflow: hidden; + min-width: 0; + flex: 1; +} + +.session-meta-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; +} + +.session-meta-left { + display: flex; + flex-direction: column; + gap: 0.1rem; + min-width: 0; } .session-name { font-weight: 500; - font-size: 1.05rem; + font-size: 0.95rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: flex; align-items: center; - gap: 0.5rem; + gap: 0.4rem; + min-width: 0; +} + +.session-name-text { + display: inline-block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; + flex: 1; } .persisted-name { font-family: -apple-system, BlinkMacSystemFont, sans-serif; - font-size: 1rem; - white-space: normal; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; + font-size: 0.9rem; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + display: inline-block; } .resumed-badge { @@ -295,6 +335,8 @@ .processing { color: #fbbf24; animation: pulse 1s infinite; + font-size: 0.75rem; + white-space: nowrap; } @keyframes pulse { @@ -333,7 +375,10 @@ } .rename-btn { - font-size: 0.8rem; + font-size: 0.65rem; + width: auto; + padding: 0 0.3rem; + letter-spacing: 0.02em; } .resume-btn:hover { @@ -351,7 +396,7 @@ border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; background: rgba(255,255,255,0.1); - color: white; + color: #a0b4cc; font-size: 1rem; } @@ -376,6 +421,88 @@ color: #60a5fa; } +.session-item.persisted.confirm-active { + opacity: 1; + border-color: rgba(78, 168, 209, 0.5); + background: rgba(78, 168, 209, 0.1); +} + +.resume-confirm { + display: flex; + gap: 0.35rem; + align-items: center; + flex-shrink: 0; +} + +.btn-resume-yes { + padding: 0.3rem 0.6rem; + border: none; + border-radius: 5px; + background: #4ea8d1; + color: #a0b4cc; + cursor: pointer; + font-size: 0.8rem; + font-weight: 600; + white-space: nowrap; + transition: background 0.15s ease; +} + +.btn-resume-yes:hover:not(:disabled) { + background: #3d8db5; +} + +.btn-resume-yes:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-resume-no { + padding: 0.3rem 0.45rem; + border: none; + border-radius: 5px; + background: rgba(255,255,255,0.1); + color: rgba(255,255,255,0.6); + cursor: pointer; + font-size: 0.8rem; + transition: all 0.15s ease; +} + +.btn-resume-no:hover { + background: rgba(239, 68, 68, 0.3); + color: #ef4444; +} + +.resume-confirm-wrap { + display: flex; + flex-direction: column; + gap: 0.3rem; + align-items: flex-end; + margin-top: 0.25rem; +} + +.resume-error { + font-size: 0.75rem; + color: #f87171; + line-height: 1.3; + max-width: 180px; + text-align: right; + word-wrap: break-word; +} + +.resume-confirm .dots span, +.btn-resume-yes .dots span { + animation: dotPulse 1.4s infinite ease-in-out; + opacity: 0; +} +.resume-confirm .dots span:nth-child(1), +.btn-resume-yes .dots span:nth-child(1) { animation-delay: 0s; } +.resume-confirm .dots span:nth-child(2), +.btn-resume-yes .dots span:nth-child(2) { animation-delay: 0.2s; } +.resume-confirm .dots span:nth-child(3), +.btn-resume-yes .dots span:nth-child(3) { animation-delay: 0.4s; } + +@keyframes dotPulse { 0%, 80%, 100% { opacity: 0; } 40% { opacity: 1; } } + .session-item.completed { background: rgba(72, 187, 120, 0.2); border: 1px solid rgba(72, 187, 120, 0.5); @@ -410,21 +537,22 @@ display: flex; align-items: center; justify-content: center; - gap: 0.4rem; - padding: 0.5rem; + gap: 0.3rem; + padding: 0.4rem; border: 1px solid transparent; border-radius: 6px; background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.7); text-decoration: none; - font-size: 0.95rem; - line-height: 1; + font-size: 0.8rem; + line-height: 1.2; transition: all 0.15s ease; + font-family: -apple-system, BlinkMacSystemFont, sans-serif; } .nav-tab:hover { background: rgba(255,255,255,0.15); - color: white; + color: #a0b4cc; } .nav-tab.active { @@ -439,7 +567,7 @@ border: 1px solid #60a5fa; border-radius: 4px; background: rgba(255,255,255,0.15); - color: white; + color: #a0b4cc; font-size: 1rem; font-weight: 500; font-family: inherit; @@ -452,13 +580,13 @@ align-items: center; gap: 0.75rem; padding: 0.5rem 1rem; - color: white; + color: #a0b4cc; } .hamburger-btn { background: none; border: none; - color: white; + color: #a0b4cc; cursor: pointer; padding: 0.25rem; display: flex; @@ -507,7 +635,7 @@ .flyout-header h3 { margin: 0; font-size: 1.2rem; - color: white; + color: #a0b4cc; } .flyout-close-btn { @@ -520,7 +648,7 @@ } .flyout-close-btn:hover { - color: white; + color: #a0b4cc; } /* === Mobile responsive === */ diff --git a/Components/Pages/Dashboard.razor.css b/Components/Pages/Dashboard.razor.css index ecfec1130f..361d19f239 100644 --- a/Components/Pages/Dashboard.razor.css +++ b/Components/Pages/Dashboard.razor.css @@ -3,7 +3,7 @@ flex-direction: column; height: 100%; background: #0f0f23; - color: white; + color: #a0b4cc; padding: 1.5rem; overflow-y: auto; } @@ -216,7 +216,7 @@ border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; background: rgba(255,255,255,0.08); - color: white; + color: #a0b4cc; font-size: 1rem; } @@ -238,7 +238,7 @@ border: none; border-radius: 6px; background: #3b82f6; - color: white; + color: #a0b4cc; cursor: pointer; font-size: 1.1rem; } @@ -276,7 +276,7 @@ border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; background: rgba(255,255,255,0.08); - color: white; + color: #a0b4cc; font-size: 1.05rem; } @@ -290,19 +290,22 @@ } .broadcast-input button { - padding: 0.75rem 1.5rem; - border: none; + padding: 0.5rem 1.2rem; + border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; - background: #7c3aed; - color: white; - font-size: 1.05rem; + background: rgba(255,255,255,0.06); + color: #a0b4cc; + font-size: 0.9rem; font-weight: 500; cursor: pointer; white-space: nowrap; + transition: all 0.15s ease; } .broadcast-input button:hover { - background: #6d28d9; + background: rgba(255,255,255,0.1); + border-color: rgba(255,255,255,0.25); + color: #c8d8f0; } /* === Mobile responsive === */ diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor index f0b562a878..a6953504f9 100644 --- a/Components/Pages/Home.razor +++ b/Components/Pages/Home.razor @@ -23,6 +23,7 @@ else if (activeSession == null) {
+

Welcome to AutoPilot

Create a new session from the sidebar to start chatting with Copilot.

@@ -30,25 +31,26 @@ else {
-

@activeSession.Name

- - @activeSession.Model - @if (activeSession.SessionId != null) - { - @activeSession.SessionId[..8] - } - @if (activeSession.IsProcessing) - { - Copilot is typing... - } +
+ @if (activeSession.IsProcessing) + { + + } + @activeModelOverride + @if (activeSession.SessionId != null) + { + πŸ”— @activeSession.SessionId[..8] πŸ“‹ + + } +
@if (HasMoreMessages) { } @@ -56,6 +58,7 @@ StreamingContent="@streamingContent" CurrentToolName="@currentToolName" ToolCount="currentTurnToolCount" + ToolActivities="turnToolActivities" IsProcessing="activeSession.IsProcessing" Compact="false" />
@@ -63,7 +66,7 @@ @if (!string.IsNullOrEmpty(lastError)) {
- @lastError + ⚠️ @lastError
} @@ -71,13 +74,13 @@
@if (!string.IsNullOrEmpty(currentIntent)) { -
@currentIntent
+
πŸ’­ @currentIntent
} @if (activeSession.MessageQueue.Any()) {
- Queued (@activeSession.MessageQueue.Count) + πŸ“‹ Queued (@activeSession.MessageQueue.Count)
@for (var i = 0; i < activeSession.MessageQueue.Count; i++) @@ -119,29 +122,32 @@ @if (activeSession.IsProcessing) { }
- +
+ + +
Β· - @(currentUsage?.Model ?? activeSession.Model) + @if (currentUsage != null) { @if (currentUsage.InputTokens.HasValue || currentUsage.OutputTokens.HasValue) @@ -158,8 +164,13 @@ Β· @activeSession.History.Count msgs Β· + + + @(fontSize)px + + + Β·
@@ -173,7 +184,7 @@ Debug Log
- +
@@ -211,6 +222,44 @@ "AutoPilot", "image-attachments"); private record PendingImage(string FilePath, string FileName, string DataUri); + private int fontSize = 20; + + private readonly string[] inlineAvailableModels = new[] + { + "claude-opus-4.6", + "claude-sonnet-4.5", + "claude-sonnet-4", + "claude-haiku-4.5", + "gpt-5.2", + "gpt-5.1", + "gpt-5", + "gpt-5-mini", + "gemini-3-pro-preview", + }; + + private string activeModelOverride + { + get + { + // Priority: usage model (actual from SDK) > session model (if not "resumed") > fallback + var model = currentUsage?.Model; + if (string.IsNullOrEmpty(model) || !inlineAvailableModels.Contains(model)) + { + model = activeSession?.Model; + if (string.IsNullOrEmpty(model) || model == "resumed" || !inlineAvailableModels.Contains(model)) + model = _selectedModelOverride ?? inlineAvailableModels[0]; + } + return model; + } + set + { + _selectedModelOverride = value; + if (activeSession != null && activeSession.Model != "resumed") + activeSession.Model = value; + StateHasChanged(); + } + } + private string? _selectedModelOverride; private List VisibleHistory { @@ -270,6 +319,10 @@ _needsScroll = false; await ForceScrollToBottom(); } + if (firstRender && fontSize != 20) + { + try { await JS.InvokeVoidAsync("setAppFontSize", fontSize); } catch { } + } try { await JS.InvokeVoidAsync("setupTextareaEnterHandler", textareaRef); } catch { } if (_dotNetRef == null) { @@ -281,19 +334,34 @@ private string currentToolName = ""; private int currentTurnToolCount; + private List turnToolActivities = new(); - private void HandleToolStarted(string sessionName, string toolName, string callId) + private void HandleToolStarted(string sessionName, string toolName, string callId, string? inputSummary) { if (sessionName != CopilotService.ActiveSessionName) return; currentToolName = toolName; currentTurnToolCount++; + turnToolActivities.Add(new ToolActivity + { + Name = toolName, + CallId = callId, + Input = inputSummary, + StartedAt = DateTime.Now + }); InvokeAsync(StateHasChanged); } private void HandleToolCompleted(string sessionName, string callId, string result, bool success) { if (sessionName != CopilotService.ActiveSessionName || activeSession == null) return; - // Don't add to history β€” just clear current tool indicator + var activity = turnToolActivities.LastOrDefault(a => a.CallId == callId); + if (activity != null) + { + activity.IsComplete = true; + activity.IsSuccess = success; + activity.Result = result; + activity.CompletedAt = DateTime.Now; + } currentToolName = ""; InvokeAsync(StateHasChanged); } @@ -338,12 +406,9 @@ private void HandleTurnStart(string sessionName) { if (sessionName != CopilotService.ActiveSessionName || activeSession == null) return; - // Don't clear streamingContent β€” let it accumulate across turns - // so messages don't vanish between tool-call-driven turns. - // CompleteResponse will add the full response to History and - // SendPromptAsync clears streamingContent when done. currentTurnToolCount = 0; currentToolName = ""; + turnToolActivities.Clear(); InvokeAsync(StateHasChanged); } @@ -391,6 +456,8 @@ { if (!string.IsNullOrEmpty(uiState.ActiveSession)) CopilotService.SetActiveSession(uiState.ActiveSession); + if (uiState.FontSize >= 12 && uiState.FontSize <= 24) + fontSize = uiState.FontSize; if (uiState.CurrentPage is "/dashboard" or "/settings") { _needsRedirect = true; @@ -429,6 +496,8 @@ } } + private bool _forceNextScroll = false; + private void RefreshState() { var prev = activeSession?.Name; @@ -437,25 +506,40 @@ { _needsScroll = true; visibleMessageCount = 50; // Reset window when switching sessions + // Clear per-session UI state so previous session's state doesn't bleed + streamingContent = ""; + currentIntent = ""; + currentToolName = ""; + currentTurnToolCount = 0; + currentUsage = null; + turnToolActivities.Clear(); } InvokeAsync(async () => { StateHasChanged(); - await ScrollToBottom(); + if (_forceNextScroll) + { + _forceNextScroll = false; + await ForceScrollToBottom(); + } + else + { + await ScrollToBottom(); + } }); } private void HandleContentReceived(string sessionName, string content) { - if (sessionName == CopilotService.ActiveSessionName && activeSession?.IsProcessing == true) + if (sessionName != CopilotService.ActiveSessionName || activeSession?.IsProcessing != true) + return; + + streamingContent += content; + InvokeAsync(async () => { - streamingContent += content; - InvokeAsync(async () => - { - StateHasChanged(); - await ScrollToBottom(); - }); - } + StateHasChanged(); + await ScrollToBottom(); + }); } private void HandleSessionComplete(string sessionName, string summary) @@ -463,9 +547,11 @@ if (sessionName == CopilotService.ActiveSessionName) { currentIntent = ""; + streamingContent = ""; } InvokeAsync(async () => { + StateHasChanged(); try { await JS.InvokeVoidAsync("showNotification", sessionName, summary); } catch { } }); @@ -499,6 +585,10 @@ StateHasChanged(); } } + // Cmd+Plus / Cmd+Minus to adjust font size + if (e.MetaKey && (e.Key == "=" || e.Key == "+")) IncreaseFontSize(); + if (e.MetaKey && e.Key == "-") DecreaseFontSize(); + if (e.MetaKey && e.Key == "0") ResetFontSize(); } private async Task StopResponse() @@ -536,9 +626,12 @@ return; } + _forceNextScroll = true; streamingContent = ""; currentIntent = ""; lastError = null; + StateHasChanged(); + await ForceScrollToBottom(); try { @@ -656,6 +749,17 @@ await JS.InvokeVoidAsync("eval", $"navigator.clipboard.writeText('{sessionId}')"); } + private void OpenSessionFolder(string sessionId) + { + var path = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".copilot", "session-state", sessionId); + if (Directory.Exists(path)) + { + System.Diagnostics.Process.Start("open", path); + } + } + private async Task InspectDom() { var result = await JS.InvokeAsync("inspectMessages"); @@ -680,6 +784,15 @@ private static string FormatTokenCount(int count) => count >= 1000 ? $"{count / 1000}k" : count.ToString(); + private void IncreaseFontSize() { if (fontSize < 24) { fontSize += 2; ApplyFontSize(); } } + private void DecreaseFontSize() { if (fontSize > 12) { fontSize -= 2; ApplyFontSize(); } } + private void ResetFontSize() { fontSize = 20; ApplyFontSize(); } + private void ApplyFontSize() + { + _ = JS.InvokeVoidAsync("setAppFontSize", fontSize); + CopilotService.SaveUiState("/", fontSize: fontSize); + } + private async Task ScrollToBottom() { try { await JS.InvokeVoidAsync("smartScrollToBottom", messagesContainer); } diff --git a/Components/Pages/Home.razor.css b/Components/Pages/Home.razor.css index cc76a983c2..325c0ddb90 100644 --- a/Components/Pages/Home.razor.css +++ b/Components/Pages/Home.razor.css @@ -3,8 +3,7 @@ flex-direction: column; height: 100%; background: #0f0f23; - color: white; - font-size: 16px; + color: #a0b4cc; } .initializing, .no-session { @@ -14,26 +13,33 @@ justify-content: center; height: 100%; gap: 1rem; - color: rgba(255,255,255,0.7); + color: #c8d8f0; font-size: 18px; } +.welcome-logo { + width: 120px; + height: 120px; + border-radius: 24px; + filter: drop-shadow(0 4px 24px rgba(78,168,209,0.4)); +} + .initializing .error { color: #ef4444; } .initializing button { padding: 0.5rem 1rem; - background: #3b82f6; + background: #4ea8d1; border: none; border-radius: 6px; - color: white; + color: #a0b4cc; cursor: pointer; } .spinner { width: 40px; height: 40px; - border: 3px solid rgba(255,255,255,0.1); - border-top-color: #3b82f6; + border: 3px solid rgba(78,168,209,0.15); + border-top-color: #4ea8d1; border-radius: 50%; animation: spin 1s linear infinite; } @@ -41,7 +47,7 @@ @keyframes spin { to { transform: rotate(360deg); } } .no-session h2 { margin: 0; } -.no-session p { margin: 0; color: rgba(255,255,255,0.5); } +.no-session p { margin: 0; color: #7a85a0; } /* === Inline icons === */ .icon { vertical-align: middle; flex-shrink: 0; } @@ -50,47 +56,88 @@ .chat-header { display: flex; align-items: center; - gap: 1rem; - padding: 1rem 1.5rem; - background: rgba(255,255,255,0.05); - border-bottom: 1px solid rgba(255,255,255,0.1); + gap: 0.75rem; + padding: 0.75rem 1.5rem; + background: rgba(78,168,209,0.06); + border-bottom: 1px solid rgba(78,168,209,0.15); + flex-shrink: 0; + min-width: 0; } -.chat-header h2 { margin: 0; font-size: 1.6rem; } +.chat-header h2 { + margin: 0; + font-size: 1.2rem; + min-width: 0; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-header-badges { + display: flex; + align-items: center; + gap: 0.5rem; + flex-shrink: 0; +} + +.folder-btn { + background: rgba(78,168,209,0.1); + border: 1px solid rgba(78,168,209,0.2); + border-radius: 6px; + color: #8e9abd; + cursor: pointer; + padding: 0.2rem 0.4rem; + display: flex; + align-items: center; + transition: all 0.15s; +} + +.folder-btn:hover { + background: rgba(78,168,209,0.25); + color: #a0b4cc; +} .session-nav-btn { - background: rgba(255,255,255,0.08); - border: 1px solid rgba(255,255,255,0.15); + background: rgba(78,168,209,0.1); + border: 1px solid rgba(78,168,209,0.2); border-radius: 6px; - color: rgba(255,255,255,0.6); + color: #8e9abd; cursor: pointer; font-size: 0.9rem; padding: 0.25rem 0.5rem; transition: all 0.15s; } .session-nav-btn:hover { - background: rgba(59,130,246,0.3); - color: white; - border-color: rgba(59,130,246,0.5); + background: rgba(78,168,209,0.3); + color: #a0b4cc; + border-color: rgba(78,168,209,0.5); } .model-badge { - padding: 0.25rem 0.5rem; - background: rgba(59, 130, 246, 0.2); - border: 1px solid rgba(59, 130, 246, 0.3); + padding: 0.2rem 0.5rem; + background: rgba(78, 168, 209, 0.2); + border: 1px solid rgba(78, 168, 209, 0.3); border-radius: 4px; - font-size: 1rem; - color: #60a5fa; + font-size: 0.85rem; + color: #4ea8d1; + line-height: 1.2; } -.typing-indicator { - margin-left: auto; - font-size: 1.1rem; - color: #fbbf24; - animation: pulse 1.5s infinite; +.processing-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #f59e0b; + box-shadow: 0 0 8px rgba(245,158,11,0.6); + animation: pulse-glow 1.5s ease-in-out infinite; + flex-shrink: 0; } -@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } +@keyframes pulse-glow { + 0%, 100% { opacity: 1; box-shadow: 0 0 8px rgba(245,158,11,0.6); } + 50% { opacity: 0.4; box-shadow: 0 0 4px rgba(245,158,11,0.3); } +} .messages { flex: 1; @@ -106,16 +153,16 @@ align-items: center; justify-content: center; height: 100%; - color: rgba(255,255,255,0.5); + color: #7a85a0; } .load-more-btn { align-self: center; padding: 0.4rem 1rem; - background: rgba(255,255,255,0.06); - border: 1px solid rgba(255,255,255,0.15); + background: rgba(78,168,209,0.08); + border: 1px solid rgba(78,168,209,0.2); border-radius: 6px; - color: rgba(255,255,255,0.6); + color: #8e9abd; font-size: 0.8rem; cursor: pointer; flex-shrink: 0; @@ -125,7 +172,7 @@ } .load-more-btn:hover { - background: rgba(255,255,255,0.1); + background: rgba(78,168,209,0.15); color: rgba(255,255,255,0.85); } @@ -150,7 +197,7 @@ .system-text { font-size: 0.8rem; - color: rgba(255, 255, 255, 0.4); + color: #646e8a; text-align: center; padding: 0.25rem 0; } @@ -161,13 +208,13 @@ display: flex; align-items: center; justify-content: center; - background: rgba(255,255,255,0.1); + background: rgba(78,168,209,0.15); border-radius: 50%; font-size: 1.2rem; flex-shrink: 0; } -.message.user .message-avatar { background: rgba(59, 130, 246, 0.3); } +.message.user .message-avatar { background: rgba(78, 168, 209, 0.3); } .message-content { display: flex; @@ -187,14 +234,14 @@ } .message.user .message-text { - background: #3b82f6; + background: #4ea8d1; border-bottom-right-radius: 4px; } .message.assistant .message-text { - background: rgba(255,255,255,0.1); + background: rgba(78,168,209,0.15); border-bottom-left-radius: 4px; - color: white; + color: #a0b4cc; } .message.streaming .message-text::after { @@ -206,14 +253,14 @@ .message-time { font-size: 0.75rem; - color: rgba(255,255,255,0.35); + color: rgba(200,216,240,0.35); padding: 0 0.5rem; } .message.user .message-time { text-align: right; } /* === Markdown body (::deep for MarkupString inner content) === */ -::deep .markdown-body { color: white; } +::deep .markdown-body { color: #a0b4cc; } ::deep .markdown-body p { margin: 0 0 0.5rem 0; } ::deep .markdown-body p:last-child { margin-bottom: 0; } @@ -251,14 +298,14 @@ ::deep .markdown-body h3 { font-size: 1.05em; } ::deep .markdown-body blockquote { - border-left: 3px solid #60a5fa; + border-left: 3px solid #4ea8d1; margin: 0.5rem 0; padding: 0.4rem 0.75rem; - background: rgba(59, 130, 246, 0.08); + background: rgba(78, 168, 209, 0.08); border-radius: 0 6px 6px 0; } -::deep .markdown-body a { color: #60a5fa; text-decoration: none; } +::deep .markdown-body a { color: #4ea8d1; text-decoration: none; } ::deep .markdown-body a:hover { text-decoration: underline; } ::deep .markdown-body table { @@ -269,12 +316,12 @@ } ::deep .markdown-body th, ::deep .markdown-body td { - border: 1px solid rgba(255,255,255,0.15); + border: 1px solid rgba(78,168,209,0.2); padding: 0.4rem 0.6rem; } ::deep .markdown-body th { - background: rgba(255,255,255,0.08); + background: rgba(78,168,209,0.1); font-weight: 600; } @@ -316,7 +363,7 @@ background: transparent; border: none; cursor: pointer; - color: white; + color: #a0b4cc; font-size: 0.85rem; } @@ -329,14 +376,14 @@ .collapse-icon { font-size: 0.7rem; - color: rgba(255,255,255,0.4); + color: #646e8a; } .reasoning-content { padding: 0 0.75rem 0.75rem 0.75rem; font-size: 0.85rem; line-height: 1.5; - color: rgba(255,255,255,0.6); + color: #8e9abd; font-style: italic; white-space: pre-wrap; word-break: break-word; @@ -361,7 +408,7 @@ gap: 0.4rem; padding: 0.25rem 0.75rem; font-size: 0.8rem; - color: rgba(255,255,255,0.4); + color: #646e8a; max-width: 90%; } @@ -370,15 +417,15 @@ /* === Tool cards === */ .tool-card { border-radius: 8px; - border: 1px solid rgba(255,255,255,0.15); + border: 1px solid rgba(78,168,209,0.2); min-width: 0; align-self: stretch; flex-shrink: 0; overflow: hidden; - background: rgba(255,255,255,0.04); + background: rgba(78,168,209,0.05); } -.tool-card.running { border-color: rgba(59, 130, 246, 0.4); } +.tool-card.running { border-color: rgba(78, 168, 209, 0.4); } .tool-card.success { border-color: rgba(34, 197, 94, 0.35); } .tool-card.error { border-color: rgba(239, 68, 68, 0.35); } @@ -392,15 +439,15 @@ .tool-info { font-weight: 600; - color: rgba(255,255,255,0.8); + color: #c8d8f0; } -.tool-status { font-size: 0.8rem; color: rgba(255,255,255,0.5); } -.tool-card.running .tool-status { color: #60a5fa; } -.tool-card.success .tool-status { color: #4ade80; } +.tool-status { font-size: 0.8rem; color: #7a85a0; } +.tool-card.running .tool-status { color: #4ea8d1; } +.tool-card.success .tool-status { color: #00c35b; } .tool-card.error .tool-status { color: #f87171; } -.tool-result-section { border-top: 1px solid rgba(255,255,255,0.08); } +.tool-result-section { border-top: 1px solid rgba(78,168,209,0.1); } .tool-result-toggle { width: 100%; @@ -409,18 +456,18 @@ border: none; cursor: pointer; font-size: 0.75rem; - color: rgba(255,255,255,0.4); + color: #646e8a; text-align: left; } -.tool-result-toggle:hover { background: rgba(255,255,255,0.05); } +.tool-result-toggle:hover { background: rgba(78,168,209,0.06); } .tool-result-content { margin: 0; padding: 0.5rem 0.75rem; font-size: 0.8rem; background: rgba(0,0,0,0.2); - color: rgba(255,255,255,0.6); + color: #8e9abd; white-space: pre-wrap; word-break: break-word; overflow-wrap: break-word; @@ -439,7 +486,7 @@ .tool-image-result { padding: 0.5rem; - border-top: 1px solid rgba(255,255,255,0.08); + border-top: 1px solid rgba(78,168,209,0.1); } .tool-image-result img { @@ -506,7 +553,7 @@ padding: 0 0.25rem; padding-bottom: var(--nav-bar-height, 0px); font-size: 0.75rem; - color: rgba(255,255,255,0.35); + color: rgba(200,216,240,0.35); } .plan-icon-toggle { @@ -514,25 +561,123 @@ display: flex; align-items: center; gap: 0.3rem; - color: rgba(255,255,255,0.35); + color: rgba(200,216,240,0.35); font-size: 0.75rem; cursor: pointer; transition: color 0.15s ease; } .plan-icon-toggle:hover { - color: rgba(255,255,255,0.55); + color: #8e9abd; } .plan-icon-toggle.active { - color: #60a5fa; + color: #4ea8d1; +} + +.mode-switcher { + display: flex; + background: rgba(78,168,209,0.08); + border: 1px solid rgba(78,168,209,0.15); + border-radius: 6px; + overflow: hidden; +} + +.mode-btn { + all: unset; + display: flex; + align-items: center; + gap: 0.2rem; + padding: 0.1rem 0.35rem; + font-size: 0.7rem; + color: rgba(200,216,240,0.4); + cursor: pointer; + transition: all 0.15s ease; +} + +.mode-btn svg { + width: 11px; + height: 11px; +} + +.mode-btn:hover { + color: #8e9abd; + background: rgba(78,168,209,0.1); } -.status-model { color: rgba(59, 130, 246, 0.6); } +.mode-btn.active { + color: #4ea8d1; + background: rgba(78,168,209,0.2); +} + +.inline-model-select { + all: unset; + font-size: 0.75rem; + color: rgba(78, 168, 209, 0.6); + cursor: pointer; + background: transparent; + border: none; + padding: 0.1rem 0; +} + +.inline-model-select:hover { + color: #4ea8d1; +} + +.inline-model-select option { + background: #1a1e3a; + color: #a0b4cc; +} .status-tokens { } .status-ctx { } .status-msgs { } -.status-sep { color: rgba(255,255,255,0.15); user-select: none; } +.status-sep { color: rgba(78,168,209,0.2); user-select: none; } + +/* === Font size controls === */ +.font-size-controls { + display: inline-flex; + align-items: center; + gap: 0.15rem; +} + +.font-size-btn { + background: rgba(78,168,209,0.1); + border: 1px solid rgba(78,168,209,0.2); + border-radius: 4px; + color: #8e9abd; + cursor: pointer; + font-size: 0.65rem; + font-weight: 600; + padding: 0.1rem 0.3rem; + line-height: 1; + transition: all 0.15s; +} + +.font-size-btn:hover:not(:disabled) { + background: rgba(78,168,209,0.25); + color: #c8d8f0; + border-color: rgba(78,168,209,0.4); +} + +.font-size-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.font-size-label { + font-size: 0.65rem; + color: #646e8a; + min-width: 2.2em; + text-align: center; + cursor: pointer; + padding: 0.1rem 0.15rem; + border-radius: 3px; + transition: color 0.15s; +} + +.font-size-label:hover { + color: #c8d8f0; +} /* === Input area === */ .input-area { @@ -541,19 +686,19 @@ gap: 0.4rem; padding: 0.5rem 0.75rem; padding-bottom: calc(0.5rem + var(--nav-bar-height, 0px)); - background: rgba(255,255,255,0.05); - border-top: 1px solid rgba(255,255,255,0.1); + background: rgba(78,168,209,0.06); + border-top: 1px solid rgba(78,168,209,0.15); } -.input-row { display: flex; gap: 0.5rem; } +.input-row { display: flex; gap: 0.5rem; align-items: stretch; } .input-area textarea { flex: 1; padding: 0.45rem 0.6rem; - border: 1px solid rgba(255,255,255,0.2); + border: 1px solid rgba(78,168,209,0.25); border-radius: 8px; - background: rgba(255,255,255,0.1); - color: white; + background: rgba(78,168,209,0.15); + color: #a0b4cc; font-size: 0.9rem; font-family: inherit; resize: none; @@ -561,16 +706,16 @@ max-height: 150px; } -.input-area textarea::placeholder { color: rgba(255,255,255,0.4); } -.input-area textarea:focus { outline: none; border-color: #3b82f6; } +.input-area textarea::placeholder { color: #646e8a; } +.input-area textarea:focus { outline: none; border-color: #4ea8d1; } .input-area textarea:disabled { opacity: 0.6; } -.input-area .input-row button { +.input-area .input-row button.send-btn { padding: 0.45rem 0.7rem; - border: none; + border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; - background: #3b82f6; - color: white; + background: transparent; + color: rgba(255,255,255,0.6); font-size: 0.9rem; font-weight: 500; cursor: pointer; @@ -578,18 +723,25 @@ display: flex; align-items: center; justify-content: center; + flex-shrink: 0; } -.input-area .input-row button:hover:not(:disabled) { background: #2563eb; } -.input-area .input-row button:disabled { opacity: 0.5; cursor: not-allowed; } +.input-area .input-row button.send-btn:hover:not(:disabled) { background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.8); } +.input-area .input-row button.send-btn:disabled { opacity: 0.3; cursor: not-allowed; } .input-area .input-row .stop-btn { - padding: 0.75rem; - background: #ef4444; + padding: 0.35rem 0.6rem; + background: transparent; + border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; min-width: auto; + color: rgba(255,255,255,0.35); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; } -.input-area .input-row .stop-btn:hover { background: #dc2626; } +.input-area .input-row .stop-btn:hover { background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.5); border-color: rgba(255,255,255,0.3); } /* === Drag & drop / image attachments === */ .input-area.drag-over { @@ -700,11 +852,11 @@ padding: 0 0.5rem; } -.error-bar button:hover { color: white; } +.error-bar button:hover { color: #a0b4cc; } /* === Badges === */ .session-id-badge { - padding: 0.25rem 0.5rem; + padding: 0.2rem 0.5rem; background: rgba(251, 191, 36, 0.2); border: 1px solid rgba(251, 191, 36, 0.3); border-radius: 4px; @@ -713,6 +865,7 @@ font-family: monospace; cursor: pointer; transition: background 0.15s ease; + line-height: 1.2; } .session-id-badge:hover { background: rgba(251, 191, 36, 0.3); } @@ -760,19 +913,19 @@ background: #333; border: 1px solid #555; border-radius: 4px; - color: white; + color: #a0b4cc; width: 120px; } -.debug-filter::placeholder { color: rgba(255,255,255,0.35); } -.debug-filter:focus { outline: none; border-color: #60a5fa; } +.debug-filter::placeholder { color: rgba(200,216,240,0.35); } +.debug-filter:focus { outline: none; border-color: #4ea8d1; } .debug-header button { padding: 0.15rem 0.4rem; font-size: 0.65rem; background: #444; border: none; - color: white; + color: #a0b4cc; border-radius: 4px; cursor: pointer; } @@ -793,9 +946,9 @@ /* === Message queue === */ .message-queue { - border: 1px solid rgba(59, 130, 246, 0.25); + border: 1px solid rgba(78, 168, 209, 0.25); border-radius: 8px; - background: rgba(59, 130, 246, 0.08); + background: rgba(78, 168, 209, 0.08); overflow: hidden; } @@ -805,8 +958,8 @@ align-items: center; padding: 0.4rem 0.75rem; font-size: 0.85rem; - color: rgba(255,255,255,0.6); - border-bottom: 1px solid rgba(59, 130, 246, 0.15); + color: #8e9abd; + border-bottom: 1px solid rgba(78, 168, 209, 0.15); } .queue-clear-btn { @@ -827,13 +980,13 @@ gap: 0.5rem; padding: 0.35rem 0.75rem; font-size: 0.9rem; - border-bottom: 1px solid rgba(255,255,255,0.05); + border-bottom: 1px solid rgba(78,168,209,0.06); } .queue-item:last-child { border-bottom: none; } .queue-index { - color: rgba(59, 130, 246, 0.7); + color: rgba(78, 168, 209, 0.7); font-weight: 600; font-size: 0.8rem; min-width: 1.2rem; @@ -841,7 +994,7 @@ .queue-text { flex: 1; - color: rgba(255,255,255,0.7); + color: #c8d8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -852,7 +1005,7 @@ font-size: 0.85rem !important; background: transparent !important; border: none !important; - color: rgba(255,255,255,0.3) !important; + color: rgba(200,216,240,0.3) !important; cursor: pointer; border-radius: 3px !important; } @@ -872,7 +1025,6 @@ .model-badge { font-size: 0.7rem; padding: 0.15rem 0.35rem; } .session-id-badge { font-size: 0.65rem; padding: 0.15rem 0.35rem; } .resumed-badge { font-size: 0.55rem; } - .typing-indicator { font-size: 0.85rem; } .messages { padding: 0.5rem; diff --git a/Components/Pages/Settings.razor b/Components/Pages/Settings.razor index 90641283f6..67fda70de1 100644 --- a/Components/Pages/Settings.razor +++ b/Components/Pages/Settings.razor @@ -182,7 +182,7 @@

Current Connection

Mode: @CopilotService.CurrentMode

-

Status: @if (CopilotService.IsInitialized) {Connected} else {Disconnected}

+

Status: @if (CopilotService.IsInitialized) { Connected} else { Disconnected}

About Transport Modes

    diff --git a/Components/Pages/Settings.razor.css b/Components/Pages/Settings.razor.css index 14fb1aa8e6..345355e3cf 100644 --- a/Components/Pages/Settings.razor.css +++ b/Components/Pages/Settings.razor.css @@ -1,6 +1,6 @@ .settings-page { padding: 1.5rem; - color: white; + color: #a0b4cc; background: #0f0f23; height: 100%; overflow-y: auto; @@ -116,7 +116,7 @@ border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; background: rgba(255,255,255,0.08); - color: white; + color: #a0b4cc; font-size: 0.95rem; } @@ -159,7 +159,7 @@ .check-btn:hover:not(:disabled) { border-color: rgba(255,255,255,0.4); - color: white; + color: #a0b4cc; } .server-actions { @@ -180,7 +180,7 @@ .action-btn.start { background: #48bb78; - color: white; + color: #a0b4cc; } .action-btn.start:hover:not(:disabled) { @@ -194,7 +194,7 @@ .action-btn.stop { background: rgba(239, 68, 68, 0.8); - color: white; + color: #a0b4cc; } .action-btn.stop:hover { @@ -218,7 +218,7 @@ border: none; border-radius: 8px; background: #3b82f6; - color: white; + color: #a0b4cc; font-size: 1rem; font-weight: 500; cursor: pointer; @@ -285,7 +285,7 @@ border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; background: rgba(255,255,255,0.08); - color: white; + color: #a0b4cc; font-size: 0.95rem; } @@ -310,7 +310,7 @@ .start-btn { background: #48bb78; - color: white; + color: #a0b4cc; } .start-btn:hover:not(:disabled) { @@ -324,7 +324,7 @@ .stop-btn { background: rgba(239, 68, 68, 0.8); - color: white; + color: #a0b4cc; } .stop-btn:hover { diff --git a/MauiProgram.cs b/MauiProgram.cs index 4b261cc359..796a8e078f 100644 --- a/MauiProgram.cs +++ b/MauiProgram.cs @@ -3,6 +3,10 @@ using ZXing.Net.Maui.Controls; using MauiDevFlow.Agent; using MauiDevFlow.Blazor; +#if MACCATALYST +using Microsoft.Maui.LifecycleEvents; +using UIKit; +#endif namespace AutoPilot.App; @@ -37,6 +41,24 @@ public static MauiApp CreateMauiApp() fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); +#if MACCATALYST + builder.ConfigureLifecycleEvents(events => + { + events.AddiOS(ios => ios.SceneWillConnect((scene, session, options) => + { + if (scene is UIWindowScene windowScene) + { + var titlebar = windowScene.Titlebar; + if (titlebar != null) + { + titlebar.TitleVisibility = UITitlebarTitleVisibility.Hidden; + titlebar.Toolbar = null; + } + } + })); + }); +#endif + builder.Services.AddMauiBlazorWebView(); // Register CopilotService as singleton so state is shared across components diff --git a/Models/AgentSessionInfo.cs b/Models/AgentSessionInfo.cs index ce4fbac39a..bb7f614d30 100644 --- a/Models/AgentSessionInfo.cs +++ b/Models/AgentSessionInfo.cs @@ -3,7 +3,7 @@ namespace AutoPilot.App.Models; public class AgentSessionInfo { public required string Name { get; set; } - public required string Model { get; init; } + public required string Model { get; set; } public DateTime CreatedAt { get; init; } public int MessageCount { get; set; } public bool IsProcessing { get; set; } diff --git a/Models/ChatMessage.cs b/Models/ChatMessage.cs index d67e01d150..df17fefa1a 100644 --- a/Models/ChatMessage.cs +++ b/Models/ChatMessage.cs @@ -34,8 +34,9 @@ public ChatMessage(string role, string content, DateTime timestamp, ChatMessageT // Tool call fields public string? ToolName { get; set; } public string? ToolCallId { get; set; } + public string? ToolInput { get; set; } public bool IsComplete { get; set; } = true; - public bool IsCollapsed { get; set; } + public bool IsCollapsed { get; set; } = true; public bool IsSuccess { get; set; } // Reasoning fields @@ -55,8 +56,8 @@ public static ChatMessage AssistantMessage(string content) => public static ChatMessage ReasoningMessage(string reasoningId) => new("assistant", "", DateTime.Now, ChatMessageType.Reasoning) { ReasoningId = reasoningId, IsComplete = false, IsCollapsed = false }; - public static ChatMessage ToolCallMessage(string toolName, string? toolCallId = null) => - new("assistant", "", DateTime.Now, ChatMessageType.ToolCall) { ToolName = toolName, ToolCallId = toolCallId, IsComplete = false }; + public static ChatMessage ToolCallMessage(string toolName, string? toolCallId = null, string? toolInput = null) => + new("assistant", "", DateTime.Now, ChatMessageType.ToolCall) { ToolName = toolName, ToolCallId = toolCallId, ToolInput = toolInput, IsComplete = false }; public static ChatMessage ErrorMessage(string content, string? toolName = null) => new("assistant", content, DateTime.Now, ChatMessageType.Error) { ToolName = toolName, IsComplete = true }; @@ -64,3 +65,25 @@ public static ChatMessage ErrorMessage(string content, string? toolName = null) public static ChatMessage SystemMessage(string content) => new("system", content, DateTime.Now, ChatMessageType.System) { IsComplete = true }; } + +public class ToolActivity +{ + public string Name { get; set; } = ""; + public string CallId { get; set; } = ""; + public string? Input { get; set; } + public DateTime StartedAt { get; set; } + public bool IsComplete { get; set; } + public bool IsSuccess { get; set; } + public string? Result { get; set; } + public DateTime? CompletedAt { get; set; } + + public string ElapsedDisplay + { + get + { + var end = CompletedAt ?? DateTime.Now; + var elapsed = end - StartedAt; + return elapsed.TotalSeconds < 1 ? "<1s" : $"{elapsed.TotalSeconds:F0}s"; + } + } +} diff --git a/Platforms/MacCatalyst/Info.plist b/Platforms/MacCatalyst/Info.plist index 7b1e6e6219..a36da199d5 100644 --- a/Platforms/MacCatalyst/Info.plist +++ b/Platforms/MacCatalyst/Info.plist @@ -34,5 +34,9 @@ XSAppIconAssets Assets.xcassets/appicon.appiconset + CFBundleDisplayName + AutoPilot + CFBundleName + AutoPilot diff --git a/Resources/AppIcon/CustomAssets.car b/Resources/AppIcon/CustomAssets.car new file mode 100644 index 0000000000..11bc4923bb Binary files /dev/null and b/Resources/AppIcon/CustomAssets.car differ diff --git a/Resources/AppIcon/appicon.icns b/Resources/AppIcon/appicon.icns new file mode 100644 index 0000000000..1a8cc8be90 Binary files /dev/null and b/Resources/AppIcon/appicon.icns differ diff --git a/Resources/AppIcon/appicon.png b/Resources/AppIcon/appicon.png new file mode 100644 index 0000000000..fdb07e8776 Binary files /dev/null and b/Resources/AppIcon/appicon.png differ diff --git a/Resources/AppIcon/appicon.svg b/Resources/AppIcon/appicon.svg deleted file mode 100644 index 9d63b6513a..0000000000 --- a/Resources/AppIcon/appicon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Resources/AppIcon/appiconfg.svg b/Resources/AppIcon/appiconfg.svg deleted file mode 100644 index 21dfb25f18..0000000000 --- a/Resources/AppIcon/appiconfg.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Resources/Images/autopilot_logo.png b/Resources/Images/autopilot_logo.png new file mode 100644 index 0000000000..87cddefc40 Binary files /dev/null and b/Resources/Images/autopilot_logo.png differ diff --git a/Services/CopilotService.cs b/Services/CopilotService.cs index e6a8cce711..011f7606b1 100644 --- a/Services/CopilotService.cs +++ b/Services/CopilotService.cs @@ -51,8 +51,9 @@ private static string GetCopilotBaseDir() private static string? _activeSessionsFile; private static string ActiveSessionsFile => _activeSessionsFile ??= Path.Combine(CopilotBaseDir, "autopilot-active-sessions.json"); - private static string? _uiStateFile; - private static string UiStateFile => _uiStateFile ??= Path.Combine(CopilotBaseDir, "autopilot-ui-state.json"); + private static readonly string SessionAliasesFile = Path.Combine(CopilotBaseDir, "autopilot-session-aliases.json"); + + private static readonly string UiStateFile = Path.Combine(CopilotBaseDir, "autopilot-ui-state.json"); private static string? _projectDir; private static string ProjectDir => _projectDir ??= FindProjectDir(); @@ -105,7 +106,7 @@ public CopilotService(ChatDatabase chatDb, ServerManager serverManager, WsBridge public event Action? OnDebug; // debug messages // Rich event types - public event Action? OnToolStarted; // sessionName, toolName, callId + public event Action? OnToolStarted; // sessionName, toolName, callId, inputSummary public event Action? OnToolCompleted; // sessionName, callId, result, success public event Action? OnReasoningReceived; // sessionName, reasoningId, deltaContent public event Action? OnReasoningComplete; // sessionName, reasoningId @@ -256,7 +257,7 @@ private async Task InitializeRemoteAsync(ConnectionSettings settings, Cancellati { var session = GetRemoteSession(s); session?.History.Add(ChatMessage.ToolCallMessage(tool, id)); - InvokeOnUI(() => OnToolStarted?.Invoke(s, tool, id)); + InvokeOnUI(() => OnToolStarted?.Invoke(s, tool, id, null)); }; _bridgeClient.OnToolCompleted += (s, id, result, success) => { @@ -527,12 +528,25 @@ private PersistedSessionInfo CreatePersistedSessionInfo(DirectoryInfo di) var eventsFileInfo = new FileInfo(eventsFile); var lastUsed = eventsFileInfo.Exists ? eventsFileInfo.LastWriteTime : di.LastWriteTime; + // Priority: alias > active session name > first message > "Untitled session" + var alias = GetSessionAlias(di.Name); + string resolvedTitle; + if (!string.IsNullOrEmpty(alias)) + resolvedTitle = alias; + else if (title != null) + resolvedTitle = title; + else + { + var activeMatch = _sessions.Values.FirstOrDefault(s => s.Info.SessionId == di.Name); + resolvedTitle = activeMatch?.Info.Name ?? "Untitled session"; + } + return new PersistedSessionInfo { SessionId = di.Name, LastModified = lastUsed, Path = di.FullName, - Title = title ?? "Untitled session", + Title = resolvedTitle, Preview = preview ?? "No preview available", WorkingDirectory = workingDir }; @@ -697,7 +711,14 @@ private List LoadHistoryFromDisk(string sessionId) // Skip report_intent β€” it's noise in history if (toolName == "report_intent") break; - var msg = ChatMessage.ToolCallMessage(toolName, toolCallId); + // Extract tool input if available + string? inputStr = null; + if (data.TryGetProperty("input", out var inputEl)) + inputStr = inputEl.ToString(); + else if (data.TryGetProperty("arguments", out var argsEl)) + inputStr = argsEl.ToString(); + + var msg = ChatMessage.ToolCallMessage(toolName, toolCallId, inputStr); msg.Timestamp = timestamp; history.Add(msg); if (toolCallId != null) @@ -784,8 +805,19 @@ public async Task ResumeSessionAsync(string sessionId, string } info.MessageCount = info.History.Count; + // Mark any stale incomplete tool calls as complete (from prior session) + foreach (var msg in info.History.Where(m => m.MessageType == ChatMessageType.ToolCall && !m.IsComplete)) + { + msg.IsComplete = true; + } + // Also mark incomplete reasoning as complete + foreach (var msg in info.History.Where(m => m.MessageType == ChatMessageType.Reasoning && !m.IsComplete)) + { + msg.IsComplete = true; + } + // Add reconnection indicator with status context - var reconnectMsg = "πŸ”„ Session reconnected"; + var reconnectMsg = $"πŸ”„ Session reconnected at {DateTime.Now:h:mm tt}"; var isStillProcessing = IsSessionStillProcessing(sessionId); if (isStillProcessing) { @@ -894,6 +926,10 @@ ALWAYS run the relaunch script as the final step after making changes to this pr Debug($"Session '{name}' created with ID: {copilotSession.SessionId}"); + // Save alias so saved sessions show the custom name + if (!string.IsNullOrEmpty(copilotSession.SessionId)) + SetSessionAlias(copilotSession.SessionId, name); + var state = new SessionState { Session = copilotSession, @@ -967,11 +1003,16 @@ void Invoke(Action action) if (toolStart.Data == null) break; var startToolName = toolStart.Data.ToolName ?? "unknown"; var startCallId = toolStart.Data.ToolCallId ?? ""; + var toolInput = ExtractToolInput(toolStart.Data); if (!FilteredTools.Contains(startToolName)) { + // Add to session history + var toolMsg = ChatMessage.ToolCallMessage(startToolName, startCallId, toolInput); + state.Info.History.Add(toolMsg); + Invoke(() => { - OnToolStarted?.Invoke(sessionName, startToolName, startCallId); + OnToolStarted?.Invoke(sessionName, startToolName, startCallId, toolInput); OnActivity?.Invoke(sessionName, $"πŸ”§ Running {startToolName}..."); }); } @@ -984,32 +1025,21 @@ void Invoke(Action action) var resultStr = FormatToolResult(toolDone.Data.Result); var hasError = toolDone.Data.Error != null; - // Log raw result type for debugging - var rawResult = toolDone.Data.Result; - if (rawResult != null) - { - var resultType = rawResult.GetType(); - Invoke(() => OnDebug?.Invoke($"[ToolResult] {completeToolName} callId={completeCallId} type={resultType.FullName}")); - // Log all properties - foreach (var prop in resultType.GetProperties()) - { - try - { - var val = prop.GetValue(rawResult); - var valStr = val?.ToString() ?? "null"; - if (valStr.Length > 200) valStr = valStr[..200] + "..."; - Invoke(() => OnDebug?.Invoke($" .{prop.Name} = {valStr}")); - } - catch { } - } - } - // Skip filtered tools if (completeToolName != null && FilteredTools.Contains(completeToolName)) break; if (resultStr == "Intent logged") break; + // Update the matching tool message in history + var histToolMsg = state.Info.History.LastOrDefault(m => m.ToolCallId == completeCallId); + if (histToolMsg != null) + { + histToolMsg.IsComplete = true; + histToolMsg.IsSuccess = !hasError; + histToolMsg.Content = resultStr; + } + Invoke(() => { OnToolCompleted?.Invoke(sessionName, completeCallId, resultStr, !hasError); @@ -1064,6 +1094,8 @@ void Invoke(Action action) var uTokenLimit = uData?.GetType().GetProperty("TokenLimit")?.GetValue(uData) as int?; var uInputTokens = uData?.GetType().GetProperty("InputTokens")?.GetValue(uData) as int?; var uOutputTokens = uData?.GetType().GetProperty("OutputTokens")?.GetValue(uData) as int?; + if (!string.IsNullOrEmpty(uModel) && state.Info.Model == "resumed") + state.Info.Model = uModel; Invoke(() => OnUsageInfoChanged?.Invoke(sessionName, new SessionUsageInfo(uModel, uCurrentTokens, uTokenLimit, uInputTokens, uOutputTokens))); break; @@ -1072,6 +1104,8 @@ void Invoke(Action action) var aModel = aData?.GetType().GetProperty("Model")?.GetValue(aData)?.ToString(); var aInput = aData?.GetType().GetProperty("InputTokens")?.GetValue(aData) as int?; var aOutput = aData?.GetType().GetProperty("OutputTokens")?.GetValue(aData) as int?; + if (!string.IsNullOrEmpty(aModel) && state.Info.Model == "resumed") + state.Info.Model = aModel; if (aInput.HasValue || aOutput.HasValue) { Invoke(() => OnUsageInfoChanged?.Invoke(sessionName, new SessionUsageInfo(aModel, null, null, aInput, aOutput))); @@ -1114,8 +1148,36 @@ private static string FormatToolResult(object? result) return result.ToString() ?? ""; } + private static string? ExtractToolInput(object? data) + { + if (data == null) return null; + try + { + var type = data.GetType(); + // Try common property names for tool input/arguments + foreach (var propName in new[] { "Input", "Arguments", "Args", "Parameters", "input", "arguments" }) + { + var prop = type.GetProperty(propName); + if (prop == null) continue; + var val = prop.GetValue(data); + if (val == null) continue; + if (val is string s && !string.IsNullOrEmpty(s)) return s; + try + { + var json = JsonSerializer.Serialize(val, new JsonSerializerOptions { WriteIndented = false }); + if (json != "{}" && json != "null" && json != "\"\"") return json; + } + catch { return val.ToString(); } + } + } + catch { } + return null; + } + private void CompleteResponse(SessionState state) { + if (!state.Info.IsProcessing) return; // Already completed (e.g. timeout) + var response = state.CurrentResponse.ToString(); if (!string.IsNullOrEmpty(response)) { @@ -1146,7 +1208,6 @@ private void CompleteResponse(SessionState state) { try { - // Small delay to let UI update await Task.Delay(500); await SendPromptAsync(state.Info.Name, nextPrompt); } @@ -1251,18 +1312,6 @@ await state.Session.SendAsync(new MessageOptions Console.WriteLine($"[DEBUG] SendAsync completed, waiting for response..."); - // Add timeout - if no response in 120 seconds, something is wrong - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cts.CancelAfter(TimeSpan.FromSeconds(120)); - cts.Token.Register(() => - { - if (!state.ResponseCompletion!.Task.IsCompleted) - { - OnError?.Invoke(sessionName, "Response timeout after 120 seconds"); - state.ResponseCompletion.TrySetCanceled(); - } - }); - return await state.ResponseCompletion.Task; } @@ -1366,6 +1415,10 @@ public bool RenameSession(string oldName, string newName) if (_activeSessionName == oldName) _activeSessionName = newName; + // Persist alias so saved sessions also show the custom name + if (state.Info.SessionId != null) + SetSessionAlias(state.Info.SessionId, newName); + SaveActiveSessionsToDisk(); OnStateChanged?.Invoke(); return true; @@ -1521,14 +1574,16 @@ public async ValueTask DisposeAsync() } } - public void SaveUiState(string currentPage, string? activeSession = null) + public void SaveUiState(string currentPage, string? activeSession = null, int? fontSize = null) { try { + var existing = LoadUiState(); var state = new UiState { CurrentPage = currentPage, - ActiveSession = activeSession ?? _activeSessionName + ActiveSession = activeSession ?? _activeSessionName, + FontSize = fontSize ?? existing?.FontSize ?? 20 }; var json = JsonSerializer.Serialize(state); File.WriteAllText(UiStateFile, json); @@ -1546,12 +1601,55 @@ public void SaveUiState(string currentPage, string? activeSession = null) } catch { return null; } } + // --- Session Aliases --- + + private Dictionary? _aliasCache; + + private Dictionary LoadAliases() + { + if (_aliasCache != null) return _aliasCache; + try + { + if (File.Exists(SessionAliasesFile)) + { + var json = File.ReadAllText(SessionAliasesFile); + _aliasCache = JsonSerializer.Deserialize>(json) ?? new(); + return _aliasCache; + } + } + catch { } + _aliasCache = new(); + return _aliasCache; + } + + public string? GetSessionAlias(string sessionId) + { + var aliases = LoadAliases(); + return aliases.TryGetValue(sessionId, out var alias) ? alias : null; + } + + public void SetSessionAlias(string sessionId, string alias) + { + var aliases = LoadAliases(); + if (string.IsNullOrWhiteSpace(alias)) + aliases.Remove(sessionId); + else + aliases[sessionId] = alias.Trim(); + _aliasCache = aliases; + try + { + var json = JsonSerializer.Serialize(aliases, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(SessionAliasesFile, json); + } + catch { } + } } public class UiState { public string CurrentPage { get; set; } = "/"; public string? ActiveSession { get; set; } + public int FontSize { get; set; } = 20; } public class ActiveSessionEntry diff --git a/Services/WsBridgeServer.cs b/Services/WsBridgeServer.cs index 2f6fec2c9d..0a928e2edb 100644 --- a/Services/WsBridgeServer.cs +++ b/Services/WsBridgeServer.cs @@ -81,7 +81,7 @@ public void SetCopilotService(CopilotService copilot) _copilot.OnContentReceived += (session, content) => Broadcast(BridgeMessage.Create(BridgeMessageTypes.ContentDelta, new ContentDeltaPayload { SessionName = session, Content = content })); - _copilot.OnToolStarted += (session, tool, callId) => + _copilot.OnToolStarted += (session, tool, callId, input) => Broadcast(BridgeMessage.Create(BridgeMessageTypes.ToolStarted, new ToolStartedPayload { SessionName = session, ToolName = tool, CallId = callId })); _copilot.OnToolCompleted += (session, callId, result, success) => diff --git a/relaunch.sh b/relaunch.sh index f5e17f06b2..cc1cbbded3 100755 --- a/relaunch.sh +++ b/relaunch.sh @@ -5,15 +5,15 @@ set -e PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)" BUILD_DIR="$PROJECT_DIR/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64" -APP_NAME="AutoPilot.App.app" +APP_NAME="AutoPilot.app" STAGING_DIR="$PROJECT_DIR/bin/staging" # Capture PIDs of currently running instances BEFORE launch -OLD_PIDS=$(ps -eo pid,comm | grep "AutoPilot.App" | grep -v grep | awk '{print $1}' | tr '\n' ' ') +OLD_PIDS=$(ps -eo pid,comm | grep "AutoPilot" | grep -v grep | grep -v "AutoPilot.App.csproj" | awk '{print $1}' | tr '\n' ' ') echo "πŸ”¨ Building..." cd "$PROJECT_DIR" -dotnet build -f net10.0-maccatalyst 2>&1 | tail -8 +dotnet build AutoPilot.App.csproj -f net10.0-maccatalyst 2>&1 | tail -8 echo "πŸ“¦ Copying to staging..." rm -rf "$STAGING_DIR/$APP_NAME" @@ -27,7 +27,7 @@ open -n "$STAGING_DIR/$APP_NAME" echo "⏳ Waiting for new instance to start..." for i in $(seq 1 30); do sleep 1 - NEW_PIDS=$(ps -eo pid,comm | grep "AutoPilot.App" | grep -v grep | awk '{print $1}') + NEW_PIDS=$(ps -eo pid,comm | grep "AutoPilot" | grep -v grep | grep -v "AutoPilot.App.csproj" | awk '{print $1}') for PID in $NEW_PIDS; do # Check if this PID is NOT in the old set β€” it's the new instance if ! echo "$OLD_PIDS" | grep -qw "$PID"; then diff --git a/wwwroot/app.css b/wwwroot/app.css index 744dc2427b..3e501426b6 100644 --- a/wwwroot/app.css +++ b/wwwroot/app.css @@ -1,6 +1,7 @@ html, body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; - font-size: 16px; + font-size: var(--app-font-size, 20px); + color: #a0b4cc; /* Android bottom nav bar safe area */ padding-bottom: var(--nav-bar-height, env(safe-area-inset-bottom, 0px)); } @@ -63,7 +64,7 @@ h1:focus { .blazor-error-boundary { background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; padding: 1rem 1rem 1rem 3.7rem; - color: white; + color: #a0b4cc; } .blazor-error-boundary::after { @@ -123,7 +124,7 @@ h1:focus { bottom: 20px; right: 20px; background: linear-gradient(135deg, #2d3748, #1a202c); - color: white; + color: #a0b4cc; padding: 16px 24px; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.3); diff --git a/wwwroot/autopilot_logo.png b/wwwroot/autopilot_logo.png new file mode 100644 index 0000000000..bd8a6719e7 Binary files /dev/null and b/wwwroot/autopilot_logo.png differ diff --git a/wwwroot/autopilot_logo_lg.png b/wwwroot/autopilot_logo_lg.png new file mode 100644 index 0000000000..725865aacd Binary files /dev/null and b/wwwroot/autopilot_logo_lg.png differ diff --git a/wwwroot/autopilot_text.png b/wwwroot/autopilot_text.png new file mode 100644 index 0000000000..04f9852885 Binary files /dev/null and b/wwwroot/autopilot_text.png differ diff --git a/wwwroot/index.html b/wwwroot/index.html index 5b87b5859a..908bf15b52 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -48,6 +48,8 @@ element.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); + // Reset height after send + setTimeout(function() { element.style.height = 'auto'; }, 50); } // Tab / Shift+Tab to cycle sessions if (e.key === 'Tab' && window._tabDotNetRef) { @@ -56,8 +58,14 @@ window._tabDotNetRef.invokeMethodAsync('CycleSession', !e.shiftKey); } }); + element.addEventListener('input', function() { + element.style.height = 'auto'; + element.style.height = Math.min(element.scrollHeight, 200) + 'px'; + }); }; + // Auto-scroll overflowed text on hover + window.scrollToBottom = function(element) { if (element) { element.scrollTop = element.scrollHeight; @@ -69,7 +77,9 @@ var threshold = 150; var isAtBottom = (element.scrollHeight - element.scrollTop - element.clientHeight) < threshold; if (isAtBottom) { - element.scrollTop = element.scrollHeight; + requestAnimationFrame(function() { + element.scrollTop = element.scrollHeight; + }); } }; @@ -221,6 +231,10 @@ return results; }); }; + + window.setAppFontSize = function(size) { + document.documentElement.style.setProperty('--app-font-size', size + 'px'); + };