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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AutoPilot.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@

<ItemGroup>
<PackageReference Include="GitHub.Copilot.SDK" Version="0.1.22" />
<PackageReference Include="Markdig" Version="0.40.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
Expand Down
80 changes: 69 additions & 11 deletions Components/Layout/SessionSidebar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
@using AutoPilot.App.Models
@inject CopilotService CopilotService
@inject IJSRuntime JS
@inject NavigationManager Nav

<div class="sidebar-content">
<div class="sidebar-header">
<h3>🤖 AutoPilot</h3>
<h3><svg style="vertical-align:middle" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect x="2" y="8" width="20" height="8" rx="2"/><path d="M6 16v4"/><path d="M18 16v4"/><circle cx="9" cy="12" r="1" fill="currentColor"/><circle cx="15" cy="12" r="1" fill="currentColor"/></svg> AutoPilot</h3>
<p class="status @(CopilotService.IsInitialized ? "connected" : "disconnected")">
@(CopilotService.IsInitialized ? "● Connected" : "○ Disconnected")
</p>
<div class="nav-tabs">
<a href="/" class="nav-tab" @onclick='() => CopilotService.SaveUiState("/")'>💬 Chat</a>
<a href="/dashboard" class="nav-tab" @onclick='() => CopilotService.SaveUiState("/dashboard")'>🎛️ Dashboard</a>
<a href="/" class="nav-tab @(currentPage == "/" ? "active" : "")" @onclick='() => { CopilotService.SaveUiState("/"); currentPage = "/"; }'><svg style="vertical-align:middle" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> Chat</a>
<a href="/dashboard" class="nav-tab @(currentPage == "/dashboard" ? "active" : "")" @onclick='() => { CopilotService.SaveUiState("/dashboard"); currentPage = "/dashboard"; }'><svg style="vertical-align:middle" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg> Dashboard</a>
</div>
</div>

Expand All @@ -25,7 +26,7 @@
<option value="@model">@model</option>
}
</select>
<button class="dir-toggle-btn" @onclick="ToggleDirectoryInput" title="Set working directory">📂</button>
<button class="dir-toggle-btn" @onclick="ToggleDirectoryInput" title="Set working directory"><svg style="vertical-align:middle" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></button>
<button @onclick="CreateSession" disabled="@isCreating">
@(isCreating ? "..." : "+")
</button>
Expand All @@ -36,13 +37,15 @@
<input type="text" id="sessionDirInput"
placeholder="Working directory (default: AutoPilot.App)"
class="dir-input" />
<button class="dir-browse-btn" @onclick="BrowseDirectory" title="Browse folders">📁</button>
<button class="dir-browse-btn" @onclick="BrowseDirectory" title="Browse folders"><svg style="vertical-align:middle" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/><line x1="12" y1="11" x2="12" y2="17"/><line x1="9" y1="14" x2="15" y2="14"/></svg></button>
</div>
}
</div>

<div class="sidebar-divider"></div>

