Skip to content

Commit 92e64f8

Browse files
Merge remote-tracking branch 'origin/main' into v2
# Conflicts: # packages/web/src/store.ts
2 parents eb3f21c + 0f61615 commit 92e64f8

42 files changed

Lines changed: 1713 additions & 114 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release-mac.yml

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
name: Release macOS DMG
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
build-macos-dmg:
13+
runs-on: macos-15
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Setup Node.js
19+
uses: actions/setup-node@v4
20+
with:
21+
node-version: 24
22+
23+
- name: Extract version from tag
24+
id: version
25+
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
26+
27+
- name: Install dependencies
28+
run: npm ci
29+
30+
- name: Build web frontend
31+
run: npm run build -w packages/web
32+
env:
33+
VITE_FIREBASE_API_KEY: AIzaSyA8NdosgijoZoOSTHoRY4XA5WH-58OF3Yk
34+
VITE_FIREBASE_AUTH_DOMAIN: botschat-130ff.firebaseapp.com
35+
VITE_FIREBASE_PROJECT_ID: botschat-130ff
36+
VITE_GA_MEASUREMENT_ID: G-HKJNG1X0JE
37+
38+
- name: Install XcodeGen
39+
run: brew install xcodegen
40+
41+
- name: Generate Xcode project
42+
working-directory: macos
43+
run: xcodegen generate
44+
45+
- name: Import signing certificate
46+
env:
47+
MACOS_CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }}
48+
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
49+
run: |
50+
KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db"
51+
KEYCHAIN_PASSWORD="$(uuidgen)"
52+
53+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
54+
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
55+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
56+
57+
echo "$MACOS_CERTIFICATE_P12" | base64 --decode > "$RUNNER_TEMP/certificate.p12"
58+
security import "$RUNNER_TEMP/certificate.p12" \
59+
-P "$MACOS_CERTIFICATE_PASSWORD" \
60+
-A \
61+
-t cert \
62+
-f pkcs12 \
63+
-k "$KEYCHAIN_PATH"
64+
65+
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
66+
security default-keychain -s "$KEYCHAIN_PATH"
67+
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
68+
69+
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
70+
echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV"
71+
72+
- name: Build and archive
73+
working-directory: macos
74+
run: |
75+
xcodebuild archive \
76+
-project BotsChatMac.xcodeproj \
77+
-scheme BotsChatMac \
78+
-configuration Release \
79+
-destination "generic/platform=macOS" \
80+
-archivePath "$RUNNER_TEMP/BotsChatMac.xcarchive" \
81+
MARKETING_VERSION="${{ steps.version.outputs.VERSION }}" \
82+
CURRENT_PROJECT_VERSION="${{ github.run_number }}" \
83+
CODE_SIGN_STYLE=Manual \
84+
CODE_SIGN_IDENTITY="Developer ID Application" \
85+
DEVELOPMENT_TEAM=C5N5PPC329 \
86+
OTHER_CODE_SIGN_FLAGS="--keychain $RUNNER_TEMP/signing.keychain-db" \
87+
ARCHS="x86_64 arm64" \
88+
ONLY_ACTIVE_ARCH=NO
89+
90+
- name: Export archive
91+
run: |
92+
cat > "$RUNNER_TEMP/ExportOptions.plist" << 'PLIST'
93+
<?xml version="1.0" encoding="UTF-8"?>
94+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
95+
<plist version="1.0">
96+
<dict>
97+
<key>method</key>
98+
<string>developer-id</string>
99+
<key>teamID</key>
100+
<string>C5N5PPC329</string>
101+
<key>signingStyle</key>
102+
<string>manual</string>
103+
<key>signingCertificate</key>
104+
<string>Developer ID Application</string>
105+
</dict>
106+
</plist>
107+
PLIST
108+
109+
xcodebuild -exportArchive \
110+
-archivePath "$RUNNER_TEMP/BotsChatMac.xcarchive" \
111+
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \
112+
-exportPath "$RUNNER_TEMP/export"
113+
114+
- name: Verify code signature
115+
run: |
116+
APP_PATH="$RUNNER_TEMP/export/BotsChat.app"
117+
echo "=== Code signature info ==="
118+
codesign -dvv "$APP_PATH"
119+
echo ""
120+
echo "=== Signature verification ==="
121+
codesign --verify --deep --strict "$APP_PATH"
122+
echo "Signature OK"
123+
124+
- name: Create DMG
125+
run: |
126+
DMG_NAME="BotsChat-mac.dmg"
127+
DMG_DIR="$RUNNER_TEMP/dmg-contents"
128+
129+
mkdir -p "$DMG_DIR"
130+
cp -R "$RUNNER_TEMP/export/BotsChat.app" "$DMG_DIR/"
131+
ln -s /Applications "$DMG_DIR/Applications"
132+
133+
hdiutil create \
134+
-volname "BotsChat" \
135+
-srcfolder "$DMG_DIR" \
136+
-ov \
137+
-format UDZO \
138+
"$RUNNER_TEMP/$DMG_NAME"
139+
140+
codesign --force --sign "Developer ID Application" \
141+
--keychain "$KEYCHAIN_PATH" \
142+
"$RUNNER_TEMP/$DMG_NAME"
143+
144+
echo "DMG_PATH=$RUNNER_TEMP/$DMG_NAME" >> "$GITHUB_ENV"
145+
echo "DMG_NAME=$DMG_NAME" >> "$GITHUB_ENV"
146+
147+
- name: Notarize DMG
148+
env:
149+
APPLE_ID: ${{ secrets.APPLE_ID }}
150+
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
151+
run: |
152+
echo "Submitting $DMG_NAME for notarization..."
153+
xcrun notarytool submit "$DMG_PATH" \
154+
--apple-id "$APPLE_ID" \
155+
--password "$APPLE_ID_PASSWORD" \
156+
--team-id "C5N5PPC329" \
157+
--wait
158+
159+
echo "Stapling notarization ticket to DMG..."
160+
xcrun stapler staple "$DMG_PATH"
161+
162+
- name: Upload DMG to GitHub Release
163+
uses: softprops/action-gh-release@v2
164+
with:
165+
files: ${{ env.DMG_PATH }}
166+
generate_release_notes: true
167+
168+
- name: Cleanup keychain
169+
if: always()
170+
run: security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@
33
[![npm](https://img.shields.io/npm/v/botschat)](https://www.npmjs.com/package/botschat)
44
[![npm](https://img.shields.io/npm/v/@botschat/botschat)](https://www.npmjs.com/package/@botschat/botschat)
55
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
6+
[![macOS](https://img.shields.io/badge/Download-macOS_App-black?logo=apple&logoColor=white)](https://github.com/botschat-app/botsChat/releases/latest/download/BotsChat-mac.dmg)
7+
[![iOS](https://img.shields.io/badge/Download-iOS_App-blue?logo=apple&logoColor=white)](https://apps.apple.com/app/botschat-chat-with-agent/id6759292058)
68

79
A self-hosted, **end-to-end encrypted** chat interface for [OpenClaw](https://github.com/openclaw/openclaw) AI agents.
810

911
BotsChat gives you a modern, Slack-like web UI to interact with your OpenClaw agents — organize conversations into **Channels**, schedule **Background Tasks**, and monitor **Job** executions. With **E2E encryption**, your chat messages, cron prompts, and job summaries are encrypted on your device before they ever leave — the server only sees ciphertext it cannot decrypt. Your API keys and data never leave your machine.
1012

13+
<div align="center">
14+
15+
https://github.com/user-attachments/assets/e727ef9e-53b9-40d4-b943-c02019588203
16+
17+
[▶ Watch in HD on YouTube](https://www.youtube.com/watch?v=_ifqYhoV7Jk)
18+
19+
</div>
20+
1121
## Key Features
1222

1323
### Structured Conversation Management
@@ -95,6 +105,8 @@ BotsChat is **100% open source** — the [same code](https://github.com/botschat
95105
| **B. Run Locally** | Development, no cloud account | Yes |
96106
| **C. Deploy to Cloudflare** | Remote access (e.g. from phone) | Yes |
97107

108+
> **Native Apps**: A macOS client is available — [download the latest DMG](https://github.com/botschat-app/botsChat/releases/latest/download/BotsChat-mac.dmg) (signed and notarized, Apple Silicon + Intel). An iOS app is also available on the [App Store](https://apps.apple.com/app/botschat-chat-with-agent/id6759292058).
109+
98110
Pick one below and follow its steps, then continue to [Install the OpenClaw Plugin](#install-the-openclaw-plugin).
99111

100112
---

macos/.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Xcode generated project (regenerated from project.yml via xcodegen)
2+
BotsChatMac.xcodeproj/
3+
4+
# Xcode build artifacts
5+
build/
6+
DerivedData/
7+
8+
# User-specific Xcode settings
9+
*.xcuserdata
10+
*.xcuserstate
11+
12+
# macOS
13+
.DS_Store

macos/BotsChatMac/AppIcon.icns

127 KB
Binary file not shown.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"colors" : [
3+
{
4+
"color" : {
5+
"color-space" : "srgb",
6+
"components" : {
7+
"alpha" : "1.000",
8+
"blue" : "0.980",
9+
"green" : "0.580",
10+
"red" : "0.200"
11+
}
12+
},
13+
"idiom" : "universal"
14+
}
15+
],
16+
"info" : {
17+
"author" : "xcode",
18+
"version" : 1
19+
}
20+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import SwiftUI
2+
import UserNotifications
3+
4+
@main
5+
struct BotsChatApp: App {
6+
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
7+
8+
var body: some Scene {
9+
WindowGroup {
10+
ContentView()
11+
.frame(minWidth: 900, minHeight: 600)
12+
}
13+
.windowStyle(.titleBar)
14+
.windowToolbarStyle(.unified(showsTitle: true))
15+
.defaultSize(width: 1200, height: 800)
16+
.commands {
17+
CommandGroup(replacing: .newItem) {}
18+
19+
CommandMenu("Chat") {
20+
Button("New Session") {
21+
NotificationCenter.default.post(name: .newSession, object: nil)
22+
}
23+
.keyboardShortcut("n", modifiers: .command)
24+
25+
Divider()
26+
27+
Button("Settings...") {
28+
NotificationCenter.default.post(name: .openSettings, object: nil)
29+
}
30+
.keyboardShortcut(",", modifiers: .command)
31+
}
32+
33+
CommandGroup(replacing: .toolbar) {
34+
Button("Toggle Sidebar") {
35+
NotificationCenter.default.post(name: .toggleSidebar, object: nil)
36+
}
37+
.keyboardShortcut("s", modifiers: [.command, .control])
38+
}
39+
}
40+
}
41+
}
42+
43+
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
44+
func applicationDidFinishLaunching(_ notification: Notification) {
45+
NSWindow.allowsAutomaticWindowTabbing = false
46+
47+
let center = UNUserNotificationCenter.current()
48+
center.delegate = self
49+
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
50+
if let error = error {
51+
print("[Notification] Permission error: \(error.localizedDescription)")
52+
}
53+
print("[Notification] Permission granted: \(granted)")
54+
}
55+
}
56+
57+
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
58+
return true
59+
}
60+
61+
// Show notifications even when the app is in the foreground (will be filtered
62+
// by JS — only sent when the window is not focused / document is hidden).
63+
func userNotificationCenter(
64+
_ center: UNUserNotificationCenter,
65+
willPresent notification: UNNotification,
66+
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
67+
) {
68+
completionHandler([.banner, .sound])
69+
}
70+
71+
// When the user taps a notification, bring the app to front and navigate
72+
func userNotificationCenter(
73+
_ center: UNUserNotificationCenter,
74+
didReceive response: UNNotificationResponse,
75+
withCompletionHandler completionHandler: @escaping () -> Void
76+
) {
77+
NSApp.activate(ignoringOtherApps: true)
78+
if let window = NSApp.windows.first {
79+
window.makeKeyAndOrderFront(nil)
80+
}
81+
82+
let userInfo = response.notification.request.content.userInfo
83+
if let sessionKey = userInfo["sessionKey"] as? String {
84+
NotificationCenter.default.post(
85+
name: .pushNavigation,
86+
object: nil,
87+
userInfo: ["sessionKey": sessionKey]
88+
)
89+
}
90+
91+
completionHandler()
92+
}
93+
}
94+
95+
extension Notification.Name {
96+
static let newSession = Notification.Name("BotsChatNewSession")
97+
static let openSettings = Notification.Name("BotsChatOpenSettings")
98+
static let toggleSidebar = Notification.Name("BotsChatToggleSidebar")
99+
static let pushNavigation = Notification.Name("BotsChatPushNavigation")
100+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.app-sandbox</key>
6+
<true/>
7+
<key>com.apple.security.network.client</key>
8+
<true/>
9+
<key>com.apple.security.network.server</key>
10+
<true/>
11+
<key>com.apple.security.files.user-selected.read-write</key>
12+
<true/>
13+
</dict>
14+
</plist>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import SwiftUI
2+
import WebKit
3+
4+
struct ContentView: View {
5+
@StateObject private var webViewManager = WebViewManager()
6+
7+
var body: some View {
8+
WebViewContainer(manager: webViewManager)
9+
.ignoresSafeArea()
10+
.onReceive(NotificationCenter.default.publisher(for: .openSettings)) { _ in
11+
webViewManager.evaluateJS("document.querySelector('[data-settings-btn]')?.click()")
12+
}
13+
.onReceive(NotificationCenter.default.publisher(for: .toggleSidebar)) { _ in
14+
webViewManager.evaluateJS("""
15+
document.querySelector('[data-sidebar-toggle]')?.click()
16+
""")
17+
}
18+
.onReceive(NotificationCenter.default.publisher(for: .pushNavigation)) { notification in
19+
if let sessionKey = notification.userInfo?["sessionKey"] as? String {
20+
let escaped = sessionKey.replacingOccurrences(of: "'", with: "\\'")
21+
webViewManager.evaluateJS("""
22+
window.dispatchEvent(new CustomEvent('botschat:push-nav', {
23+
detail: { sessionKey: '\(escaped)' }
24+
}));
25+
""")
26+
}
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)