Skip to content

Polish macOS dashboard UI#6262

Merged
kodjima33 merged 2 commits into
mainfrom
nik/macos-ui-polish
Apr 1, 2026
Merged

Polish macOS dashboard UI#6262
kodjima33 merged 2 commits into
mainfrom
nik/macos-ui-polish

Conversation

@kodjima33
Copy link
Copy Markdown
Collaborator

Summary

  • polish the macOS desktop dashboard/chat/conversation chrome to better match the older Flutter app
  • tighten the memories page so more memories fit and move the brain map inline above the memories list
  • add export-backed verification hooks used for local/mac mini UI checks

Verification

  • rebuilt and launched /Applications/Omi Dev.app locally
  • exported and reviewed full-page renders for dashboard, chat, memories, tasks, rewind, and apps
  • verified the follow-up memories density pass with a fresh full-memories.png export

Notes

  • settings and permissions exports still fail in the existing exporter path
  • no direct merge to main; PR path only

@kodjima33 kodjima33 force-pushed the nik/macos-ui-polish branch from 27f7dad to 37246c1 Compare April 1, 2026 19:36
@kodjima33 kodjima33 merged commit 525c38e into main Apr 1, 2026
2 checks passed
@kodjima33 kodjima33 deleted the nik/macos-ui-polish branch April 1, 2026 19:37
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 1, 2026

Greptile Summary

This PR is a macOS desktop UI polish pass that tightens spacing, upgrades corner radii, and introduces a shared design-token layer (OmiChrome, updated OmiColors) so that all surfaces use consistent radii, shadows, and colour values. It also adds ViewExporter.swift — a local dev tool that renders each major page to PNG/PDF by spawning isolated subprocesses, and is invoked via --export-views / --export-fullpages flags at launch.

Key changes:

  • OmiChrome.swift (new) — centralises radius constants and exposes two reusable modifiers: omiPanel (card chrome with shadow) and omiControlSurface (interactive element surface).
  • OmiColors.swift — adds backgroundRaised, textQuaternary, and amber; the new amber constant is identical to the existing warning constant (0xF59E0B), which is a minor redundancy.
  • DesktopHomeView — main content container corner radius raised to OmiChrome.windowRadius (26 pt) with a subtle gradient fill.
  • MemoriesPage — memory cards become compact 2-line-truncated Button views with card chrome; brain map (MemoryGraphInlineCard) is now inlined above the list rather than behind a separate tab.
  • MemoryGraphPage — new MemoryGraphInlineCard component added; existing full-screen page preserved for modal use.
  • ChatInputView / ChatPage — input area wrapped in omiPanel; Ask/Act toggle migrated to new palette.
  • ConversationsPage / ConversationListView / ConversationRowView — spacing, padding, and control surfaces updated; whole-file re-indent from 4-space to 2-space.
  • ViewExporter.swift (new) — subprocess-isolated PNG/PDF exporter for local UI verification; a few maintainability issues noted in inline comments (unread stderr pipe, hardcoded view counts, Apple-Silicon-only pdf2svg path).

Confidence Score: 5/5

  • Safe to merge — all findings are P2 style/maintainability suggestions with no impact on production runtime behaviour.
  • Every UI change is cosmetic (spacing, radii, colours, re-indent) with no logic mutations. The new ViewExporter is dev-only code gated behind CLI flags and never runs in normal app operation. The three noted issues (unread stderr pipe, hardcoded view counts, Intel pdf2svg path) only affect the local export tool and carry no production risk.
  • desktop/Desktop/Sources/ViewExporter.swift — contains the unread stderr pipe and hardcoded count values worth fixing before the exporter is used on an Intel Mac or with verbose subprocess logging.

Important Files Changed