<div class="session-list">
<div class="section-header">Active Sessions</div>
<div class="section-header"><span class="section-header-label"><svg style="vertical-align:middle" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> Active Sessions</span></div>
@if (!sessions.Any())
{
<p class="no-sessions">No active sessions.<br/>Create one above or resume below.</p>
Expand All @@ -60,9 +63,20 @@
<span class="session-name">
@if (completedSessions.Contains(session.Name))
{
<span class="done-badge">✓</span>
<span class="done-badge"><svg style="vertical-align:middle" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>
}
@if (renamingSession == session.Name)
{
<input type="text" class="rename-input" id="renameInput"
value="@session.Name"
@onclick:stopPropagation="true"
@onblur="CommitRename"
@onkeydown="HandleRenameKeyDown" />
}
else
{
<span @ondblclick="() => StartRename(session.Name)" @ondblclick:stopPropagation="true">@session.Name</span>
}
@session.Name
@if (session.IsResumed)
{
<span class="resumed-badge">resumed</span>
Expand All @@ -81,6 +95,8 @@
{
<span class="processing">●</span>
}
<button class="rename-btn" @onclick:stopPropagation="true"
@onclick="() => StartRename(session.Name)" title="Rename session"><svg style="vertical-align:middle" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>
<button class="close-btn" @onclick:stopPropagation="true"
@onclick="() => CloseSession(session.Name)">×</button>
</div>
Expand All @@ -90,16 +106,17 @@

@if (persistedSessions.Any())
{
<div class="sidebar-divider"></div>
<div class="section-header" @onclick="TogglePersistedSessions" style="cursor: pointer;">
📁 Saved Sessions (@filteredPersistedSessions.Count())
<span class="section-header-label"><svg style="vertical-align:middle" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg> Saved Sessions (@filteredPersistedSessions.Count())</span>
<span class="toggle-icon">@(showPersistedSessions ? "▼" : "▶")</span>
</div>

@if (showPersistedSessions)
{
<div class="filter-box">
<input type="text" @bind="sessionFilter" @bind:event="oninput"
placeholder="🔍 Filter sessions..." class="filter-input" />
placeholder="Filter sessions..." class="filter-input" />
</div>

@foreach (var persisted in filteredPersistedSessions.Take(30))
Expand All @@ -117,7 +134,7 @@
}
</span>
</div>
<span class="resume-icon"> Resume</span>
<span class="resume-icon"><svg style="vertical-align:middle" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg> Resume</span>
</div>
}
}
Expand All @@ -132,6 +149,8 @@
private bool isCreating = false;
private bool showPersistedSessions = false;
private bool showDirectoryInput = false;
private string? renamingSession = null;
private string currentPage = "/";
private List<AgentSessionInfo> sessions = new();
private List<PersistedSessionInfo> persistedSessions = new();

Expand Down Expand Up @@ -159,6 +178,8 @@

protected override void OnInitialized()
{
currentPage = new Uri(Nav.Uri).AbsolutePath;
Nav.LocationChanged += (_, e) => { currentPage = new Uri(e.Location).AbsolutePath; InvokeAsync(StateHasChanged); };
CopilotService.OnStateChanged += RefreshSessions;
CopilotService.OnSessionComplete += HandleSessionComplete;
RefreshSessions();
Expand Down Expand Up @@ -296,6 +317,43 @@
}
}

private async Task StartRename(string sessionName)
{
renamingSession = sessionName;
StateHasChanged();
// Focus the input after render
await Task.Yield();
await JS.InvokeVoidAsync("eval", @"
var el = document.getElementById('renameInput');
if (el) { el.focus(); el.select(); }
");
}

private async Task CommitRename()
{
if (renamingSession == null) return;
var oldName = renamingSession;
renamingSession = null;

var newName = await JS.InvokeAsync<string>("eval", "document.getElementById('renameInput')?.value || ''");
if (!string.IsNullOrWhiteSpace(newName) && newName.Trim() != oldName)
{
CopilotService.RenameSession(oldName, newName.Trim());
}
}

private async Task HandleRenameKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
await CommitRename();
}
else if (e.Key == "Escape")
{
renamingSession = null;
}
}

private void SelectSession(string name)
{
completedSessions.Remove(name);
Expand Down
59 changes: 55 additions & 4 deletions Components/Layout/SessionSidebar.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@
border-bottom: 1px solid rgba(255,255,255,0.1);
}

.sidebar-divider {
min-height: 1px;
height: 1px;
background: rgba(255,255,255,0.15);
margin: 0;
flex-shrink: 0;
}

.session-list > .sidebar-divider {
margin: 0.5rem -0.5rem;
}

.sidebar-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.4rem;
Expand All @@ -33,7 +45,6 @@
display: flex;
gap: 0.5rem;
padding: 1rem;
border-bottom: 1px solid rgba(255,255,255,0.1);
flex-wrap: wrap;
}

Expand Down Expand Up @@ -147,15 +158,22 @@

.section-header {
padding: 0.5rem 0.75rem;
font-size: 1rem;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
color: rgba(255,255,255,0.5);
display: flex;
justify-content: space-between;
align-items: center;
}

.section-header-label {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}

.toggle-icon {
font-size: 0.6rem;
}
Expand Down Expand Up @@ -250,7 +268,7 @@
50% { opacity: 0.5; }
}

.close-btn, .resume-btn {
.close-btn, .resume-btn, .rename-btn {
width: 1.5rem;
height: 1.5rem;
border: none;
Expand All @@ -265,7 +283,8 @@
}

.session-item:hover .close-btn,
.session-item:hover .resume-btn {
.session-item:hover .resume-btn,
.session-item:hover .rename-btn {
opacity: 1;
}

Expand All @@ -274,6 +293,15 @@
color: #ef4444;
}

.rename-btn:hover {
background: rgba(59, 130, 246, 0.3);
color: #60a5fa;
}

.rename-btn {
font-size: 0.8rem;
}

.resume-btn:hover {
background: rgba(59, 130, 246, 0.3);
color: #60a5fa;
Expand Down Expand Up @@ -339,14 +367,18 @@
display: flex;
gap: 0.5rem;
margin-top: 0.75rem;
--bs-nav-tabs-border-color: transparent;
border-bottom: none;
}

.nav-tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
padding: 0.5rem;
border: 1px solid transparent;
border-radius: 6px;
background: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.7);
Expand All @@ -360,3 +392,22 @@
background: rgba(255,255,255,0.15);
color: white;
}

