Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 13 additions & 5 deletions apps/ios/Sources/Model/NodeAppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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("<key>aps-environment</key>") {
if contents.contains("<string>production</string>") {
return "production"
}
}
return "sandbox"
}

private func refreshBrandingFromGateway() async {
Expand Down
7 changes: 7 additions & 0 deletions apps/ios/Sources/OpenClawApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 2 additions & 0 deletions ui/src/ui/app-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 2 additions & 0 deletions ui/src/ui/app-view-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ export type AppViewState = {
handleSendChat: (messageOverride?: string, opts?: { restoreDraft?: boolean }) => Promise<void>;
handleAbortChat: () => Promise<void>;
removeQueuedMessage: (id: string) => void;
chatHistoryUp: () => void;
chatHistoryDown: () => void;
handleChatScroll: (event: Event) => void;
resetToolStream: () => void;
resetChatScroll: () => void;
Expand Down
46 changes: 46 additions & 0 deletions ui/src/ui/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -496,13 +500,55 @@ export class OpenClawApp extends LitElement {
messageOverride?: string,
opts?: Parameters<typeof handleSendChatInternal>[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<typeof handleSendChatInternal>[0],
messageOverride,
opts,
);
}

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);
}
Expand Down
36 changes: 26 additions & 10 deletions ui/src/ui/views/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) => {
Expand Down