Filename Overview
desktop/Desktop/Sources/ViewExporter.swift New dev-tool file that renders SwiftUI views to PNG/PDF via subprocess isolation; contains a stderr pipe that is never consumed (potential deadlock) and hardcoded view counts that must be manually kept in sync.
desktop/Desktop/Sources/Theme/OmiChrome.swift New file centralising design tokens (radii) and reusable omiPanel/omiControlSurface view modifiers; clean and well-structured.
desktop/Desktop/Sources/Theme/OmiColors.swift Adds backgroundRaised, textQuaternary, and amber (duplicate of warning) colours; re-indented to 2-space style.
desktop/Desktop/Sources/OmiApp.swift Adds early-exit hook in applicationDidFinishLaunching to run ViewExporter when export flags are present; rest of app startup is unchanged.
desktop/Desktop/Sources/MainWindow/Pages/MemoriesPage.swift Major UI density pass: memory cards become 2-line-truncated buttons with card chrome; header search/filter bar tightened; re-indented to 2-space style; logic unchanged.
desktop/Desktop/Sources/MainWindow/Pages/MemoryGraph/MemoryGraphPage.swift Adds new MemoryGraphInlineCard component so the brain map embeds above the memory list; existing full-screen MemoryGraphPage is preserved for modal use; re-indented to 2-space style.
desktop/Desktop/Sources/MainWindow/DesktopHomeView.swift Content container corner radius updated to OmiChrome.windowRadius (26 pt), gradient background added to main content area, padding tweaked from 12 to 14 pt.
desktop/Desktop/Sources/MainWindow/Components/ChatInputView.swift Input area wrapped in omiPanel with adjusted corner radii and colours; Ask/Act toggle background and active-state colour updated to match new palette.
desktop/Desktop/Sources/MainWindow/Pages/ConversationsPage.swift Padding and spacing adjustments across filter buttons, list header, and merge bar; "Try Again" and filter chip buttons migrated to omiControlSurface; re-indented to 2-space style.
desktop/Desktop/Sources/MainWindow/Components/ConversationListView.swift Section header font size nudged from 12 to 13 pt, spacing tightened; horizontal padding increased from 16 to 24 pt; re-indented to 2-space style; no logic changes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[App Launch] -->|"--export-views / --export-fullpages flag"| B[ViewExporter.run]
    A -->|normal launch| Z[DesktopHomeView]

    B --> C{Mode?}
    C -->|--export-views| D[runBatch standalone\ncount=15]
    C -->|--export-fullpages| E[runBatch fullpage\ncount=11]
    C -->|--export-single i dir| F[exportView single standalone]
    C -->|--export-fullpage-single i dir| G[exportView single fullpage]

    D -->|"spawn subprocess per view\n(0..14)"| F
    E -->|"spawn subprocess per view\n(0..10)"| G

    F --> H[NSHostingView render]
    G --> H
    H --> I[Write PNG]
    H --> J[Write PDF]
    D --> K[convertPDFsToSVG\n/opt/homebrew/bin/pdf2svg]
    E --> K

    Z --> L[SidebarView]
    Z --> M[Content Area\nOmiChrome.windowRadius=26pt\ngradient background]
    M --> N[MemoriesPage\nMemoryGraphInlineCard above list\n2-line truncated cards]
    M --> O[ChatPage\nomiPanel input area]
    M --> P[ConversationsPage\nomiControlSurface chips]
    M --> Q[DashboardPage\nspacing 28pt / padding 30pt]
Loading

Reviews (1): Last reviewed commit: "Refine memories page density" | Re-trigger Greptile

