## Agent 核心功能 - **流式输出刘海弹窗**:实时展示模型思考内容,支持自动滚动 - **工具调用进度**:最多可见3条 + 可滚动查看全部历史 + 防抖并行滚动 - **打字/语音双输入**:Agent 模式可选手动打字或语音输入 - **快速/思考模式切换**:支持 thinking mode 开关 - **多轮对话连续性**:同一会话内持续对话,新建对话按钮 ⊕ 开启新会话 - **工作时间显示**:工作完成状态栏显示耗时,如「工作完成 · 00:42」 ## Bug 修复 - **SIGTERM 优雅退出**:bridge.js 处理 SIGTERM,避免 code 15 误报异常 - **弹窗复用不重建**:输入内容继续对话时弹窗内部刷新,不再闪出闪进 - **关闭按钮立刻生效**:ignoreMouse=true 绕过 DynamicNotchKit 延迟 - **设置窗口 Agent UI 构建时机**:从延迟构建改为 init 中直接构建 - **模型编辑器**:修复 grid column nil 崩溃 + 改为独立 KeyEquivalentPanel 窗口支持 Cmd+A/C/V/X - **双击 Control 三向轮换**:语音输入 ↔ AI助手 ↔ 智能体 - **Fn 长按冲突处理**:非 Agent 模式下自动关闭 Agent 刘海弹窗 - **Save 后刷新下拉**:保存配置后默认模型下拉框即时同步 ## UI 交互优化 - **工具列表无灰色背景**:工具执行期间不再有底色变化 - **工作时隐藏新建对话按钮**:thinking/running 期间 ⊕ 自动隐藏 - **工具调用去 timeout 显示**:运行指令行不再显示 (timeout=30s) ## 文件拆分(单文件≤500行) - AgentNotchView → AgentProgressTracker + AppKitTextField + AgentNotchView - SettingsWindow → AgentModelEditor + KeyEquivalentSecureTextField + KeyEquivalentPanel + SettingsWindow ## 底层改动 - DynamicNotchKit: 新增 contentVersion + notifyContentChanged 机制 - NotchView: 动画绑定 contentVersion 支持弹窗内容变化弹性动画
194 lines
7.6 KiB
Swift
194 lines
7.6 KiB
Swift
import Cocoa
|
||
|
||
// MARK: - Agent 模型编辑弹窗
|
||
|
||
/// 显示添加/编辑模型的独立窗口,通过回调返回结果
|
||
final class AgentModelEditor {
|
||
static func show(for model: AgentModelConfig?, onSave: @escaping (AgentModelConfig) -> Void) {
|
||
let isEdit = model != nil
|
||
let panel = KeyEquivalentPanel(
|
||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 340),
|
||
styleMask: [.titled, .closable],
|
||
backing: .buffered,
|
||
defer: false
|
||
)
|
||
panel.title = isEdit ? "编辑模型" : "添加模型"
|
||
panel.isReleasedWhenClosed = false
|
||
panel.center()
|
||
|
||
guard let content = panel.contentView else { return }
|
||
|
||
let nameField = KeyEquivalentTextField(frame: .zero)
|
||
nameField.placeholderString = "kimi-k2.5"
|
||
nameField.stringValue = model?.name ?? ""
|
||
|
||
let urlField = KeyEquivalentTextField(frame: .zero)
|
||
urlField.placeholderString = "https://api.example.com/v1"
|
||
urlField.stringValue = model?.url ?? ""
|
||
|
||
let keyField = KeyEquivalentSecureTextField(frame: .zero)
|
||
keyField.placeholderString = "sk-..."
|
||
keyField.stringValue = model?.apikey ?? ""
|
||
|
||
let modesField = KeyEquivalentTextField(frame: .zero)
|
||
modesField.placeholderString = "快速,思考"
|
||
modesField.stringValue = model?.modes ?? "快速,思考"
|
||
|
||
let multimodalField = KeyEquivalentTextField(frame: .zero)
|
||
multimodalField.placeholderString = "图片,视频(可留空)"
|
||
multimodalField.stringValue = model?.multimodal ?? ""
|
||
|
||
let maxOutputField = KeyEquivalentTextField(frame: .zero)
|
||
maxOutputField.placeholderString = "32000"
|
||
maxOutputField.stringValue = model.map { String($0.maxOutput) } ?? "32000"
|
||
|
||
let maxContextField = KeyEquivalentTextField(frame: .zero)
|
||
maxContextField.placeholderString = "256000"
|
||
maxContextField.stringValue = model.map { String($0.maxContext) } ?? "256000"
|
||
|
||
let fields: [(String, NSTextField)] = [
|
||
("模型名称:", nameField),
|
||
("API URL:", urlField),
|
||
("API Key:", keyField),
|
||
("模式:", modesField),
|
||
("多模态:", multimodalField),
|
||
("最大输出:", maxOutputField),
|
||
("最大上下文:", maxContextField),
|
||
]
|
||
|
||
let grid = NSGridView(frame: .zero)
|
||
grid.translatesAutoresizingMaskIntoConstraints = false
|
||
grid.columnSpacing = 8
|
||
grid.rowSpacing = 10
|
||
|
||
for (labelText, field) in fields {
|
||
let label = makeLabel(labelText)
|
||
field.font = .systemFont(ofSize: 13)
|
||
field.bezelStyle = .roundedBezel
|
||
field.translatesAutoresizingMaskIntoConstraints = false
|
||
grid.addRow(with: [label, field])
|
||
}
|
||
|
||
grid.column(at: 0).xPlacement = .trailing
|
||
grid.column(at: 1).width = 260
|
||
|
||
let saveBtn = NSButton(title: isEdit ? "保存" : "添加", target: nil, action: nil)
|
||
saveBtn.bezelStyle = .rounded
|
||
saveBtn.translatesAutoresizingMaskIntoConstraints = false
|
||
saveBtn.keyEquivalent = "\r"
|
||
|
||
let cancelBtn = NSButton(title: "取消", target: nil, action: nil)
|
||
cancelBtn.bezelStyle = .rounded
|
||
cancelBtn.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
let buttonStack = NSStackView(views: [cancelBtn, saveBtn])
|
||
buttonStack.translatesAutoresizingMaskIntoConstraints = false
|
||
buttonStack.orientation = .horizontal
|
||
buttonStack.spacing = 12
|
||
buttonStack.distribution = .fillEqually
|
||
|
||
content.addSubview(grid)
|
||
content.addSubview(buttonStack)
|
||
|
||
NSLayoutConstraint.activate([
|
||
grid.topAnchor.constraint(equalTo: content.topAnchor, constant: 20),
|
||
grid.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
||
grid.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
|
||
buttonStack.topAnchor.constraint(equalTo: grid.bottomAnchor, constant: 16),
|
||
buttonStack.centerXAnchor.constraint(equalTo: content.centerXAnchor),
|
||
buttonStack.widthAnchor.constraint(equalToConstant: 200)
|
||
])
|
||
|
||
let helper = ModelEditorHelper(
|
||
panel: panel,
|
||
nameField: nameField, urlField: urlField, keyField: keyField,
|
||
modesField: modesField, multimodalField: multimodalField,
|
||
maxOutputField: maxOutputField, maxContextField: maxContextField,
|
||
isEdit: isEdit, onSave: onSave
|
||
)
|
||
|
||
saveBtn.target = helper
|
||
saveBtn.action = #selector(ModelEditorHelper.saveAction(_:))
|
||
cancelBtn.target = helper
|
||
cancelBtn.action = #selector(ModelEditorHelper.cancelAction(_:))
|
||
|
||
objc_setAssociatedObject(saveBtn, "helper", helper, .OBJC_ASSOCIATION_RETAIN)
|
||
objc_setAssociatedObject(cancelBtn, "helper", helper, .OBJC_ASSOCIATION_RETAIN)
|
||
|
||
panel.makeKeyAndOrderFront(nil)
|
||
}
|
||
|
||
private static func makeLabel(_ text: String) -> NSTextField {
|
||
let label = NSTextField(labelWithString: text)
|
||
label.font = .systemFont(ofSize: 11, weight: .semibold)
|
||
label.textColor = .secondaryLabelColor
|
||
label.alignment = .right
|
||
label.translatesAutoresizingMaskIntoConstraints = false
|
||
return label
|
||
}
|
||
}
|
||
|
||
// MARK: - Helper
|
||
|
||
private class ModelEditorHelper: NSObject {
|
||
weak var panel: KeyEquivalentPanel?
|
||
let nameField: NSTextField
|
||
let urlField: NSTextField
|
||
let keyField: KeyEquivalentSecureTextField
|
||
let modesField: NSTextField
|
||
let multimodalField: NSTextField
|
||
let maxOutputField: NSTextField
|
||
let maxContextField: NSTextField
|
||
let isEdit: Bool
|
||
let onSave: (AgentModelConfig) -> Void
|
||
|
||
init(panel: KeyEquivalentPanel,
|
||
nameField: NSTextField, urlField: NSTextField, keyField: KeyEquivalentSecureTextField,
|
||
modesField: NSTextField, multimodalField: NSTextField,
|
||
maxOutputField: NSTextField, maxContextField: NSTextField,
|
||
isEdit: Bool, onSave: @escaping (AgentModelConfig) -> Void) {
|
||
self.panel = panel
|
||
self.nameField = nameField
|
||
self.urlField = urlField
|
||
self.keyField = keyField
|
||
self.modesField = modesField
|
||
self.multimodalField = multimodalField
|
||
self.maxOutputField = maxOutputField
|
||
self.maxContextField = maxContextField
|
||
self.isEdit = isEdit
|
||
self.onSave = onSave
|
||
}
|
||
|
||
@objc func saveAction(_ sender: Any) {
|
||
let name = nameField.stringValue.trimmingCharacters(in: .whitespaces)
|
||
let url = urlField.stringValue.trimmingCharacters(in: .whitespaces)
|
||
let key = keyField.stringValue.trimmingCharacters(in: .whitespaces)
|
||
let modes = modesField.stringValue.trimmingCharacters(in: .whitespaces)
|
||
let multimodal = multimodalField.stringValue.trimmingCharacters(in: .whitespaces)
|
||
let maxOutput = Int(maxOutputField.stringValue) ?? 32000
|
||
let maxContext = Int(maxContextField.stringValue) ?? 256000
|
||
|
||
guard !name.isEmpty, !url.isEmpty, !key.isEmpty, !modes.isEmpty else {
|
||
let alert = NSAlert()
|
||
alert.messageText = "请填写模型名称、API URL、API Key 和模式"
|
||
alert.alertStyle = .warning
|
||
alert.addButton(withTitle: "确定")
|
||
alert.runModal()
|
||
return
|
||
}
|
||
|
||
let model = AgentModelConfig(
|
||
url: url, name: name, apikey: key,
|
||
modes: modes, multimodal: multimodal,
|
||
maxOutput: maxOutput, maxContext: maxContext
|
||
)
|
||
|
||
panel?.close()
|
||
onSave(model)
|
||
}
|
||
|
||
@objc func cancelAction(_ sender: Any) {
|
||
panel?.close()
|
||
}
|
||
}
|