Skip to content

Commit 1d7995b

Browse files
mattleibowCopilot
andcommitted
Phase 1: AppChat as standalone popover (revert session integration)
- Revert SessionKind enum, nullable CopilotSession, and routing changes - Revert ExpandedSessionView/SessionSidebar AppChat integration - Create AppChatPopover.razor — floating overlay with ChatMessageList reuse - Add ✨ FAB toggle in MainLayout - Keep AI services: DirectLocalChatService, AppChatTools, NonFunctionInvokingChatClient - Remove Kind reference from AppChatTools (SessionKind deleted) - 610 tests passing, build clean Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 30a8f1f commit 1d7995b

11 files changed

Lines changed: 749 additions & 3 deletions

PolyPilot.Tests/PolyPilot.Tests.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
<PackageReference Include="GitHub.Copilot.SDK" Version="0.1.25" />
1313
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
1414
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
15+
<PackageReference Include="Microsoft.Extensions.AI" Version="10.3.0" />
16+
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-preview.260212.1" />
1517
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
1618
<PackageReference Include="xunit" Version="2.9.3" />
1719
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
@@ -66,6 +68,9 @@
6668
<Compile Include="../PolyPilot/Models/TutorialStep.cs" Link="Shared/TutorialStep.cs" />
6769
<Compile Include="../PolyPilot/Services/TutorialService.cs" Link="Shared/TutorialService.cs" />
6870
<Compile Include="../PolyPilot/BuildInfo.cs" Link="Shared/BuildInfo.cs" />
71+
<Compile Include="../PolyPilot/Services/AI/DirectLocalChatService.cs" Link="Shared/DirectLocalChatService.cs" />
72+
<Compile Include="../PolyPilot/Services/AI/AppChatTools.cs" Link="Shared/AppChatTools.cs" />
73+
<Compile Include="../PolyPilot/Services/AI/NonFunctionInvokingChatClient.cs" Link="Shared/NonFunctionInvokingChatClient.cs" />
6974
</ItemGroup>
7075

