Files
hapi/ios/Hapi/Features/Home/AppCommands.swift
T
weishu e1093466c9 feat(ios): support macOS through Mac Catalyst
Build the app and notification extension for Mac Catalyst with the Mac
idiom, so Macs get a 1:1 native window instead of the scaled iPad app.
The Mac build keeps the run.hapi.app bundle ID and adds Mac entitlements
(sandbox, network, microphone, user-selected files, Mac APNs).

- Always use the split on Mac with a sidebar list style, denser rows,
  14pt reading text, a hidden duplicate titlebar title, a minimum window
  size and page-sized sheets; open web links in the default browser and
  hide the unavailable live QR scanner.
- Add menu bar / hardware-keyboard commands: New Session, Settings,
  Toggle Sidebar, a Session menu (settings, files, rename, export, stop,
  previous/next session) and a guide link.
- Composer: Return sends on Mac (Shift/Option-Return for a newline, IME
  untouched, first suggestion picked); dropped files and pasted images or
  copied files become attachments on all platforms.
- CI builds Mac Catalyst and runs the navigation, split and transcript
  geometry tests there; README documents the Mac build.
2026-11-03 11:15:11 +08:00

104 lines
3.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
/// Actions the focused home window offers to the menu bar (Mac) and the
/// hardware-keyboard shortcut overlay (iPad).
struct HomeCommandActions {
var newSession: () -> Void
var openSettings: () -> Void
/// Nil when the window has no sidebar to toggle (iPhone stack).
var toggleSidebar: (() -> Void)?
/// Selects the row above (-1) or below (+1) the open session.
var selectAdjacentSession: ((Int) -> Void)?
}
/// Actions of the chat shown in the focused window.
struct ChatCommandActions {
var openSessionSettings: () -> Void
var openFiles: () -> Void
var rename: () -> Void
var export: (() -> Void)?
/// Nil unless the agent is mid-turn.
var stop: (() -> Void)?
}
private struct HomeCommandActionsKey: FocusedValueKey {
typealias Value = HomeCommandActions
}
private struct ChatCommandActionsKey: FocusedValueKey {
typealias Value = ChatCommandActions
}
extension FocusedValues {
var homeCommands: HomeCommandActions? {
get { self[HomeCommandActionsKey.self] }
set { self[HomeCommandActionsKey.self] = newValue }
}
var chatCommands: ChatCommandActions? {
get { self[ChatCommandActionsKey.self] }
set { self[ChatCommandActionsKey.self] = newValue }
}
}
struct HapiCommands: Commands {
@FocusedValue(\.homeCommands) private var home
@FocusedValue(\.chatCommands) private var chat
@Environment(\.openURL) private var openURL
private static let guideURL = URL(string: "https://hapi.run/docs/guide/native-apps")
var body: some Commands {
CommandGroup(replacing: .newItem) {
Button("New Session") { home?.newSession() }
.keyboardShortcut("n")
.disabled(home == nil)
}
CommandGroup(replacing: .appSettings) {
Button("Settings…") { home?.openSettings() }
.keyboardShortcut(",")
.disabled(home == nil)
}
// The composer is plain text; Format › Bold/Italic would also
// shadow ⌘I.
CommandGroup(replacing: .textFormatting) {}
// Not a document app and no customizable toolbar.
CommandGroup(replacing: .saveItem) {}
CommandGroup(replacing: .importExport) {}
CommandGroup(replacing: .toolbar) {
Button("Toggle Sidebar") { home?.toggleSidebar?() }
.keyboardShortcut("s", modifiers: [.command, .control])
.disabled(home?.toggleSidebar == nil)
}
CommandGroup(replacing: .help) {
if let url = Self.guideURL {
Button("HAPI Guide") { openURL(url) }
}
}
CommandMenu("Session") {
Button("Session Settings…") { chat?.openSessionSettings() }
.keyboardShortcut("i")
.disabled(chat == nil)
Button("Session Files") { chat?.openFiles() }
.keyboardShortcut("o", modifiers: [.command, .shift])
.disabled(chat == nil)
Divider()
Button("Rename…") { chat?.rename() }
.disabled(chat == nil)
Button("Export Conversation…") { chat?.export?() }
.disabled(chat?.export == nil)
Divider()
Button("Stop") { chat?.stop?() }
.keyboardShortcut(".")
.disabled(chat?.stop == nil)
Divider()
Button("Previous Session") { home?.selectAdjacentSession?(-1) }
.keyboardShortcut(.upArrow, modifiers: [.command, .option])
.disabled(home?.selectAdjacentSession == nil)
Button("Next Session") { home?.selectAdjacentSession?(1) }
.keyboardShortcut(.downArrow, modifiers: [.command, .option])
.disabled(home?.selectAdjacentSession == nil)
}
}
}