惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
博客园_首页
雷峰网
雷峰网
Hugging Face - Blog
Hugging Face - Blog
V
Visual Studio Blog
The Cloudflare Blog
罗磊的独立博客
美团技术团队
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
大猫的无限游戏
大猫的无限游戏
阮一峰的网络日志
阮一峰的网络日志
V
V2EX
博客园 - 叶小钗
The GitHub Blog
The GitHub Blog
Last Week in AI
Last Week in AI
J
Java Code Geeks
MyScale Blog
MyScale Blog
G
Google Developers Blog
U
Unit 42
Y
Y Combinator Blog
P
Proofpoint News Feed
Vercel News
Vercel News

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
refactor(macos): move sessions into context submenu (#754...
ngutman · 2026-05-01 · via Recent Commits to openclaw:main

@@ -176,99 +176,31 @@ extension MenuSessionsInjector {

176176

let channelState = ControlChannel.shared.state

177177178178

var cursor = insertIndex

179-

var headerView: NSView?

180-181-

if let snapshot = self.cachedSnapshot {

182-

let now = Date()

183-

let mainKey = self.mainSessionKey

184-

let rows = snapshot.rows.filter { row in

185-

if row.key == "main", mainKey != "main" { return false }

186-

if row.key == mainKey { return true }

187-

guard let updatedAt = row.updatedAt else { return false }

188-

return now.timeIntervalSince(updatedAt) <= self.activeWindowSeconds

189-

}.sorted { lhs, rhs in

190-

if lhs.key == mainKey { return true }

191-

if rhs.key == mainKey { return false }

192-

return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast)

193-

}

194-

if !rows.isEmpty {

195-

let previewKeys = rows.prefix(20).map(\.key)

196-

let task = Task {

197-

await SessionMenuPreviewLoader.prewarm(sessionKeys: previewKeys, maxItems: 10)

198-

}

199-

self.previewTasks.append(task)

200-

}

201-202-

let headerItem = NSMenuItem()

203-

headerItem.tag = self.tag

204-

headerItem.isEnabled = false

205-

let statusText = self

206-

.cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState))

207-

let hosted = self.makeHostedView(

208-

rootView: AnyView(MenuSessionsHeaderView(

209-

count: rows.count,

210-

statusText: statusText)),

211-

width: width,

212-

highlighted: false)

213-

headerItem.view = hosted

214-

headerView = hosted

215-

menu.insertItem(headerItem, at: cursor)

216-

cursor += 1

217-218-

if rows.isEmpty {

219-

menu.insertItem(

220-

self.makeMessageItem(text: "No active sessions", symbolName: "minus", width: width),

221-

at: cursor)

222-

cursor += 1

223-

} else {

224-

for row in rows {

225-

let item = NSMenuItem()

226-

item.tag = self.tag

227-

item.isEnabled = true

228-

item.submenu = self.buildSubmenu(for: row, storePath: snapshot.storePath)

229-

item.view = self.makeHostedView(

230-

rootView: AnyView(SessionMenuLabelView(row: row, width: width)),

231-

width: width,

232-

highlighted: true)

233-

menu.insertItem(item, at: cursor)

234-

cursor += 1

235-

}

236-

}

237-

} else {

238-

let headerItem = NSMenuItem()

239-

headerItem.tag = self.tag

240-

headerItem.isEnabled = false

241-

let statusText = isConnected

242-

? (self.cachedErrorText ?? "Loading sessions…")

243-

: self.controlChannelStatusText(for: channelState)

244-

let hosted = self.makeHostedView(

245-

rootView: AnyView(MenuSessionsHeaderView(

246-

count: 0,

247-

statusText: statusText)),

248-

width: width,

249-

highlighted: false)

250-

headerItem.view = hosted

251-

headerView = hosted

252-

menu.insertItem(headerItem, at: cursor)

253-

cursor += 1

254-255-

if !isConnected {

256-

menu.insertItem(

257-

self.makeMessageItem(

258-

text: "Connect the gateway to see sessions",

259-

symbolName: "bolt.slash",

260-

width: width),

261-

at: cursor)

262-

cursor += 1

263-

}

264-

}

179+

let item = NSMenuItem(title: "Context", action: nil, keyEquivalent: "")

180+

item.tag = self.tag

181+

item.isEnabled = true

182+

item.submenu = self.buildContextSubmenu(

183+

width: width,

184+

isConnected: isConnected,

185+

channelState: channelState)

186+

let hosted = self.makeHostedView(

187+

rootView: AnyView(ContextRootMenuLabelView(

188+

subtitle: self.contextRootSubtitle(

189+

isConnected: isConnected,

190+

channelState: channelState),

191+

width: width)),

192+

width: width,

193+

highlighted: true)

194+

item.view = hosted

195+

menu.insertItem(item, at: cursor)

196+

cursor += 1

265197266198

cursor = self.insertUsageSection(into: menu, at: cursor, width: width)

267199