7176
</Project>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
@using PolyPilot.Models
2+
@using PolyPilot.Services
3+
@using PolyPilot.Services.AI
4+
@inject DirectLocalChatService LocalChat
5+
@inject IJSRuntime JS
6+
7+
@if (IsVisible)
8+
{
9+
<div class="appchat-popover @(isExpanded ? "expanded" : "collapsed")">
10+
<div class="appchat-header" @onclick="ToggleExpand">
11+
<span class="appchat-title">✨ App Chat</span>
12+
<div class="appchat-header-actions">
13+
@if (messages.Count > 0)
14+
{
15+
<button class="appchat-clear-btn" title="Clear chat" @onclick="ClearChat" @onclick:stopPropagation="true">🗑</button>
16+
}
17+
<button class="appchat-close-btn" title="Close" @onclick="Close" @onclick:stopPropagation="true">✕</button>
18+
</div>
19+
</div>
20+
21+
@if (isExpanded)
22+
{
23+
<div class="appchat-body">
24+
<ChatMessageList Messages="@messages"
25+
StreamingContent="@streamingContent"
26+
IsProcessing="@isProcessing"
27+
Compact="true"
28+
Layout="ChatLayout.BothLeft"
29+
Style="ChatStyle.Minimal" />
30+
</div>
31+
32+
<div class="appchat-input-area">
33+
<input id="appchat-input"
34+
type="text"
35+
placeholder="Ask about your sessions..."
36+
disabled="@isProcessing"
37+
@onkeydown="HandleKeyDown" />
38+
<button class="appchat-send-btn"
39+
disabled="@isProcessing"
40+
@onclick="SendMessage">
41+
@(isProcessing ? "" : "")
42+
</button>
43+
</div>
44+
}
45+
</div>
46+
}
47+
48+
@code {
49+
[Parameter] public bool IsVisible { get; set; }
50+
[Parameter] public EventCallback OnClose { get; set; }
51+
52+
private List<ChatMessage> messages = new();
53+
private string streamingContent = "";
54+
private bool isProcessing;
55+
private bool isExpanded = true;
56+
private string sessionName = "appchat-" + Guid.NewGuid().ToString("N")[..8];
57+
private CancellationTokenSource? _cts;
58+
59+
private void ToggleExpand() => isExpanded = !isExpanded;
60+
61+
private async Task Close()
62+
{
63+
_cts?.Cancel();
64+
await OnClose.InvokeAsync();
65+
}
66+
67+
private void ClearChat()
68+
{
69+
_cts?.Cancel();
70+
messages.Clear();
71+
streamingContent = "";
72+
isProcessing = false;
73+
LocalChat.RemoveSession(sessionName);
74+
sessionName = "appchat-" + Guid.NewGuid().ToString("N")[..8];
75+
}
76+
77+
private async Task HandleKeyDown(KeyboardEventArgs e)
78+
{
79+
if (e.Key == "Enter" && !isProcessing)
80+
await SendMessage();
81+
}
82+
83+
private async Task SendMessage()
84+
{
85+
var prompt = await JS.InvokeAsync<string>("eval", "document.getElementById('appchat-input')?.value || ''");
86+
if (string.IsNullOrWhiteSpace(prompt)) return;
87+
88+
await JS.InvokeVoidAsync("eval", "document.getElementById('appchat-input').value = ''");
89+
90+
messages.Add(ChatMessage.UserMessage(prompt));
91+
isProcessing = true;
92+
streamingContent = "";
93+
StateHasChanged();
94+
95+
_cts?.Cancel();
96+
_cts = new CancellationTokenSource();
97+
98+
await LocalChat.SendPromptStreamingAsync(
99+
sessionName,
100+
prompt,
101+
onDelta: delta =>
102+
{
103+
streamingContent += delta;
104+
InvokeAsync(StateHasChanged);
105+
},
106+
onComplete: full =>
107+
{
108+
messages.Add(ChatMessage.AssistantMessage(full));
109+
streamingContent = "";
110+
isProcessing = false;
111+
InvokeAsync(StateHasChanged);
112+
},
113+
onError: error =>
114+
{
115+
messages.Add(ChatMessage.ErrorMessage(error));
116+
streamingContent = "";
117+
isProcessing = false;
118+
InvokeAsync(StateHasChanged);
119+
},
120+
cancellationToken: _cts.Token);
121+
}
122+
123+
public void Dispose()
124+
{
125+
_cts?.Cancel();
126+
_cts?.Dispose();
127+
LocalChat.RemoveSession(sessionName);
128+
}
129+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
.appchat-popover {
2+
position: fixed;
3+
top: 50px;
4+
left: 16px;
5+
width: 380px;
6+
max-height: 560px;
7+
background: var(--bg-primary, #1e1e2e);
8+
border: 1px solid var(--border-color, #444);
9+
border-radius: 12px;
10+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
11+
z-index: 200;
12+
display: flex;
13+
flex-direction: column;
14+
overflow: hidden;
15+
transition: max-height 0.2s ease;
16+
}
17+
18+
.appchat-popover.collapsed {
19+
max-height: 44px;
20+
}
21+
22+
.appchat-header {
23+
display: flex;
24+
align-items: center;
25+
justify-content: space-between;
26+
padding: 10px 14px;
27+
background: var(--bg-secondary, #2a2a3e);
28+
cursor: pointer;
29+
user-select: none;
30+
border-bottom: 1px solid var(--border-color, #444);
31+
flex-shrink: 0;
32+
}
33+
34+
.appchat-title {
35+
font-size: 13px;
36+
font-weight: 600;
37+
color: var(--text-primary, #e0e0e0);
38+
}
39+
40+
.appchat-header-actions {
41+
display: flex;
42+
gap: 4px;
43+
}
44+
45+
.appchat-clear-btn,
46+
.appchat-close-btn {
47+
background: none;
48+
border: none;
49+
color: var(--text-secondary, #999);
50+
cursor: pointer;
51+
font-size: 13px;
52+
padding: 2px 6px;
53+
border-radius: 4px;
54+
line-height: 1;
55+
}
56+
57+
.appchat-clear-btn:hover,
58+
.appchat-close-btn:hover {
59+
background: var(--bg-hover, rgba(255, 255, 255, 0.1));
60+
color: var(--text-primary, #e0e0e0);
61+
}
62+
63+
.appchat-body {
64+
flex: 1;
65+
overflow-y: auto;
66+
min-height: 200px;
67+
max-height: 440px;
68+
padding: 8px;
69+
}
70+
71+
.appchat-input-area {
72+
display: flex;
73+
gap: 6px;
74+
padding: 8px 10px;
75+
border-top: 1px solid var(--border-color, #444);
76+
background: var(--bg-secondary, #2a2a3e);
77+
flex-shrink: 0;
78+
}
79+
80+
.appchat-input-area input {
81+
flex: 1;
82+
background: var(--bg-primary, #1e1e2e);
83+
border: 1px solid var(--border-color, #555);
84+
border-radius: 6px;
85+
padding: 6px 10px;
86+
color: var(--text-primary, #e0e0e0);
87+
font-size: 13px;
88+
outline: none;
89+
}
90+
91+
.appchat-input-area input:focus {
92+
border-color: var(--accent-color, #7c3aed);
93+
}
94+
95+
.appchat-input-area input:disabled {
96+
opacity: 0.5;
97+
}
98+
99+
.appchat-send-btn {
100+
background: var(--accent-color, #7c3aed);
101+
border: none;
102+
border-radius: 6px;
103+
color: white;
104+
padding: 6px 12px;
105+
cursor: pointer;
106+
font-size: 14px;
107+
line-height: 1;
108+
}
109+
110+
.appchat-send-btn:hover:not(:disabled) {
111+
opacity: 0.85;
112+
}
113+
114+
.appchat-send-btn:disabled {
115+
opacity: 0.4;
116+
cursor: not-allowed;
117+
}
118+
119+
/* Mobile adjustments */
120+
@media (max-width: 768px) {
121+
.appchat-popover {
122+
top: 50px;
123+
left: 8px;
124+
right: 8px;
125+
width: auto;
126+
max-height: 50vh;
127+
}
128+
129+
.appchat-body {
130+
max-height: 35vh;
131+
}
132+
}

PolyPilot/Components/Layout/MainLayout.razor

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
<div class="page @(flyoutOpen ? "flyout-open" : "")">
66
<div class="sidebar desktop-only">
7-
<SessionSidebar />
7+
<SessionSidebar OnToggleAppChat="ToggleAppChat" />
88
<div class="sidebar-resize-handle" @onmousedown="StartResize"></div>
99
</div>
1010

1111
<div class="mobile-top-bar mobile-only">
12-
<SessionSidebar IsMobileTopBar="true" OnToggleFlyout="ToggleFlyout" FontSize="fontSize" OnFontSizeChange="HandleFontSizeChange" />
12+
<SessionSidebar IsMobileTopBar="true" OnToggleFlyout="ToggleFlyout" OnToggleAppChat="ToggleAppChat" FontSize="fontSize" OnFontSizeChange="HandleFontSizeChange" />
1313
</div>
1414

1515
<main>
@@ -20,14 +20,17 @@
2020

2121
<div class="flyout-backdrop mobile-only @(flyoutOpen ? "open" : "")" @onclick="CloseFlyout"></div>
2222
<div class="flyout-panel mobile-only @(flyoutOpen ? "open" : "")">
23-
<SessionSidebar IsFlyoutPanel="true" OnToggleFlyout="CloseFlyout" OnSessionSelected="CloseFlyout" />
23+
<SessionSidebar IsFlyoutPanel="true" OnToggleFlyout="CloseFlyout" OnSessionSelected="CloseFlyout" OnToggleAppChat="ToggleAppChat" />
2424
</div>
2525

2626
<PolyPilot.Components.Tutorial.TutorialOverlay />
27+
28+
<AppChatPopover IsVisible="@appChatOpen" OnClose="() => appChatOpen = false" />
2729
</div>
2830

2931
@code {
3032
private bool flyoutOpen;
33+
private bool appChatOpen;
3134
private int fontSize = 20;
3235

3336
[Inject] private IJSRuntime JS { get; set; } = default!;
@@ -121,4 +124,9 @@
121124
{
122125
flyoutOpen = false;
123126
}
127+
128+
private void ToggleAppChat()
129+
{
130+
appChatOpen = !appChatOpen;
131+
}
124132
}

PolyPilot/Components/Layout/SessionSidebar.razor

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ else
5555
<a href="/" class="header-icon-btn @(currentPage == "/" || currentPage == "/dashboard" ? "active" : "")" title="Dashboard" @onclick="DashboardClicked" @onclick:preventDefault="true"><svg 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></a>
5656
<a href="/settings" class="header-icon-btn @(currentPage == "/settings" ? "active" : "")" title="Settings" @onclick='() => { CopilotService.SaveUiState("/settings"); currentPage = "/settings"; }'><svg 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="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></a>
5757
<a href="/tutorial" class="header-icon-btn @(currentPage == "/tutorial" ? "active" : "")" title="Tutorial" @onclick='() => { CopilotService.SaveUiState("/tutorial"); currentPage = "/tutorial"; }'><svg 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"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></a>
58+
<button class="header-icon-btn appchat-toggle" title="App Chat" @onclick="() => OnToggleAppChat.InvokeAsync()">✨</button>
5859
</div>
5960
</div>
6061
<p class="status @(CopilotService.IsInitialized ? "connected" : "disconnected")" style="font-size:0.8rem">
@@ -169,6 +170,7 @@ else
169170
[Parameter] public bool IsFlyoutPanel { get; set; }
170171
[Parameter] public EventCallback OnToggleFlyout { get; set; }
171172
[Parameter] public EventCallback OnSessionSelected { get; set; }
173+
[Parameter] public EventCallback OnToggleAppChat { get; set; }
172174
[Parameter] public int FontSize { get; set; } = 20;
173175
[Parameter] public EventCallback<int> OnFontSizeChange { get; set; }
174176

PolyPilot/Components/Layout/SessionSidebar.razor.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,13 @@
10821082
color: var(--accent-primary);
10831083
}
10841084

1085+
.appchat-toggle {
1086+
font-size: 12px;
1087+
border: none;
1088+
background: none;
1089+
cursor: pointer;
1090+
}
1091+
10851092
.rename-input {
10861093
width: 100%;
10871094
padding: 0.2rem 0.4rem;

0 commit comments

Comments
 (0)