Comment on lines +485 to +518
for i in 0..<count {
let viewName: String
if flag == "--export-single" {
viewName = standaloneViewAt(i)?.0 ?? "unknown-\(i)"
} else {
viewName = fullPageViewAt(i)?.0 ?? "unknown-\(i)"
}
NSLog("ViewExporter: [\(i+1)/\(count)] Spawning subprocess for \(viewName)...")

let process = Process()
process.executableURL = URL(fileURLWithPath: executablePath)
process.arguments = [flag, "\(i)", dir]
process.standardError = Pipe()
process.standardOutput = FileHandle.nullDevice

do {
try process.run()
process.waitUntilExit()

if process.terminationStatus == 0 {
NSLog("ViewExporter: [\(i+1)/\(count)] \(viewName) OK")
exportedCount += 1
} else {
NSLog(
"ViewExporter: [\(i+1)/\(count)] \(viewName) FAILED (exit \(process.terminationStatus))"
)
failedCount += 1
crashedViews.append(viewName)
}
} catch {
NSLog("ViewExporter: [\(i+1)/\(count)] \(viewName) FAILED (\(error.localizedDescription))")
failedCount += 1
crashedViews.append(viewName)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Subprocess stderr pipe never consumed — could deadlock on verbose output

process.standardError = Pipe() captures stderr but the Pipe's fileHandleForReading is never read. On macOS a Pipe has a 64 KB kernel buffer. If any subprocess writes more than that to stderr (e.g. from SwiftUI layout warnings or added logging), the subprocess blocks on write() while the parent is blocked on process.waitUntilExit(), causing a permanent hang.

For a quick fix, drain stderr on a background thread or just send it to /dev/null:

Suggested change
for i in 0..<count {
let viewName: String
if flag == "--export-single" {
viewName = standaloneViewAt(i)?.0 ?? "unknown-\(i)"
} else {
viewName = fullPageViewAt(i)?.0 ?? "unknown-\(i)"
}
NSLog("ViewExporter: [\(i+1)/\(count)] Spawning subprocess for \(viewName)...")
let process = Process()
process.executableURL = URL(fileURLWithPath: executablePath)
process.arguments = [flag, "\(i)", dir]
process.standardError = Pipe()
process.standardOutput = FileHandle.nullDevice
do {
try process.run()
process.waitUntilExit()
if process.terminationStatus == 0 {
NSLog("ViewExporter: [\(i+1)/\(count)] \(viewName) OK")
exportedCount += 1
} else {
NSLog(
"ViewExporter: [\(i+1)/\(count)] \(viewName) FAILED (exit \(process.terminationStatus))"
)
failedCount += 1
crashedViews.append(viewName)
}
} catch {
NSLog("ViewExporter: [\(i+1)/\(count)] \(viewName) FAILED (\(error.localizedDescription))")
failedCount += 1
crashedViews.append(viewName)
}
let process = Process()
process.executableURL = URL(fileURLWithPath: executablePath)
process.arguments = [flag, "\(i)", dir]
process.standardError = FileHandle.nullDevice
process.standardOutput = FileHandle.nullDevice

If you want to capture subprocess output for debugging, use process.standardError = Pipe() and consume pipe.fileHandleForReading.readDataToEndOfFile() after waitUntilExit(), or read it asynchronously before waiting.

return (entry.0, entry.1(), entry.2)
}

static var standaloneViewCount: Int { 15 }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hardcoded counts must be kept in sync with the view arrays manually

standaloneViewCount = 15 and fullPageCount = 11 (line 307) are magic numbers that must match the actual number of entries in standaloneViewAt / fullPageViewAt. If someone adds a view to one of those arrays without updating the matching count, the batch exporter silently skips the new view — the out-of-range index causes the subprocess to return 1 and crashedViews gets a spurious entry.

Consider deriving the counts from the arrays themselves. One approach is to move each registry into a static stored property so .count is accessible:

// Instead of a local `let views: [...] = [...]` inside the function,
// make it a static stored property:
private static let standaloneViews: [(String, () -> AnyView, CGSize)] = [...]
static var standaloneViewCount: Int { standaloneViews.count }
static func standaloneViewAt(_ index: Int) -> (String, AnyView, CGSize)? { ... }

At minimum, add an assert(standaloneViewCount == /* expected */ 15) or a unit test that validates the count.

Comment on lines +592 to +593
let pdf2svgPath = "/opt/homebrew/bin/pdf2svg"
guard FileManager.default.fileExists(atPath: pdf2svgPath) else {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 pdf2svg path hard-codes Apple Silicon Homebrew prefix

/opt/homebrew/bin/pdf2svg is the Homebrew prefix for Apple Silicon. Intel Macs use /usr/local/bin/pdf2svg, so the SVG conversion step is silently skipped on Intel hardware with the log message "pdf2svg not found".

Suggested change
let pdf2svgPath = "/opt/homebrew/bin/pdf2svg"
guard FileManager.default.fileExists(atPath: pdf2svgPath) else {
let pdf2svgPath: String = {
// Homebrew on Apple Silicon lives in /opt/homebrew; Intel Macs use /usr/local
let asSilicon = "/opt/homebrew/bin/pdf2svg"
let intel = "/usr/local/bin/pdf2svg"
return FileManager.default.fileExists(atPath: asSilicon) ? asSilicon : intel
}()

static let warning = Color(hex: 0xF59E0B) // Amber
static let error = Color(hex: 0xEF4444) // Red
static let info = Color(hex: 0x3B82F6) // Blue
static let amber = Color(hex: 0xF59E0B) // Same as warning, for starred items
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 amber duplicates warning — same hex value 0xF59E0B

Both OmiColors.warning (line 32) and the new OmiColors.amber resolve to the same colour. Having two names for the same semantic value creates confusion about which to use and risks them drifting in the future.

Consider either re-using warning for starred items and documenting the dual purpose, or defining amber as an alias:

static let amber: Color = warning  // same amber/warning yellow

This makes the relationship explicit and avoids silent divergence.

Glucksberg pushed a commit to Glucksberg/omi-local that referenced this pull request Apr 28, 2026
* Polish macOS dashboard UI

* Refine memories page density
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant