Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
775c7dd
Stop Home refetching status data on every visit
h3nock Jul 7, 2026
dc93f84
Persist conversations search across page navigation
h3nock Jul 7, 2026
31bd037
Cache brain map layouts and skip rebuilds on unchanged graphs
h3nock Jul 7, 2026
143f5b2
Extract Home header controls into shared page-header components
h3nock Jul 7, 2026
41ea8f1
Adopt the shared page header on Conversations, Tasks and Memories
h3nock Jul 7, 2026
1fc73dd
Open chat as a page instead of an inline panel on Home
h3nock Jul 7, 2026
d51c7a0
Match Tasks search field and row hover to the other pages
h3nock Jul 7, 2026
0ca929f
Add chrome capture and titlebar dump actions to desktop automation br…
h3nock Jul 7, 2026
ae8dd6e
Replace in-content page headers with native unified window toolbar
h3nock Jul 7, 2026
0f4b4d5
Unify header controls across Conversations, Tasks and Memories pages
h3nock Jul 7, 2026
cc714c4
Make toolbar Capture and Listening states readable at a glance
h3nock Jul 7, 2026
0d056fa
Show the Today section task count next to its clean action
h3nock Jul 8, 2026
14de2ff
Remove the prominent fill from header add buttons
h3nock Jul 8, 2026
d80a84b
Clean up task rows: hover-cluster detail button, first-line checkbox,…
h3nock Jul 8, 2026
d2cb669
Match Rewind header controls to the shared page header style
h3nock Jul 8, 2026
e559956
Collapse chat input to a single surface with inline attach and send
h3nock Jul 8, 2026
659c250
Align task drag handle with the checkbox on hover
h3nock Jul 8, 2026
8ebe2a6
Remove the standalone hotkey chip from the Rewind header
h3nock Jul 8, 2026
57cf0e1
Revert "Open chat as a page instead of an inline panel on Home"
h3nock Jul 8, 2026
427d842
Drop the superseded page-header changelog fragment
h3nock Jul 8, 2026
fed99ca
Guard home connector status across session changes
h3nock Jul 8, 2026
1e6bdfa
Preserve graph bootstrap retry on rebuild failure
h3nock Jul 8, 2026
cb88cab
Keep toolbar controls available in legacy home
h3nock Jul 8, 2026
ff6dd78
Cover desktop UI refactor files in e2e flows
h3nock Jul 8, 2026
2aba8cc
Migrate legacy Omi device home status
h3nock Jul 8, 2026
8a73087
Keep onboarding import scoped to active user
h3nock Jul 8, 2026
fc8c011
Remove legacy Omi device flag after migration
h3nock Jul 8, 2026
e73e1ae
Tighten onboarding import scope regression
h3nock Jul 8, 2026
61f7e14
Resolve screen recording helper after toolbar extraction
h3nock Jul 8, 2026
0bdbbef
Filter redesigned home chat transcript
h3nock Jul 8, 2026
eb2d7b3
desktop: keep navigation chrome in app
Git-on-my-level Jul 9, 2026
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
46 changes: 41 additions & 5 deletions desktop/macos/Desktop/Sources/DesktopAutomationBridge.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import AppKit

Check warning on line 1 in desktop/macos/Desktop/Sources/DesktopAutomationBridge.swift

View workflow job for this annotation

GitHub Actions / Hygiene

Large changed file

desktop/macos/Desktop/Sources/DesktopAutomationBridge.swift is 2236 lines; consider splitting files over 800 lines.
import CryptoKit
import Foundation
import Network
Expand Down Expand Up @@ -534,7 +534,7 @@
/// Register the always-available actions that don't need any view's `@State` —
/// they post the same notifications / hit the same services as the real controls,
/// so they exercise the genuine code paths. Idempotent.
func registerBuiltins() {

Check warning on line 537 in desktop/macos/Desktop/Sources/DesktopAutomationBridge.swift

View workflow job for this annotation

GitHub Actions / Hygiene

Long function

func registerBuiltins() is 1055 lines; consider extracting focused helpers over 150 lines.
guard !didRegisterBuiltins else { return }
didRegisterBuiltins = true

Expand Down Expand Up @@ -1198,12 +1198,13 @@

register(
name: "capture_main_window_png",
summary: "Write PNG of the frontmost Omi window (in-process capture)",
params: ["path"]
summary: "Write PNG of the frontmost Omi window (in-process capture); chrome=1 includes titlebar/toolbar",
params: ["path", "chrome"]
) { params in
guard let path = params["path"], !path.isEmpty else {
return ["error": "missing 'path'"]
}
let includeChrome = params["chrome"] == "1"
return await MainActor.run { () -> [String: String] in
guard
let window = NSApp.windows.first(where: {
Expand All @@ -1213,11 +1214,12 @@
else {
return ["error": "no_visible_window"]
}
let bounds = contentView.bounds
guard let rep = contentView.bitmapImageRepForCachingDisplay(in: bounds) else {
let captureView = includeChrome ? (contentView.superview ?? contentView) : contentView
let bounds = captureView.bounds
guard let rep = captureView.bitmapImageRepForCachingDisplay(in: bounds) else {
return ["error": "bitmap_rep_failed"]
}
contentView.cacheDisplay(in: bounds, to: rep)
captureView.cacheDisplay(in: bounds, to: rep)
guard let data = rep.representation(using: .png, properties: [:]) else {
return ["error": "png_encode_failed"]
}
Expand All @@ -1230,6 +1232,40 @@
}
}

register(
name: "dump_window_chrome",
summary: "Dump the main window's titlebar/toolbar view hierarchy (class, frame, text)",
params: []
) { _ in
await MainActor.run { () -> [String: String] in
guard
let window = NSApp.windows.first(where: {
$0.isVisible && $0.title.range(of: "omi", options: .caseInsensitive) != nil
}),
let frameView = window.contentView?.superview
else {
return ["error": "no_visible_window"]
}
var lines: [String] = []
func walk(_ view: NSView, depth: Int) {
// Skip the content subtree — only chrome is interesting here.
if view === window.contentView { return }
let indent = String(repeating: " ", count: depth)
var line = "\(indent)\(type(of: view)) frame=\(NSStringFromRect(view.frame))"
if let text = view as? NSTextField {
line += " text=\"\(text.stringValue)\""
}
if view.isHidden { line += " hidden" }
lines.append(line)
for sub in view.subviews {
walk(sub, depth: depth + 1)
}
}
walk(frameView, depth: 0)
return ["chrome": lines.joined(separator: "\n")]
}
}

register(
name: "capture_floating_bar_png",
summary: "Write PNG of the floating control bar window (in-process capture)",
Expand Down Expand Up @@ -1735,7 +1771,7 @@
statusCode: 401)
}

switch (request.method, request.path) {

Check warning on line 1774 in desktop/macos/Desktop/Sources/DesktopAutomationBridge.swift

View workflow job for this annotation

GitHub Actions / Hygiene

Long function

private func routeUntimed(request: HTTPRequest) async -> HTTPResponse is 214 lines; consider extracting focused helpers over 150 lines.
case ("GET", "/health"):
let snapshot = await cachedAutomationSnapshot()
return jsonResponse(DesktopAutomationResponse(ok: true, result: snapshot, error: nil))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import AppKit
import SwiftUI

/// Status of a capture-style feature as shown in the toolbar controls.
enum HomeStatusState {
case active
case inactive
case blocked

var indicator: Color {
switch self {
case .active:
return HomePalette.green
case .inactive:
return HomePalette.faint
case .blocked:
return Color(red: 1.0, green: 0.24, blue: 0.30)
}
}

var text: String {
switch self {
case .active:
return "On"
case .inactive:
return "Off"
case .blocked:
return "Blocked"
}
}

var isActive: Bool {
if case .active = self { return true }
return false
}

var isBlocked: Bool {
if case .blocked = self { return true }
return false
}
}

/// Shared state + actions behind the Capture and Listening Home controls.
/// One instance per `InAppStatusControls`; the underlying truth lives in
/// `ProactiveAssistantsPlugin` / `AssistantSettings` / `AppState`, so
/// instances stay in sync via the monitoring notifications.
///
/// Extracted from `DashboardPage` so control surfaces can share the toggle
/// logic without duplicating permission and monitoring state handling.
@MainActor
final class CaptureListeningController: ObservableObject {
@Published private(set) var isCaptureMonitoring = false
@Published private(set) var isTogglingCapture = false
@Published private(set) var isTogglingListening = false

private let appState: AppState
private var observers: [NSObjectProtocol] = []

init(appState: AppState) {
self.appState = appState
syncCaptureState()

let center = NotificationCenter.default
let syncNames: [Notification.Name] = [
.assistantMonitoringStateDidChange,
.screenCapturePermissionLost,
.screenCaptureKitBroken,
]
for name in syncNames {
observers.append(
center.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
Task { @MainActor in self?.syncCaptureState() }
})
}
}

deinit {
for observer in observers {
NotificationCenter.default.removeObserver(observer)
}
}

// MARK: - State

var isCaptureLive: Bool {
isCaptureMonitoring || ProactiveAssistantsPlugin.shared.isMonitoring
}

var captureStatus: HomeStatusState {
if appState.isScreenCaptureKitBroken || appState.isScreenRecordingStale
|| !appState.hasScreenRecordingPermission
{
return .blocked
}
return isCaptureLive ? .active : .inactive
}

var listeningCaptureMode: AssistantSettings.SystemAudioCaptureMode {
AssistantSettings.shared.systemAudioCaptureMode
}

var listeningModeTitle: String {
switch listeningCaptureMode {
case .always:
return "Always"
case .onlyDuringMeetings:
return appState.isAwaitingMeeting ? "Meetings only" : "In meeting"
case .never:
return "Mic only"
}
}

// MARK: - Actions

func toggleListening() {
let enabled = !appState.isTranscribing
if enabled && !appState.hasMicrophonePermission {
appState.requestMicrophonePermission()
return
}

isTogglingListening = true
AssistantSettings.shared.transcriptionEnabled = enabled
AnalyticsManager.shared.settingToggled(setting: "transcription", enabled: enabled)
NotificationCenter.default.post(
name: .toggleTranscriptionRequested,
object: nil,
userInfo: ["enabled": enabled]
)

DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
self?.isTogglingListening = false
}
}

func toggleListeningMode() {
let nextMode: AssistantSettings.SystemAudioCaptureMode =
listeningCaptureMode == .onlyDuringMeetings ? .always : .onlyDuringMeetings
AssistantSettings.shared.systemAudioCaptureMode = nextMode
AnalyticsManager.shared.settingToggled(
setting: "meetings_only_listening",
enabled: nextMode == .onlyDuringMeetings
)
objectWillChange.send()
}

func toggleCapture() {
syncCaptureState()
let enabled = !isCaptureLive
isTogglingCapture = true

if enabled {
ProactiveAssistantsPlugin.shared.refreshScreenRecordingPermission()
guard ProactiveAssistantsPlugin.shared.hasScreenRecordingPermission else {
setScreenAnalysisEnabled(false)
isCaptureMonitoring = false
isTogglingCapture = false
ScreenCaptureService.requestScreenRecordingAccessAndOpenSettings()
return
}
}

setScreenAnalysisEnabled(enabled)
AnalyticsManager.shared.settingToggled(setting: "monitoring", enabled: enabled)

if enabled {
ProactiveAssistantsPlugin.shared.startMonitoring { [weak self] success, _ in
DispatchQueue.main.async {
guard let self else { return }
self.isTogglingCapture = false
self.isCaptureMonitoring = ProactiveAssistantsPlugin.shared.isMonitoring
if !success {
self.setScreenAnalysisEnabled(false)
self.isCaptureMonitoring = false
}
}
}
} else {
ProactiveAssistantsPlugin.shared.stopMonitoring()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
self?.isTogglingCapture = false
self?.isCaptureMonitoring = false
}
}
}

func syncCaptureState() {
ProactiveAssistantsPlugin.shared.refreshScreenRecordingPermission()
isCaptureMonitoring = ProactiveAssistantsPlugin.shared.isMonitoring
objectWillChange.send()
}

private func setScreenAnalysisEnabled(_ enabled: Bool) {
AssistantSettings.shared.screenAnalysisEnabled = enabled
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,19 @@ struct ChatInputView: View {
attachments: currentAttachments,
onRemove: { id in onAttachmentRemoved?(id) }
)
.padding(.top, 8)
.padding(.horizontal, 4)
}

HStack(alignment: .center, spacing: 8) {
// One surface: paperclip, editor, and send all live inside the
// container — no field-inside-a-panel double chrome.
HStack(alignment: .bottom, spacing: 4) {
if attachmentsEnabled {
Button(action: pickFiles) {
Image(systemName: "paperclip")
.scaledFont(size: 18, weight: .medium)
.scaledFont(size: 16, weight: .medium)
.foregroundColor(OmiColors.textTertiary)
.frame(width: 32, height: 32)
.frame(width: 32, height: 40)
}
.buttonStyle(.plain)
.help("Attach files")
Expand Down Expand Up @@ -110,8 +114,6 @@ struct ChatInputView: View {
}
.frame(maxHeight: 200)
.clipped()
.background(OmiColors.backgroundTertiary)
.clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))

// Floating Ask/Act toggle (top-right, inside the input area)
if askModeEnabled {
Expand All @@ -121,33 +123,45 @@ struct ChatInputView: View {
}
}

// Send/Stop button — inline to the right of the input
// Send/Stop button — inside the container, trailing edge.
// Upstream dropped follow-up-while-streaming, so Stop shows
// for the whole sending state.
if isSending {
if isStopping {
ProgressView()
.controlSize(.small)
.frame(width: 24, height: 24)
.frame(width: 28, height: 40)
} else {
Button(action: { onStop?() }) {
Image(systemName: "stop.circle.fill")
.scaledFont(size: 24)
.foregroundColor(.red.opacity(0.8))
.frame(width: 28, height: 40)
}
.buttonStyle(.plain)
}
} else {
Button(action: handleSubmit) {
Image(systemName: "arrow.up.circle.fill")
.scaledFont(size: 24)
.foregroundColor(canSend ? OmiColors.purplePrimary : OmiColors.textQuaternary)
.foregroundColor(canSend ? OmiColors.textPrimary : OmiColors.textQuaternary)
.frame(width: 28, height: 40)
}
.buttonStyle(.plain)
.disabled(!canSend)
}
}
}
.padding(12)
.omiPanel(fill: OmiColors.backgroundSecondary, radius: 22, stroke: dropStrokeColor, shadowOpacity: 0.1, shadowRadius: 12, shadowY: 6)
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 24, style: .continuous)
.fill(OmiColors.backgroundSecondary)
)
.overlay(
RoundedRectangle(cornerRadius: 24, style: .continuous)
.stroke(dropStrokeColor, lineWidth: 1)
)
.fixedSize(horizontal: false, vertical: true)
.if(attachmentsEnabled) { view in
view.onDrop(of: [UTType.fileURL], isTargeted: $isDropTargeted, perform: handleDrop)
Expand Down Expand Up @@ -185,7 +199,7 @@ struct ChatInputView: View {
}

private var dropStrokeColor: Color {
isDropTargeted ? OmiColors.purplePrimary.opacity(0.6) : OmiColors.border.opacity(0.2)
isDropTargeted ? OmiColors.border : OmiColors.border.opacity(0.25)
}

private func handleSubmit() {
Expand Down Expand Up @@ -421,10 +435,10 @@ struct ChatModeToggle: View {
Button(action: { mode = targetMode }) {
Text(label)
.scaledFont(size: 12, weight: .medium)
.foregroundColor(mode == targetMode ? .white : OmiColors.textTertiary)
.foregroundColor(mode == targetMode ? .black : OmiColors.textTertiary)
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(mode == targetMode ? OmiColors.userBubble : Color.clear)
.background(mode == targetMode ? OmiColors.textPrimary : Color.clear)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
}
.buttonStyle(.plain)
Expand Down
Loading
Loading