cursor = self.insertCostUsageSection(into: menu, at: cursor, width: width)

268200269-

DispatchQueue.main.async { [weak self, weak headerView] in

270-

guard let self, let headerView else { return }

271-

self.captureMenuWidthIfAvailable(from: headerView)

201+

DispatchQueue.main.async { [weak self, weak hosted] in

202+

guard let self, let hosted else { return }

203+

self.captureMenuWidthIfAvailable(from: hosted)

272204

}

273205

}

274206

@@ -346,6 +278,124 @@ extension MenuSessionsInjector {

346278

_ = cursor

347279

}

348280281+

private func buildContextSubmenu(

282+

width: CGFloat,

283+

isConnected: Bool,

284+

channelState: ControlChannel.ConnectionState) -> NSMenu

285+

{

286+

let menu = NSMenu()

287+

let width = max(300, width)

288+

var cursor = 0

289+290+

if let snapshot = self.cachedSnapshot {

291+

let rows = self.activeRows(from: snapshot)

292+

if !rows.isEmpty {

293+

let previewKeys = rows.prefix(20).map(\.key)

294+

let task = Task {

295+

await SessionMenuPreviewLoader.prewarm(sessionKeys: previewKeys, maxItems: 10)

296+

}

297+

self.previewTasks.append(task)

298+

}

299+300+

let headerItem = NSMenuItem()

301+

headerItem.tag = self.tag

302+

headerItem.isEnabled = false

303+

let statusText = self.cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState))

304+

headerItem.view = self.makeHostedView(

305+

rootView: AnyView(MenuSessionsHeaderView(

306+

count: rows.count,

307+

statusText: statusText)),

308+

width: width,

309+

highlighted: false)

310+

menu.insertItem(headerItem, at: cursor)

311+

cursor += 1

312+313+

if rows.isEmpty {

314+

menu.insertItem(

315+

self.makeMessageItem(text: "No active sessions", symbolName: "minus", width: width),

316+

at: cursor)

317+

cursor += 1

318+

} else {

319+

for row in rows {

320+

let item = NSMenuItem()

321+

item.tag = self.tag

322+

item.isEnabled = true

323+

item.representedObject = row.key

324+

item.submenu = self.buildSubmenu(for: row, storePath: snapshot.storePath)

325+

item.view = self.makeHostedView(

326+

rootView: AnyView(SessionMenuLabelView(row: row, width: width)),

327+

width: width,

328+

highlighted: true)

329+

menu.insertItem(item, at: cursor)

330+

cursor += 1

331+

}

332+

}

333+

} else {

334+

let headerItem = NSMenuItem()

335+

headerItem.tag = self.tag

336+

headerItem.isEnabled = false

337+

let statusText = isConnected

338+

? (self.cachedErrorText ?? "Loading sessions…")

339+

: self.controlChannelStatusText(for: channelState)

340+

headerItem.view = self.makeHostedView(

341+

rootView: AnyView(MenuSessionsHeaderView(

342+

count: 0,

343+

statusText: statusText)),

344+

width: width,

345+

highlighted: false)

346+

menu.insertItem(headerItem, at: cursor)

347+

cursor += 1

348+349+

if !isConnected {

350+

menu.insertItem(

351+

self.makeMessageItem(

352+

text: "Connect the gateway to see sessions",

353+

symbolName: "bolt.slash",

354+

width: width),

355+

at: cursor)

356+

cursor += 1

357+

}

358+

}

359+360+

_ = cursor

361+

return menu

362+

}

363+364+

private func contextRootSubtitle(

365+

isConnected: Bool,

366+

channelState: ControlChannel.ConnectionState) -> String

367+

{

368+

if let snapshot = self.cachedSnapshot {

369+

return self.sessionsSubtitle(count: self.activeRows(from: snapshot).count)

370+

}

371+372+

if isConnected {

373+

return self.cachedErrorText ?? "Loading…"

374+

}

375+376+

return self.controlChannelStatusText(for: channelState)

377+

}

378+379+

private func activeRows(from snapshot: SessionStoreSnapshot) -> [SessionRow] {

380+

let now = Date()

381+

let mainKey = self.mainSessionKey

382+

return snapshot.rows.filter { row in

383+

if row.key == "main", mainKey != "main" { return false }

384+

if row.key == mainKey { return true }

385+

guard let updatedAt = row.updatedAt else { return false }

386+

return now.timeIntervalSince(updatedAt) <= self.activeWindowSeconds

387+

}.sorted { lhs, rhs in

388+

if lhs.key == mainKey { return true }

389+

if rhs.key == mainKey { return false }

390+

return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast)

391+

}

392+

}

393+394+

private func sessionsSubtitle(count: Int) -> String {

395+

if count == 1 { return "1 session · 24h" }

396+

return "\(count) sessions · 24h"

397+

}

398+349399

private func insertUsageSection(into menu: NSMenu, at cursor: Int, width: CGFloat) -> Int {

350400

let rows = self.usageRows

351401

if rows.isEmpty {