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

推荐订阅源

V
Visual Studio Blog
爱范儿
爱范儿
GbyAI
GbyAI
博客园 - 叶小钗
Last Week in AI
Last Week in AI
Jina AI
Jina AI
Microsoft Security Blog
Microsoft Security Blog
云风的 BLOG
云风的 BLOG
C
Check Point Blog
H
Help Net Security
P
Proofpoint News Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
大猫的无限游戏
大猫的无限游戏
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog RSS Feed
Y
Y Combinator Blog
U
Unit 42
T
Tailwind CSS Blog
MyScale Blog
MyScale Blog
N
Netflix TechBlog - Medium
S
SegmentFault 最新的问题
J
Java Code Geeks
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

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
style: refine mac voice settings layout · openclaw/opencl...
steipete · 2026-05-19 · via Recent Commits to openclaw:main

@@ -251,67 +251,67 @@ struct VoiceWakeSettings: View {

251251252252

private var triggerTable: some View {

253253

SettingsCardGroup("Trigger Words") {

254-

HStack {

255-

Text("Wake phrases")

256-

.font(.callout.weight(.semibold))

257-

Spacer()

258-

Button {

259-

self.addWord()

260-

} label: {

261-

Label("Add word", systemImage: "plus")

262-

}

263-

.disabled(self.triggerEntries

264-

.contains(where: { $0.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }))

254+

SettingsCardRow(

255+

title: "Wake phrases",

256+

subtitle: "Short phrases that start voice wake detection.")

257+

{

258+

HStack(spacing: 8) {

259+

Button {

260+

self.addWord()

261+

} label: {

262+

Label("Add word", systemImage: "plus")

263+

}

264+

.disabled(self.triggerEntries

265+

.contains(where: { $0.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }))

265266266-

Button("Reset defaults") {

267-

self.triggerEntries = defaultVoiceWakeTriggers.map { TriggerEntry(id: UUID(), value: $0) }

268-

self.syncTriggerEntriesToState()

267+

Button("Reset") {

268+

self.triggerEntries = defaultVoiceWakeTriggers.map { TriggerEntry(id: UUID(), value: $0) }

269+

self.syncTriggerEntriesToState()

270+

}

269271

}

272+

.buttonStyle(.bordered)

270273

}

271-

.padding(.horizontal, 14)

272-

.padding(.top, 12)

273-274-

VStack(spacing: 0) {

275-

ForEach(self.$triggerEntries) { $entry in

276-

HStack(spacing: 8) {

277-

TextField("Wake word", text: $entry.value)

278-

.textFieldStyle(.roundedBorder)

279-

.onSubmit {

280-

self.syncTriggerEntriesToState()

281-

}

282274283-

Button {

284-

self.removeWord(id: entry.id)

285-

} label: {

286-

Image(systemName: "trash")

287-

}

288-

.buttonStyle(.borderless)

289-

.help("Remove trigger word")

290-

.frame(width: 24)

291-

}

292-

.padding(8)

275+

self.triggerPhraseRows

293276294-

if entry.id != self.triggerEntries.last?.id {

295-

Divider()

277+

TriggerPhraseHelpRow()

278+

}

279+

}

280+281+

private var triggerPhraseRows: some View {

282+

Group {

283+

if self.triggerEntries.isEmpty {

284+

HStack(spacing: 10) {

285+

Image(systemName: "text.badge.plus")

286+

.font(.callout)

287+

.foregroundStyle(.secondary)

288+

.frame(width: 22)

289+

Text("No wake phrases configured")

290+

.font(.callout.weight(.medium))

291+

.foregroundStyle(.secondary)

292+

Spacer()

293+

}

294+

.padding(.horizontal, 14)

295+

.padding(.vertical, 14)

296+

} else {

297+

VStack(spacing: 0) {

298+

ForEach(self.$triggerEntries) { $entry in

299+

TriggerPhraseRow(

300+

value: $entry.value,

301+

showsDivider: entry.id != self.triggerEntries.last?.id,

302+

onSubmit: {

303+

self.syncTriggerEntriesToState()

304+

},

305+

onRemove: {

306+

self.removeWord(id: entry.id)

307+

})

296308

}

297309

}

298310

}

299-

.frame(maxWidth: .infinity, minHeight: 180, alignment: .topLeading)

300-

.background(Color(nsColor: .textBackgroundColor))

301-

.clipShape(RoundedRectangle(cornerRadius: 6))

302-

.overlay(

303-

RoundedRectangle(cornerRadius: 6)

304-

.stroke(Color.secondary.opacity(0.25), lineWidth: 1))

305-

.padding(.horizontal, 14)

306-307-

Text(

308-

"OpenClaw reacts when any trigger appears in a transcription. "

309-

+ "Keep them short to avoid false positives.")

310-

.font(.footnote)

311-

.foregroundStyle(.secondary)

312-

.fixedSize(horizontal: false, vertical: true)

313-

.padding(.horizontal, 14)

314-

.padding(.bottom, 12)

311+

}

312+

.overlay(alignment: .bottom) {

313+

Divider()

314+

.padding(.leading, 14)

315315

}

316316

}

317317

@@ -535,70 +535,82 @@ struct VoiceWakeSettings: View {

535535

.frame(width: self.controlWidth)

536536

}

537537538-

if !self.state.voiceWakeAdditionalLocaleIDs.isEmpty {

539-

VStack(alignment: .leading, spacing: 8) {

540-

Text("Additional languages")

541-

.font(.footnote.weight(.semibold))

542-

ForEach(

543-

Array(self.state.voiceWakeAdditionalLocaleIDs.enumerated()),

544-

id: \.offset)

545-

{ idx, localeID in

546-

HStack(spacing: 8) {

547-

Picker("Extra \(idx + 1)", selection: Binding(

548-

get: { localeID },

549-

set: { newValue in

550-

guard self.state

551-

.voiceWakeAdditionalLocaleIDs.indices

552-

.contains(idx) else { return }

553-

self.state

554-

.voiceWakeAdditionalLocaleIDs[idx] =

555-

newValue

556-

})) {

557-

ForEach(self.availableLocales.map(\.identifier), id: \.self) { id in

558-

Text(self.friendlyName(for: Locale(identifier: id))).tag(id)

559-

}

560-

}

561-

.labelsHidden()

562-

.frame(width: 220)

563-564-

Button {

565-

guard self.state.voiceWakeAdditionalLocaleIDs.indices.contains(idx) else { return }

566-

self.state.voiceWakeAdditionalLocaleIDs.remove(at: idx)

567-

} label: {

568-

Image(systemName: "trash")

569-

}

570-

.buttonStyle(.borderless)

571-

.help("Remove language")

572-

}

573-

}

574-538+

SettingsCardRow(

539+

title: "Additional languages",

540+

subtitle: self.additionalLanguagesSubtitle,

541+

showsDivider: !self.state.voiceWakeAdditionalLocaleIDs.isEmpty)

542+

{

543+

if self.state.voiceWakeAdditionalLocaleIDs.isEmpty {

575544

Button {

576-

if let first = availableLocales.first {

577-

self.state.voiceWakeAdditionalLocaleIDs.append(first.identifier)

578-

}

545+

self.addAdditionalLocale()

579546

} label: {

580-

Label("Add language", systemImage: "plus")

547+

Label("Add", systemImage: "plus")

581548

}

549+

.buttonStyle(.bordered)

582550

.disabled(self.availableLocales.isEmpty)

583551

}

584-

.padding(.horizontal, 14)

585-

.padding(.bottom, 10)

586-

} else {

552+

}

553+554+

if !self.state.voiceWakeAdditionalLocaleIDs.isEmpty {

555+

self.additionalLanguageRows

556+

}

557+

}

558+

}

559+560+

private var additionalLanguagesSubtitle: String {

561+

if self.state.voiceWakeAdditionalLocaleIDs.isEmpty {

562+

return "None configured."

563+

}

564+

return "Tried after the primary language."

565+

}

566+567+

private var additionalLanguageRows: some View {

568+

VStack(alignment: .leading, spacing: 0) {

569+

ForEach(Array(self.state.voiceWakeAdditionalLocaleIDs.enumerated()), id: \.offset) { idx, localeID in

570+

AdditionalLanguageRow(

571+

index: idx,

572+

selection: self.additionalLocaleBinding(index: idx, fallback: localeID),

573+

localeIDs: self.availableLocales.map(\.identifier),

574+

localeName: { id in self.friendlyName(for: Locale(identifier: id)) },

575+

showsDivider: true,

576+

onRemove: {

577+

guard self.state.voiceWakeAdditionalLocaleIDs.indices.contains(idx) else { return }

578+

self.state.voiceWakeAdditionalLocaleIDs.remove(at: idx)

579+

})

580+

}

581+582+

SettingsCardRow(title: "Add another language", showsDivider: false) {

587583

Button {

588-

if let first = availableLocales.first {

589-

self.state.voiceWakeAdditionalLocaleIDs.append(first.identifier)

590-

}

584+

self.addAdditionalLocale()

591585

} label: {

592-

Label("Add additional language", systemImage: "plus")

586+

Label("Add", systemImage: "plus")

593587

}

594-

.buttonStyle(.link)

588+

.buttonStyle(.bordered)

595589

.disabled(self.availableLocales.isEmpty)

596-

.padding(.horizontal, 14)

597-

.padding(.bottom, 10)

598590

}

599591

}

600592

}

601593594+

private func additionalLocaleBinding(index: Int, fallback: String) -> Binding<String> {

595+

Binding(

596+

get: {

597+

guard self.state.voiceWakeAdditionalLocaleIDs.indices.contains(index) else { return fallback }

598+

return self.state.voiceWakeAdditionalLocaleIDs[index]

599+

},

600+

set: { newValue in

601+

guard self.state.voiceWakeAdditionalLocaleIDs.indices.contains(index) else { return }

602+

self.state.voiceWakeAdditionalLocaleIDs[index] = newValue

603+

})

604+

}

605+606+

private func addAdditionalLocale() {

607+

let selected = Set([self.state.voiceWakeLocaleID] + self.state.voiceWakeAdditionalLocaleIDs)

608+

let next = self.availableLocales.first { !selected.contains($0.identifier) } ?? self.availableLocales.first

609+

if let next {

610+

self.state.voiceWakeAdditionalLocaleIDs.append(next.identifier)

611+

}

612+

}

613+602614

@MainActor

603615

private func loadMicsIfNeeded(force: Bool = false) async {

604616

guard force || self.availableMics.isEmpty, !self.loadingMics else { return }

@@ -716,6 +728,108 @@ struct VoiceWakeSettings: View {

716728

}

717729

}

718730731+

private struct TriggerPhraseRow: View {

732+

@Binding var value: String

733+

let showsDivider: Bool

734+

let onSubmit: () -> Void

735+

let onRemove: () -> Void

736+737+

var body: some View {

738+

HStack(spacing: 12) {

739+

Image(systemName: "quote.opening")

740+

.font(.callout.weight(.semibold))

741+

.foregroundStyle(.secondary)

742+

.frame(width: 24)

743+744+

TextField("Wake phrase", text: self.$value)

745+

.textFieldStyle(.roundedBorder)

746+

.font(.callout.weight(.medium))

747+

.frame(maxWidth: 420)

748+

.onSubmit(self.onSubmit)

749+750+

Spacer(minLength: 8)

751+752+

Button(action: self.onRemove) {

753+

Image(systemName: "trash")

754+

.font(.callout)

755+

.symbolRenderingMode(.hierarchical)

756+

}

757+

.buttonStyle(.plain)

758+

.foregroundStyle(.secondary)

759+

.frame(width: 26, height: 26)

760+

.contentShape(Rectangle())

761+

.help("Remove trigger word")

762+

}

763+

.padding(.horizontal, 14)

764+

.padding(.vertical, 10)

765+

.overlay(alignment: .bottom) {

766+

if self.showsDivider {

767+

Divider()

768+

.padding(.leading, 50)

769+

}

770+

}

771+

}

772+

}

773+774+

private struct AdditionalLanguageRow: View {

775+

let index: Int

776+

@Binding var selection: String

777+

let localeIDs: [String]

778+

let localeName: (String) -> String

779+

let showsDivider: Bool

780+

let onRemove: () -> Void

781+782+

var body: some View {

783+

SettingsCardRow(

784+

title: "Language \(self.index + 2)",

785+

subtitle: "Fallback recognition language.",

786+

showsDivider: self.showsDivider)

787+

{

788+

HStack(spacing: 10) {

789+

Picker("Language \(self.index + 2)", selection: self.$selection) {

790+

ForEach(self.localeIDs, id: \.self) { id in

791+

Text(self.localeName(id)).tag(id)

792+

}

793+

}

794+

.labelsHidden()

795+

.frame(width: 220)

796+797+

Button(action: self.onRemove) {

798+

Image(systemName: "trash")

799+

.font(.callout)

800+

.symbolRenderingMode(.hierarchical)

801+

}

802+

.buttonStyle(.plain)

803+

.foregroundStyle(.secondary)

804+

.frame(width: 26, height: 26)

805+

.contentShape(Rectangle())

806+

.help("Remove language")

807+

}

808+

}

809+

}

810+

}

811+812+

private struct TriggerPhraseHelpRow: View {

813+

var body: some View {

814+

HStack(alignment: .top, spacing: 10) {

815+

Image(systemName: "info.circle")

816+

.font(.footnote.weight(.semibold))

817+

.foregroundStyle(.secondary)

818+

.frame(width: 18)

819+

.padding(.top, 1)

820+821+

Text("OpenClaw reacts when any trigger appears in a transcription. Keep phrases short to avoid false positives.")

822+

.font(.footnote)

823+

.foregroundStyle(.secondary)

824+

.fixedSize(horizontal: false, vertical: true)

825+826+

Spacer(minLength: 0)

827+

}

828+

.padding(.horizontal, 14)

829+

.padding(.vertical, 11)

830+

}

831+

}

832+719833

#if DEBUG

720834

struct VoiceWakeSettings_Previews: PreviewProvider {

721835

static var previews: some View {