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) => {