diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index 34826aefeaf7..7bb43676e68d 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -517,11 +517,19 @@ final class NodeAppModel { private static let deepLinkKeyUserDefaultsKey = "deeplink.agent.key" private static let canvasUnattendedDeepLinkKey: String = NodeAppModel.generateDeepLinkKey() private static var apnsEnvironment: String { -#if DEBUG - "sandbox" -#else - "production" -#endif + // Determine the actual APNs environment from the embedded provisioning + // profile rather than the build configuration. Xcode development + // provisioning profiles always produce sandbox device tokens, even for + // Release builds, so #if DEBUG is unreliable. + if let path = Bundle.main.path(forResource: "embedded", ofType: "mobileprovision"), + let data = try? Data(contentsOf: URL(fileURLWithPath: path)), + let contents = String(data: data, encoding: .ascii), + contents.contains("aps-environment") { + if contents.contains("production") { + return "production" + } + } + return "sandbox" } private func refreshBrandingFromGateway() async { diff --git a/apps/ios/Sources/OpenClawApp.swift b/apps/ios/Sources/OpenClawApp.swift index c94b1209f8d6..a3b117a5fa4e 100644 --- a/apps/ios/Sources/OpenClawApp.swift +++ b/apps/ios/Sources/OpenClawApp.swift @@ -55,6 +55,13 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc self.registerBackgroundWakeRefreshTask() UNUserNotificationCenter.current().delegate = self application.registerForRemoteNotifications() + Task { + let center = UNUserNotificationCenter.current() + let status = await center.notificationSettings() + if status.authorizationStatus == .notDetermined { + _ = try? await center.requestAuthorization(options: [.alert, .sound, .badge]) + } + } return true } diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 97b2271b1bf4..f30c3b2403b4 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -1054,6 +1054,8 @@ export function renderApp(state: AppViewState) { }, onChatScroll: (event) => state.handleChatScroll(event), onDraftChange: (next) => (state.chatMessage = next), + onHistoryUp: () => state.chatHistoryUp(), + onHistoryDown: () => state.chatHistoryDown(), attachments: state.chatAttachments, onAttachmentsChange: (next) => (state.chatAttachments = next), onSend: () => state.handleSendChat(), diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index c5cf3573ac4d..696f81300275 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -310,6 +310,8 @@ export type AppViewState = { handleSendChat: (messageOverride?: string, opts?: { restoreDraft?: boolean }) => Promise; handleAbortChat: () => Promise; removeQueuedMessage: (id: string) => void; + chatHistoryUp: () => void; + chatHistoryDown: () => void; handleChatScroll: (event: Event) => void; resetToolStream: () => void; resetChatScroll: () => void; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 799ea9100c64..e25342c4a38d 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -154,6 +154,10 @@ export class OpenClawApp extends LitElement { @state() chatQueue: ChatQueueItem[] = []; @state() chatAttachments: ChatAttachment[] = []; @state() chatManualRefreshInFlight = false; + // Input history for up/down arrow navigation (not reactive — no re-render needed) + private chatInputHistory: string[] = []; + private chatInputHistoryCursor = -1; + private chatInputHistoryDraft = ""; // Sidebar state for tool output viewing @state() sidebarOpen = false; @state() sidebarContent: string | null = null; @@ -496,6 +500,19 @@ export class OpenClawApp extends LitElement { messageOverride?: string, opts?: Parameters[2], ) { + const text = (messageOverride ?? this.chatMessage).trim(); + if (text) { + // Avoid consecutive duplicates in history + if (this.chatInputHistory[this.chatInputHistory.length - 1] !== text) { + this.chatInputHistory.push(text); + // Cap history at 100 entries + if (this.chatInputHistory.length > 100) { + this.chatInputHistory.shift(); + } + } + this.chatInputHistoryCursor = -1; + this.chatInputHistoryDraft = ""; + } await handleSendChatInternal( this as unknown as Parameters[0], messageOverride, @@ -503,6 +520,35 @@ export class OpenClawApp extends LitElement { ); } + chatHistoryUp() { + const history = this.chatInputHistory; + if (history.length === 0) { + return; + } + if (this.chatInputHistoryCursor === -1) { + // Save current draft before navigating + this.chatInputHistoryDraft = this.chatMessage; + this.chatInputHistoryCursor = history.length - 1; + } else if (this.chatInputHistoryCursor > 0) { + this.chatInputHistoryCursor--; + } + this.chatMessage = history[this.chatInputHistoryCursor] ?? ""; + } + + chatHistoryDown() { + if (this.chatInputHistoryCursor === -1) { + return; + } + if (this.chatInputHistoryCursor < this.chatInputHistory.length - 1) { + this.chatInputHistoryCursor++; + this.chatMessage = this.chatInputHistory[this.chatInputHistoryCursor] ?? ""; + } else { + // Past the end — restore original draft + this.chatInputHistoryCursor = -1; + this.chatMessage = this.chatInputHistoryDraft; + } + } + async handleWhatsAppStart(force: boolean) { await handleWhatsAppStartInternal(this, force); } diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts index e63f56c25fa7..827e3f8e3e80 100644 --- a/ui/src/ui/views/chat.ts +++ b/ui/src/ui/views/chat.ts @@ -73,6 +73,8 @@ export type ChatProps = { onToggleFocusMode: () => void; onDraftChange: (next: string) => void; onSend: () => void; + onHistoryUp?: () => void; + onHistoryDown?: () => void; onAbort?: () => void; onQueueRemove: (id: string) => void; onNewSession: () => void; @@ -253,7 +255,7 @@ export function renderChat(props: ChatProps) { const composePlaceholder = props.connected ? hasAttachments ? "Add a message or paste more images..." - : "Message (↩ to send, Shift+↩ for line breaks, paste images)" + : "Message (↩ send, Shift+↩ newline, ↑↓ history)" : "Connect to the gateway to start chatting…"; const splitRatio = props.splitRatio ?? 0.6; @@ -431,21 +433,35 @@ export function renderChat(props: ChatProps) { dir=${detectTextDirection(props.draft)} ?disabled=${!props.connected} @keydown=${(e: KeyboardEvent) => { - if (e.key !== "Enter") { - return; - } if (e.isComposing || e.keyCode === 229) { return; } - if (e.shiftKey) { + if (e.key === "Enter") { + if (e.shiftKey) { + return; + } // Allow Shift+Enter for line breaks + if (!props.connected) { + return; + } + e.preventDefault(); + if (canCompose) { + props.onSend(); + } return; - } // Allow Shift+Enter for line breaks - if (!props.connected) { + } + // Up/Down arrow history navigation (only when input is empty or single-line) + const textarea = e.target as HTMLTextAreaElement; + const value = textarea.value; + const isMultiline = value.includes("\n"); + if (e.key === "ArrowUp" && !isMultiline && props.onHistoryUp) { + e.preventDefault(); + props.onHistoryUp(); return; } - e.preventDefault(); - if (canCompose) { - props.onSend(); + if (e.key === "ArrowDown" && !isMultiline && props.onHistoryDown) { + e.preventDefault(); + props.onHistoryDown(); + return; } }} @input=${(e: Event) => {