Skip to content

Commit edbb26a

Browse files
authored
Merge branch 'main' into fix-usage-tooltip-clipping
2 parents efd1461 + 45d9a09 commit edbb26a

435 files changed

Lines changed: 17398 additions & 3885 deletions

File tree

Some content is hidden

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

.agents/skills/crabbox/SKILL.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,13 @@ Use the smallest Crabbox lane that proves the reported user path, not just the
287287
touched code. Aim for one after-fix E2E proof before commenting, closing, or
288288
opening a PR for a user-visible bug.
289289

290+
When the user says "test in Crabbox", do not simply copy tests to the remote
291+
box and run them there. Crabbox is for remote real-scenario proof: copy or
292+
install OpenClaw as the user would, run the same setup/update/CLI/Gateway/API
293+
call that failed, and capture behavior from that entrypoint. For regressions or
294+
bug reports, prove the broken state first when feasible, then run the same
295+
scenario after the fix.
296+
290297
Pick the lane by symptom:
291298

292299
- Docker/setup/install bug: build a package tarball and run the matching
@@ -308,8 +315,9 @@ Pick the lane by symptom:
308315

309316
Efficient flow:
310317

311-
1. Reproduce or prove the pre-fix symptom when feasible. If the issue cannot be
312-
reproduced, capture the exact command and observed behavior instead.
318+
1. Reproduce or prove the pre-fix symptom from the real user-facing entrypoint
319+
when feasible. If the issue cannot be reproduced, capture the exact command
320+
and observed behavior instead.
313321
2. Patch locally and run narrow local tests for edit speed.
314322
3. Run one Crabbox E2E command that starts from the user-facing entrypoint:
315323
package install, Docker setup, onboarding, channel add, gateway start, or

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Skills own workflows; root owns hard policy and routing.
6565
## Validation
6666

6767
- Use `$openclaw-testing` for test/CI choice and `$crabbox` for remote/full/E2E proof.
68+
- Crabbox request means real scenario proof: install/update/call/repro user path; not just copy tests and run them remotely.
6869
- Small/narrow tests, lints, format checks, and type probes are fine locally only in a healthy normal checkout.
6970
- In Codex worktrees, direct local `pnpm test*`, `pnpm check*`, `pnpm crabbox:run`, and `scripts/committer` can trigger pnpm dependency reconciliation or install prompts. Prefer `node` wrappers locally and Crabbox/Testbox for pnpm-gated proof.
7071
- Full suites, broad changed gates, Docker/package/E2E/live/cross-OS proof, or anything that bogs down the Mac: Crabbox/Testbox.

CHANGELOG.md

Lines changed: 55 additions & 0 deletions
Large diffs are not rendered by default.

apps/macos/Sources/OpenClaw/AppState.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -380,10 +380,10 @@ final class AppState {
380380
{
381381
self.remoteTarget = configRemoteTarget
382382
} else if resolvedConnectionMode == .remote,
383-
configRemoteTransport != .direct,
384-
storedRemoteTarget.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
385-
let host = AppState.remoteHost(from: configRemoteUrl),
386-
!LoopbackHost.isLoopbackHost(host)
383+
configRemoteTransport != .direct,
384+
storedRemoteTarget.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
385+
let host = AppState.remoteHost(from: configRemoteUrl),
386+
!LoopbackHost.isLoopbackHost(host)
387387
{
388388
self.remoteTarget = "\(NSUserName())@\(host)"
389389
} else {

apps/macos/Sources/OpenClaw/ContextRootMenuLabelView.swift

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,51 @@ struct ContextRootMenuLabelView: View {
99
MenuItemHighlightColors.palette(self.isHighlighted)
1010
}
1111

12+
private var usesStackedLayout: Bool {
13+
self.subtitle.count > 28 || self.subtitle.contains("\n")
14+
}
15+
1216
var body: some View {
13-
HStack(alignment: .firstTextBaseline, spacing: 8) {
14-
Text("Context")
15-
.font(.callout.weight(.semibold))
16-
.foregroundStyle(self.palette.primary)
17-
.lineLimit(1)
18-
.layoutPriority(1)
17+
HStack(alignment: self.usesStackedLayout ? .top : .firstTextBaseline, spacing: 8) {
18+
VStack(alignment: .leading, spacing: 2) {
19+
Text("Context")
20+
.font(.callout.weight(.semibold))
21+
.foregroundStyle(self.palette.primary)
22+
.lineLimit(1)
23+
24+
if self.usesStackedLayout {
25+
self.subtitleText
26+
.lineLimit(3)
27+
.fixedSize(horizontal: false, vertical: true)
28+
}
29+
}
30+
.layoutPriority(1)
1931

2032
Spacer(minLength: 8)
2133

22-
Text(self.subtitle)
23-
.font(.caption.monospacedDigit())
24-
.foregroundStyle(self.palette.secondary)
25-
.lineLimit(1)
26-
.truncationMode(.tail)
27-
.layoutPriority(2)
34+
if !self.usesStackedLayout {
35+
self.subtitleText
36+
.lineLimit(1)
37+
.layoutPriority(2)
38+
}
2839

2940
Image(systemName: "chevron.right")
3041
.font(.caption.weight(.semibold))
3142
.foregroundStyle(self.palette.secondary)
3243
.padding(.leading, 2)
44+
.padding(.top, self.usesStackedLayout ? 2 : 0)
3345
}
34-
.padding(.vertical, 8)
46+
.padding(.vertical, self.usesStackedLayout ? 7 : 8)
3547
.padding(.leading, 22)
3648
.padding(.trailing, 14)
3749
.frame(width: max(1, self.width), alignment: .leading)
3850
}
51+
52+
private var subtitleText: some View {
53+
Text(self.subtitle)
54+
.font(.caption.monospacedDigit())
55+
.foregroundStyle(self.palette.secondary)
56+
.multilineTextAlignment(.leading)
57+
.truncationMode(.tail)
58+
}
3959
}

apps/macos/Sources/OpenClaw/DashboardWindowController.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ import Foundation
33
import WebKit
44

55
private final class DashboardWindowContentView: NSView {
6-
override var mouseDownCanMoveWindow: Bool { true }
6+
override var mouseDownCanMoveWindow: Bool {
7+
true
8+
}
79
}
810

911
private final class DashboardWindowDragRegionView: NSView {
10-
override var mouseDownCanMoveWindow: Bool { true }
12+
override var mouseDownCanMoveWindow: Bool {
13+
true
14+
}
1115

1216
override func mouseDown(with event: NSEvent) {
1317
self.window?.performDrag(with: event)
@@ -275,7 +279,7 @@ final class DashboardWindowController: NSWindowController, WKNavigationDelegate,
275279

276280
private func showLoadFailure(_ error: Error) {
277281
let nsError = error as NSError
278-
if nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled { return }
282+
if nsError.domain == NSURLErrorDomain, nsError.code == NSURLErrorCancelled { return }
279283
dashboardWindowLogger.error(
280284
"dashboard load failed url=\(self.currentURL.absoluteString, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
281285
let html = Self.failureHTML(url: self.currentURL, message: error.localizedDescription)
@@ -324,8 +328,8 @@ final class DashboardWindowController: NSWindowController, WKNavigationDelegate,
324328
<body>
325329
<main>
326330
<h1>Dashboard unavailable</h1>
327-
<p>\(Self.htmlEscape(message))</p>
328-
<code>\(Self.htmlEscape(url.absoluteString))</code>
331+
<p>\(self.htmlEscape(message))</p>
332+
<code>\(self.htmlEscape(url.absoluteString))</code>
329333
</main>
330334
</body>
331335
</html>

apps/macos/Sources/OpenClaw/DevicePairingApprovalPrompter.swift

Lines changed: 163 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ final class DevicePairingApprovalPrompter {
3333
let remoteIp: String?
3434
}
3535

36-
private struct PendingRequest: Codable, Equatable, Identifiable {
36+
struct PendingRequest: Codable, Equatable, Identifiable {
3737
let requestId: String
3838
let deviceId: String
3939
let publicKey: String
@@ -115,14 +115,16 @@ final class DevicePairingApprovalPrompter {
115115
PairingAlertSupport.presentPairingAlert(
116116
request: req,
117117
requestId: req.requestId,
118-
messageText: "Allow device to connect?",
119-
informativeText: Self.describe(req),
118+
messageText: Self.alertTitle(for: req),
119+
informativeText: Self.alertSummary(for: req),
120+
buttonTitles: PairingAlertSupport.ButtonTitles(approve: Self.approveButtonTitle(for: req)),
121+
accessoryView: Self.buildAccessoryView(for: req),
120122
state: self.alertState,
121123
onResponse: self.handleAlertResponse)
122124
}
123125

124126
private func handleAlertResponse(_ response: NSApplication.ModalResponse, request: PendingRequest) async {
125-
var shouldRemove = response != .alertFirstButtonReturn
127+
var shouldRemove = response != .alertSecondButtonReturn
126128
defer {
127129
if shouldRemove {
128130
if self.queue.first == request {
@@ -144,14 +146,14 @@ final class DevicePairingApprovalPrompter {
144146

145147
switch response {
146148
case .alertFirstButtonReturn:
149+
_ = await self.approve(requestId: request.requestId)
150+
case .alertSecondButtonReturn:
147151
shouldRemove = false
148152
if let idx = self.queue.firstIndex(of: request) {
149153
self.queue.remove(at: idx)
150154
}
151155
self.queue.append(request)
152156
return
153-
case .alertSecondButtonReturn:
154-
_ = await self.approve(requestId: request.requestId)
155157
case .alertThirdButtonReturn:
156158
await self.reject(requestId: request.requestId)
157159
default:
@@ -233,24 +235,166 @@ final class DevicePairingApprovalPrompter {
233235
self.updatePendingCounts()
234236
}
235237

236-
private static func describe(_ req: PendingRequest) -> String {
237-
var lines: [String] = []
238-
lines.append("Device: \(req.displayName ?? req.deviceId)")
239-
if let platform = req.platform {
240-
lines.append("Platform: \(platform)")
238+
static func alertTitle(for req: PendingRequest) -> String {
239+
self.isMac(req.platform) ? "New Mac wants to connect" : "New device wants to connect"
240+
}
241+
242+
static func alertSummary(for req: PendingRequest) -> String {
243+
let subject = self.isMac(req.platform) ? "this Mac app" : "this device"
244+
return "Approve \(subject) to control OpenClaw. Only approve if this is yours; you can remove it later in Settings."
245+
}
246+
247+
static func approveButtonTitle(for req: PendingRequest) -> String {
248+
self.isMac(req.platform) ? "Approve Mac" : "Approve Device"
249+
}
250+
251+
static func buildAccessoryView(for req: PendingRequest) -> NSView {
252+
let stack = NSStackView()
253+
stack.orientation = .vertical
254+
stack.alignment = .leading
255+
stack.spacing = 8
256+
stack.edgeInsets = NSEdgeInsets(top: 2, left: 0, bottom: 0, right: 0)
257+
258+
stack.addArrangedSubview(self.makeValueRow(label: "Device", value: self.deviceName(for: req)))
259+
if let platform = self.prettyPlatform(req.platform) {
260+
stack.addArrangedSubview(self.makeValueRow(label: "Platform", value: platform))
241261
}
242-
if let role = req.role {
243-
lines.append("Role: \(role)")
262+
if let role = self.prettyRole(req.role) {
263+
stack.addArrangedSubview(self.makeValueRow(label: "Role", value: role))
244264
}
245-
if let scopes = req.scopes, !scopes.isEmpty {
246-
lines.append("Scopes: \(scopes.joined(separator: ", "))")
265+
let accessItems = self.friendlyScopeNames(req.scopes)
266+
if !accessItems.isEmpty {
267+
stack.addArrangedSubview(self.makeSectionLabel("Access requested"))
268+
for item in accessItems {
269+
stack.addArrangedSubview(self.makeBullet(item))
270+
}
247271
}
248-
if let remoteIp = req.remoteIp {
249-
lines.append("IP: \(remoteIp)")
272+
stack.addArrangedSubview(self.makeDetailLine(req))
273+
274+
let fitting = stack.fittingSize
275+
stack.frame = NSRect(x: 0, y: 0, width: 420, height: fitting.height)
276+
return stack
277+
}
278+
279+
static func deviceName(for req: PendingRequest) -> String {
280+
let trimmedName = req.displayName?.trimmingCharacters(in: .whitespacesAndNewlines)
281+
if let trimmedName, !trimmedName.isEmpty, trimmedName != req.deviceId {
282+
return trimmedName
283+
}
284+
return self.isMac(req.platform) ? "OpenClaw Mac app" : "New device"
285+
}
286+
287+
static func prettyPlatform(_ raw: String?) -> String? {
288+
let platform = raw?.trimmingCharacters(in: .whitespacesAndNewlines)
289+
guard let platform, !platform.isEmpty else { return nil }
290+
switch platform.lowercased() {
291+
case "macintel", "x86_64-apple-darwin":
292+
return "Mac (Intel)"
293+
case "macarm", "macarm64", "arm64-apple-darwin", "aarch64-apple-darwin":
294+
return "Mac (Apple silicon)"
295+
case "darwin":
296+
return "Mac"
297+
default:
298+
if platform.lowercased().contains("mac") {
299+
return "Mac"
300+
}
301+
return platform
302+
}
303+
}
304+
305+
static func prettyRole(_ raw: String?) -> String? {
306+
let role = raw?.trimmingCharacters(in: .whitespacesAndNewlines)
307+
guard let role, !role.isEmpty else { return nil }
308+
return role == "operator" ? "Operator" : role
309+
}
310+
311+
static func friendlyScopeNames(_ scopes: [String]?) -> [String] {
312+
guard let scopes else { return [] }
313+
var seen = Set<String>()
314+
return scopes.compactMap { scope in
315+
let normalized = scope.trimmingCharacters(in: .whitespacesAndNewlines)
316+
guard !normalized.isEmpty, seen.insert(normalized).inserted else { return nil }
317+
switch normalized {
318+
case "operator.admin":
319+
return "Admin access"
320+
case "operator.read":
321+
return "Read OpenClaw data"
322+
case "operator.write":
323+
return "Send messages and make changes"
324+
case "operator.approvals":
325+
return "Manage approvals"
326+
case "operator.pairing":
327+
return "Pair and repair devices"
328+
case "operator.talk.secrets":
329+
return "Use Talk credentials"
330+
default:
331+
return normalized
332+
}
333+
}
334+
}
335+
336+
static func shortIdentifier(_ id: String) -> String {
337+
let trimmed = id.trimmingCharacters(in: .whitespacesAndNewlines)
338+
guard trimmed.count > 20 else { return trimmed }
339+
return "\(trimmed.prefix(8))...\(trimmed.suffix(7))"
340+
}
341+
342+
private static func isMac(_ platform: String?) -> Bool {
343+
guard let platform else { return false }
344+
let lower = platform.lowercased()
345+
return lower.contains("mac") || lower.contains("darwin")
346+
}
347+
348+
private static func makeValueRow(label: String, value: String) -> NSView {
349+
let row = NSStackView()
350+
row.orientation = .horizontal
351+
row.alignment = .firstBaseline
352+
row.spacing = 8
353+
354+
let labelField = self.makeLabel("\(label):", font: .systemFont(ofSize: 12, weight: .semibold))
355+
labelField.textColor = .secondaryLabelColor
356+
labelField.setContentHuggingPriority(.required, for: .horizontal)
357+
let valueField = self.makeLabel(value, font: .systemFont(ofSize: 12, weight: .regular))
358+
valueField.maximumNumberOfLines = 2
359+
360+
row.addArrangedSubview(labelField)
361+
row.addArrangedSubview(valueField)
362+
return row
363+
}
364+
365+
private static func makeSectionLabel(_ text: String) -> NSTextField {
366+
let label = self.makeLabel(text, font: .systemFont(ofSize: 12, weight: .semibold))
367+
label.textColor = .secondaryLabelColor
368+
return label
369+
}
370+
371+
private static func makeBullet(_ text: String) -> NSTextField {
372+
let label = self.makeLabel("\(text)", font: .systemFont(ofSize: 12, weight: .regular))
373+
label.maximumNumberOfLines = 2
374+
return label
375+
}
376+
377+
private static func makeDetailLine(_ req: PendingRequest) -> NSTextField {
378+
var parts = ["ID \(self.shortIdentifier(req.deviceId))"]
379+
if let remoteIp = req.remoteIp?.trimmingCharacters(in: .whitespacesAndNewlines), !remoteIp.isEmpty {
380+
parts.append("IP \(remoteIp.replacingOccurrences(of: "::ffff:", with: ""))")
250381
}
251382
if req.isRepair == true {
252-
lines.append("Repair: yes")
383+
parts.append("repair request")
253384
}
254-
return lines.joined(separator: "\n")
385+
let label = self.makeLabel(
386+
parts.joined(separator: " · "),
387+
font: .monospacedSystemFont(ofSize: 11, weight: .regular))
388+
label.textColor = .tertiaryLabelColor
389+
label.maximumNumberOfLines = 2
390+
return label
391+
}
392+
393+
private static func makeLabel(_ text: String, font: NSFont) -> NSTextField {
394+
let label = NSTextField(labelWithString: text)
395+
label.font = font
396+
label.lineBreakMode = .byWordWrapping
397+
label.textColor = .labelColor
398+
return label
255399
}
256400
}

apps/macos/Sources/OpenClaw/MenuHeaderCard.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ struct MenuHeaderCard<Content: View>: View {
3737
Text(statusText)
3838
.font(.caption)
3939
.foregroundStyle(.secondary)
40-
.lineLimit(1)
40+
.multilineTextAlignment(.leading)
41+
.lineLimit(3)
4142
.truncationMode(.tail)
43+
.fixedSize(horizontal: false, vertical: true)
4244
}
4345
self.content
4446
}

0 commit comments

Comments
 (0)