## 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 支持弹窗内容变化弹性动画
106 lines
3.1 KiB
Swift
106 lines
3.1 KiB
Swift
import SwiftUI
|
||
|
||
// MARK: - Agent 进度跟踪
|
||
|
||
final class AgentProgressTracker: ObservableObject {
|
||
struct ToolEntry: Identifiable {
|
||
let id = UUID()
|
||
let tool: String
|
||
let display: String
|
||
var resultLines: [String] = []
|
||
}
|
||
|
||
@Published var mode: String = "idle" // idle / thinking / running / done / error
|
||
@Published var elapsedSeconds: Int = 0
|
||
@Published var toolEntries: [ToolEntry] = []
|
||
@Published var finalContent: String = ""
|
||
@Published var errorMessage: String = ""
|
||
@Published var streamingContent: String = "" // 实时流式输出
|
||
@Published var isTextInputMode: Bool = false // 打字输入模式标记(外部控制)
|
||
|
||
private var timer: Timer?
|
||
private var startTime: Date = Date()
|
||
private var toolPhaseActive = false // 工具后首段内容应替换
|
||
|
||
func start() {
|
||
startTime = Date()
|
||
mode = "thinking"
|
||
toolEntries = []
|
||
finalContent = ""
|
||
errorMessage = ""
|
||
streamingContent = ""
|
||
toolPhaseActive = false
|
||
|
||
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||
guard let self = self, self.mode != "done", self.mode != "error" else { return }
|
||
self.elapsedSeconds = Int(Date().timeIntervalSince(self.startTime))
|
||
}
|
||
}
|
||
|
||
/// 打字模式:清空状态但不进入 thinking
|
||
func resetForInput() {
|
||
mode = "idle"
|
||
elapsedSeconds = 0
|
||
toolEntries = []
|
||
finalContent = ""
|
||
errorMessage = ""
|
||
streamingContent = ""
|
||
toolPhaseActive = false
|
||
timer?.invalidate()
|
||
timer = nil
|
||
}
|
||
|
||
func appendChunk(_ chunk: String) {
|
||
if toolPhaseActive {
|
||
streamingContent = chunk
|
||
toolPhaseActive = false
|
||
} else {
|
||
streamingContent += chunk
|
||
}
|
||
}
|
||
|
||
func addTool(_ tool: String, display: String) {
|
||
mode = "running"
|
||
toolPhaseActive = true // 标记:下一段内容应替换
|
||
toolEntries.append(ToolEntry(tool: tool, display: display))
|
||
}
|
||
|
||
func updateToolResult(_ tool: String, resultLines: [String]) {
|
||
if let idx = toolEntries.lastIndex(where: { $0.tool == tool && $0.resultLines.isEmpty }) {
|
||
toolEntries[idx].resultLines = resultLines
|
||
}
|
||
}
|
||
|
||
func finish(content: String) {
|
||
mode = "done"
|
||
finalContent = content
|
||
timer?.invalidate()
|
||
timer = nil
|
||
}
|
||
|
||
func fail(_ message: String) {
|
||
mode = "error"
|
||
errorMessage = message
|
||
timer?.invalidate()
|
||
timer = nil
|
||
}
|
||
|
||
var formattedTime: String {
|
||
let m = elapsedSeconds / 60
|
||
let s = elapsedSeconds % 60
|
||
return String(format: "%02d:%02d", m, s)
|
||
}
|
||
|
||
var summary: String {
|
||
if mode == "done" {
|
||
return "工作完成 · \(formattedTime)"
|
||
} else if mode == "error" {
|
||
return "错误: \(errorMessage)"
|
||
} else if mode == "idle" {
|
||
return "准备就绪"
|
||
} else {
|
||
return "工作中 \(formattedTime)"
|
||
}
|
||
}
|
||
}
|