Skip to content

Commit 2c9e43f

Browse files
authored
feat(desktop): embed chat in Dashboard, move conversations to its own page (#6496)
## Summary - Dashboard now hosts the chat (messages + sticky input) where the conversations list used to live, with a top/bottom gradient mask so messages fade into the background instead of clipping under the input. - Conversations list moved to its own sidebar page (replaces the standalone Chat sidebar item). Cmd+2 navigates to Conversations; \`navigateToChat\` notifications route to Dashboard. - Goals widget empty state drops the duplicate "Tap to add goal" tile and centers the Generate AI Goal action — the header + button is the way to add manually. - Trimmed the suggestion banner: removed the small "Suggested first ask" label to give the chat a few more vertical pixels. - Dashboard chat input placeholder reads "Ask omi anything". ## Test plan - [ ] Dashboard renders Tasks + Goals row at top, chat in the middle, sticky input at the bottom - [ ] Sending a message from the dashboard input works and replies stream in - [ ] Top + bottom of the chat fade into the dashboard background - [ ] Sidebar item 2 is "Conversations"; clicking it opens the standalone conversations list with detail navigation - [ ] Cmd+2 navigates to Conversations - [ ] Goals card with no goals shows only the centered Generate AI Goal button (plus the header + button) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 91dc34e + c6486d6 commit 2c9e43f

8 files changed

Lines changed: 208 additions & 67 deletions

File tree

desktop/CHANGELOG.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
2-
"unreleased": [],
2+
"unreleased": [
3+
"Replaced Conversations on Dashboard with embedded Chat (Ask omi anything) and moved the Conversations list to its own sidebar page"
4+
],
35
"releases": [
46
{
57
"version": "0.11.275",

desktop/Desktop/Sources/MainWindow/Components/GoalsWidget.swift

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,11 @@ struct GoalsWidget: View {
3737

3838

3939
if goals.isEmpty {
40-
// Empty state with AI suggestion
41-
VStack(spacing: 16) {
42-
Button(action: { showingCreateSheet = true }) {
43-
HStack {
44-
Spacer()
45-
Image(systemName: "plus")
46-
.scaledFont(size: 14)
47-
.foregroundColor(OmiColors.textTertiary)
48-
Text("Tap to add goal")
49-
.scaledFont(size: 13)
50-
.foregroundColor(OmiColors.textTertiary)
51-
Spacer()
52-
}
53-
.frame(maxWidth: .infinity)
54-
.padding(.vertical, 20)
55-
}
56-
.buttonStyle(.plain)
40+
// Empty state — header already has a + button, so just offer
41+
// the AI generation action centered in the empty area.
42+
VStack {
43+
Spacer(minLength: 0)
5744

58-
// AI goal generation button
5945
Button(action: { triggerGoalGeneration() }) {
6046
HStack(spacing: 6) {
6147
if isGeneratingGoal {
@@ -67,7 +53,7 @@ struct GoalsWidget: View {
6753
.scaledFont(size: 12)
6854
}
6955
Text(isGeneratingGoal ? "Generating..." : "Generate AI Goal")
70-
.scaledFont(size: 12, weight: .medium)
56+
.scaledFont(size: 13, weight: .medium)
7157
}
7258
.foregroundColor(OmiColors.purplePrimary)
7359
.padding(.horizontal, 14)
@@ -76,8 +62,10 @@ struct GoalsWidget: View {
7662
}
7763
.buttonStyle(.plain)
7864
.disabled(isGeneratingGoal)
65+
66+
Spacer(minLength: 0)
7967
}
80-
.padding(.vertical, 12)
68+
.frame(maxWidth: .infinity, maxHeight: .infinity)
8169
} else {
8270
// Goals list
8371
VStack(spacing: 14) {

desktop/Desktop/Sources/MainWindow/DesktopHomeView.swift

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,8 @@ struct DesktopHomeView: View {
423423
]
424424
if currentTierLevel >= 2 { visibleRawValues.insert(SidebarNavItem.memories.rawValue) }
425425
if currentTierLevel >= 3 { visibleRawValues.insert(SidebarNavItem.tasks.rawValue) }
426-
if currentTierLevel >= 4 { visibleRawValues.insert(SidebarNavItem.chat.rawValue) }
426+
// Conversations replaced Chat in the sidebar; tier 1 unlocks it.
427+
if currentTierLevel >= 1 { visibleRawValues.insert(SidebarNavItem.conversations.rawValue) }
427428

428429
if !visibleRawValues.contains(selectedIndex) {
429430
selectedIndex = SidebarNavItem.dashboard.rawValue
@@ -709,8 +710,9 @@ struct DesktopHomeView: View {
709710
}
710711
}
711712
.onReceive(NotificationCenter.default.publisher(for: .navigateToChat)) { _ in
713+
// Chat now lives on the Dashboard page.
712714
withAnimation(.easeInOut(duration: 0.2)) {
713-
selectedIndex = SidebarNavItem.chat.rawValue
715+
selectedIndex = SidebarNavItem.dashboard.rawValue
714716
}
715717
}
716718
.onReceive(NotificationCenter.default.publisher(for: .navigateToTasks)) { _ in
@@ -764,12 +766,13 @@ private struct PageContentView: View {
764766
switch selectedIndex {
765767
case 0:
766768
DashboardPage(
767-
viewModel: viewModelContainer.dashboardViewModel, appState: appState,
769+
viewModel: viewModelContainer.dashboardViewModel,
770+
appState: appState,
771+
appProvider: viewModelContainer.appProvider,
772+
chatProvider: viewModelContainer.chatProvider,
768773
selectedIndex: $selectedTabIndex)
769774
case 1:
770-
DashboardPage(
771-
viewModel: viewModelContainer.dashboardViewModel, appState: appState,
772-
selectedIndex: $selectedTabIndex)
775+
ConversationsPageHost(appState: appState)
773776
case 2:
774777
ChatPage(
775778
appProvider: viewModelContainer.appProvider, chatProvider: viewModelContainer.chatProvider
@@ -804,13 +807,27 @@ private struct PageContentView: View {
804807
HelpPage()
805808
default:
806809
DashboardPage(
807-
viewModel: viewModelContainer.dashboardViewModel, appState: appState,
810+
viewModel: viewModelContainer.dashboardViewModel,
811+
appState: appState,
812+
appProvider: viewModelContainer.appProvider,
813+
chatProvider: viewModelContainer.chatProvider,
808814
selectedIndex: $selectedTabIndex)
809815
}
810816
}
811817
}
812818
}
813819

820+
/// Hosts the standalone Conversations page with its own selection state
821+
/// so tapping a row navigates to the detail view.
822+
private struct ConversationsPageHost: View {
823+
let appState: AppState
824+
@State private var selectedConversation: ServerConversation? = nil
825+
826+
var body: some View {
827+
ConversationsPage(appState: appState, selectedConversation: $selectedConversation)
828+
}
829+
}
830+
814831
#Preview {
815832
DesktopHomeView()
816833
}

desktop/Desktop/Sources/MainWindow/Pages/DashboardPage.swift

Lines changed: 157 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -196,26 +196,107 @@ class DashboardViewModel: ObservableObject {
196196
struct DashboardPage: View {
197197
@ObservedObject var viewModel: DashboardViewModel
198198
@ObservedObject var appState: AppState
199+
@ObservedObject var appProvider: AppProvider
200+
@ObservedObject var chatProvider: ChatProvider
199201
@Binding var selectedIndex: Int
200-
@State private var selectedConversation: ServerConversation? = nil
202+
@State private var citedConversation: ServerConversation? = nil
203+
@State private var isLoadingCitation = false
204+
205+
private var selectedApp: OmiApp? {
206+
guard let appId = chatProvider.selectedAppId else { return nil }
207+
return appProvider.chatApps.first { $0.id == appId }
208+
}
201209

202210
var body: some View {
203-
Group {
204-
if selectedConversation != nil {
205-
// Full-page conversation detail (hides dashboard)
206-
ConversationsPage(appState: appState, selectedConversation: $selectedConversation)
207-
} else {
208-
// Dashboard + conversations in one scrollable page
209-
ScrollView {
210-
VStack(spacing: 0) {
211-
dashboardWidgets
212-
ConversationsPage(appState: appState, selectedConversation: $selectedConversation, embedded: true)
211+
VStack(spacing: 0) {
212+
dashboardWidgets
213+
214+
// Chat messages — fills remaining vertical space, scrolls internally,
215+
// morphs into the dashboard background (no card chrome).
216+
// A bottom gradient mask softens messages into the background near
217+
// the input field so they fade out instead of clipping abruptly.
218+
ChatMessagesView(
219+
messages: chatProvider.messages,
220+
isSending: chatProvider.isSending,
221+
hasMoreMessages: chatProvider.hasMoreMessages,
222+
isLoadingMoreMessages: chatProvider.isLoadingMoreMessages,
223+
isLoadingInitial: (chatProvider.isLoading || chatProvider.isLoadingSessions)
224+
&& !chatProvider.isClearing,
225+
app: selectedApp,
226+
onLoadMore: { await chatProvider.loadMoreMessages() },
227+
onRate: { messageId, rating in
228+
Task { await chatProvider.rateMessage(messageId, rating: rating) }
229+
},
230+
onCitationTap: { citation in
231+
handleCitationTap(citation)
232+
},
233+
sessionsLoadError: chatProvider.sessionsLoadError,
234+
onRetry: { Task { await chatProvider.retryLoad() } },
235+
welcomeContent: { dashboardChatWelcome }
236+
)
237+
.frame(maxWidth: .infinity, maxHeight: .infinity)
238+
.mask(
239+
LinearGradient(
240+
stops: [
241+
.init(color: .clear, location: 0.0),
242+
.init(color: .black, location: 0.08),
243+
.init(color: .black, location: 0.92),
244+
.init(color: .clear, location: 1.0),
245+
],
246+
startPoint: .top,
247+
endPoint: .bottom
248+
)
249+
)
250+
251+
ChatInputView(
252+
onSend: { text in
253+
AnalyticsManager.shared.chatMessageSent(
254+
messageLength: text.count, hasContext: selectedApp != nil, source: "dashboard_chat")
255+
Task { await chatProvider.sendMessage(text) }
256+
},
257+
onFollowUp: { text in
258+
Task { await chatProvider.sendFollowUp(text) }
259+
},
260+
onStop: {
261+
chatProvider.stopAgent()
262+
},
263+
isSending: chatProvider.isSending,
264+
isStopping: chatProvider.isStopping,
265+
placeholder: "Ask omi anything",
266+
mode: $chatProvider.chatMode,
267+
inputText: $chatProvider.draftText
268+
)
269+
.padding(.horizontal, 30)
270+
.padding(.top, 12)
271+
.padding(.bottom, 20)
272+
}
273+
.frame(maxWidth: .infinity, maxHeight: .infinity)
274+
.background(Color.clear)
275+
.sheet(item: $citedConversation) { conversation in
276+
ConversationDetailView(
277+
conversation: conversation,
278+
onBack: {
279+
citedConversation = nil
280+
}
281+
)
282+
.frame(minWidth: 500, minHeight: 500)
283+
}
284+
.overlay {
285+
if isLoadingCitation {
286+
ZStack {
287+
Color.black.opacity(0.3)
288+
VStack(spacing: 12) {
289+
ProgressView()
290+
Text("Loading source...")
291+
.scaledFont(size: 13)
292+
.foregroundColor(.white)
213293
}
294+
.padding(20)
295+
.background(OmiColors.backgroundSecondary)
296+
.cornerRadius(12)
214297
}
215298
}
216299
}
217-
.frame(maxWidth: .infinity, maxHeight: .infinity)
218-
.background(Color.clear)
219300
.onAppear {
220301
if PostOnboardingPromptSuggestions.shouldShowPopup && !postOnboardingSuggestions.isEmpty {
221302
NotificationCenter.default.post(name: .showTryAskingPopup, object: nil)
@@ -226,6 +307,58 @@ struct DashboardPage: View {
226307
}
227308
}
228309

310+
/// Welcome message shown when there are no chat messages yet.
311+
/// Transparent — no card chrome — so it morphs into the dashboard background.
312+
private var dashboardChatWelcome: some View {
313+
VStack(spacing: 12) {
314+
if let logoURL = Bundle.resourceBundle.url(forResource: "herologo", withExtension: "png"),
315+
let logoImage = NSImage(contentsOf: logoURL)
316+
{
317+
Image(nsImage: logoImage)
318+
.resizable()
319+
.scaledToFit()
320+
.frame(width: 40, height: 40)
321+
}
322+
323+
Text("Ask omi anything")
324+
.scaledFont(size: 16, weight: .semibold)
325+
.foregroundColor(OmiColors.textPrimary)
326+
327+
Text("Your personal AI assistant — knows you through your memories and conversations")
328+
.scaledFont(size: 13)
329+
.foregroundColor(OmiColors.textSecondary)
330+
.multilineTextAlignment(.center)
331+
.padding(.horizontal, 40)
332+
}
333+
.frame(maxWidth: .infinity)
334+
.padding(.vertical, 32)
335+
}
336+
337+
/// Handle tapping on a citation card — opens the cited conversation in a sheet.
338+
private func handleCitationTap(_ citation: Citation) {
339+
guard citation.sourceType == .conversation else {
340+
log("Citation tapped: \(citation.title) (memory - no detail view)")
341+
return
342+
}
343+
344+
isLoadingCitation = true
345+
346+
Task {
347+
do {
348+
let conversation = try await APIClient.shared.getConversation(id: citation.id)
349+
await MainActor.run {
350+
citedConversation = conversation
351+
isLoadingCitation = false
352+
}
353+
} catch {
354+
logError("Failed to fetch cited conversation", error: error)
355+
await MainActor.run {
356+
isLoadingCitation = false
357+
}
358+
}
359+
}
360+
}
361+
229362
private var dashboardWidgets: some View {
230363
VStack(alignment: .leading, spacing: 28) {
231364
if shouldShowSuggestionBanner {
@@ -290,8 +423,8 @@ struct DashboardPage: View {
290423
}
291424
}
292425
.padding(.horizontal, 30)
293-
.padding(.top, 40)
294-
.padding(.bottom, 16)
426+
.padding(.top, 32)
427+
.padding(.bottom, 12)
295428
}
296429

297430
private var postOnboardingSuggestions: [String] {
@@ -315,7 +448,13 @@ struct DashboardPage: View {
315448
}
316449

317450
#Preview {
318-
DashboardPage(viewModel: DashboardViewModel(), appState: AppState(), selectedIndex: .constant(0))
319-
.frame(width: 800, height: 600)
320-
.background(OmiColors.backgroundPrimary)
451+
DashboardPage(
452+
viewModel: DashboardViewModel(),
453+
appState: AppState(),
454+
appProvider: AppProvider(),
455+
chatProvider: ChatProvider(),
456+
selectedIndex: .constant(0)
457+
)
458+
.frame(width: 800, height: 600)
459+
.background(OmiColors.backgroundPrimary)
321460
}

desktop/Desktop/Sources/MainWindow/SidebarView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ enum SidebarNavItem: Int, CaseIterable {
6767

6868
/// Items shown in the main navigation (top section)
6969
static var mainItems: [SidebarNavItem] {
70-
[.dashboard, .chat, .memories, .tasks, .rewind, .apps]
70+
[.dashboard, .conversations, .memories, .tasks, .rewind, .apps]
7171
}
7272
}
7373

desktop/Desktop/Sources/OmiApp.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,10 @@ struct OMIApp: App {
147147
}
148148
.keyboardShortcut("1", modifiers: .command)
149149

150-
Button("Chat") {
150+
Button("Conversations") {
151151
NotificationCenter.default.post(
152152
name: .navigateToSidebarItem, object: nil,
153-
userInfo: ["rawValue": SidebarNavItem.chat.rawValue])
153+
userInfo: ["rawValue": SidebarNavItem.conversations.rawValue])
154154
}
155155
.keyboardShortcut("2", modifiers: .command)
156156

desktop/Desktop/Sources/PostOnboardingPromptViews.swift

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -167,21 +167,7 @@ struct PromptSuggestionBanner: View {
167167
var body: some View {
168168
VStack(alignment: .leading, spacing: 14) {
169169
Button(action: onOpen) {
170-
VStack(alignment: .leading, spacing: 14) {
171-
HStack(spacing: 8) {
172-
Image(systemName: "sparkles")
173-
.font(.system(size: 11, weight: .semibold))
174-
Text("Suggested first ask")
175-
.font(.system(size: 12, weight: .semibold))
176-
}
177-
.foregroundColor(calloutAmber)
178-
.padding(.horizontal, 10)
179-
.padding(.vertical, 6)
180-
.background(
181-
Capsule()
182-
.fill(calloutAmber.opacity(0.14))
183-
)
184-
170+
VStack(alignment: .leading, spacing: 12) {
185171
Text("Next step -> Ask omi")
186172
.font(.system(size: 20, weight: .semibold, design: .serif))
187173
.foregroundColor(bannerPrimaryText)
@@ -219,7 +205,8 @@ struct PromptSuggestionBanner: View {
219205
}
220206
}
221207
.frame(maxWidth: .infinity, alignment: .leading)
222-
.padding(22)
208+
.padding(.horizontal, 22)
209+
.padding(.vertical, 18)
223210
.background(
224211
RoundedRectangle(cornerRadius: 20, style: .continuous)
225212
.fill(

0 commit comments

Comments
 (0)