.nav-tab.active {
background: rgba(59, 130, 246, 0.25);
color: #60a5fa;
border: 1px solid rgba(59, 130, 246, 0.4);
}

.rename-input {
width: 100%;
padding: 0.2rem 0.4rem;
border: 1px solid #60a5fa;
border-radius: 4px;
background: rgba(255,255,255,0.15);
color: white;
font-size: 1rem;
font-weight: 500;
font-family: inherit;
outline: none;
}
45 changes: 37 additions & 8 deletions Components/Pages/Dashboard.razor
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<div class="dashboard">
<div class="dashboard-header">
<h2>🎛️ Session Orchestrator</h2>
<h2><svg style="vertical-align:middle" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg> Session Orchestrator</h2>
<span class="session-count">@sessions.Count active sessions</span>
</div>

Expand All @@ -30,8 +30,8 @@
<span class="card-status-dot @cardClass"></span>
<h3>@session.Name</h3>
</div>
<button class="card-close" @onclick="() => CloseSession(session.Name)" title="Close session">✕</button>
<span class="card-model">@session.Model</span>
<button class="card-close" @onclick="() => CloseSession(session.Name)" title="Close session">✕</button>
</div>

<div class="card-messages">
Expand All @@ -46,10 +46,39 @@
{
@foreach (var msg in lastMessages)
{
<div class="card-msg @(msg.IsUser ? "user" : "assistant")">
<span class="card-msg-role">@(msg.IsUser ? "You" : "AI")</span>
<span class="card-msg-text">@msg.Content</span>
</div>
@switch (msg.MessageType)
{
case ChatMessageType.User:
<div class="card-msg user">
<span class="card-msg-role">You</span>
<span class="card-msg-text">@Truncate(msg.Content, 120)</span>
</div>
break;
case ChatMessageType.Assistant:
<div class="card-msg assistant">
<span class="card-msg-role">AI</span>
<span class="card-msg-text">@Truncate(msg.Content, 120)</span>
</div>
break;
case ChatMessageType.ToolCall:
<div class="card-msg tool">
<span class="card-msg-role"><svg style="vertical-align:middle" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></span>
<span class="card-msg-text">@(msg.ToolName ?? "tool") @(msg.IsComplete ? (msg.IsSuccess ? "✓" : "✗") : "…")</span>
</div>
break;
case ChatMessageType.Reasoning:
<div class="card-msg reasoning">
<span class="card-msg-role"><svg style="vertical-align:middle" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a8 8 0 0 0-8 8c0 3.4 2.1 6.3 5 7.4V20a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-2.6c2.9-1.1 5-4 5-7.4a8 8 0 0 0-8-8z"/><line x1="10" y1="22" x2="14" y2="22"/></svg></span>
<span class="card-msg-text">@(msg.IsComplete ? "Thought" : "Thinking...")</span>
</div>
break;
case ChatMessageType.Error:
<div class="card-msg error">
<span class="card-msg-role"><svg style="vertical-align:middle" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#f87171" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>
<span class="card-msg-text">@Truncate(msg.Content, 80)</span>
</div>
break;
}
}
}
@if (session.IsProcessing)
Expand All @@ -74,15 +103,15 @@
disabled="@session.IsProcessing" />
<button @onclick="() => SendFromCard(session.Name)"
disabled="@session.IsProcessing">
@(session.IsProcessing ? "" : "➤")
@(session.IsProcessing ? "" : "➤")
</button>
</div>
</div>
}
</div>

<div class="broadcast-section">
<h3>📢 Broadcast to All</h3>
<h3><svg style="vertical-align:middle" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5.5 8.5L9 12l-3.5 3.5"/><path d="M2 12h7"/><path d="M14 4.5A7.5 7.5 0 0 1 14 19.5"/><path d="M17.5 2a11 11 0 0 1 0 20"/></svg> Broadcast to All</h3>
<div class="broadcast-input">
<input type="text" id="broadcastInput"
placeholder="Send same message to all sessions..." />
Expand Down
Loading