=== 记忆系统 === - 新增 remember(记录)和 recall(搜索)工具,持久化到 ~/Library/Application Support/VoiceInput/memory/memory.json - 新增 set_identity 工具,独立管理助手名字/用户称呼/语气 - prompt 重写为个人助手身份,注入 macOS 能力 + 记忆规则 - prompt 中积极调用记忆工具,按需搜索、自然融入回答 - 禁止相对时间,待办必须绝对日期 === 对话持久化 === - 集成 conversation_store,done 前自动写入对话文件 - token 记录:prompt/completion 累加,total 覆盖为最新上下文长度 - 启动时加载最新对话继续(使用 eagent /resume 逻辑) - 去重:末尾连续 assistant 消息自动合并 - 修复消息加载顺序 bug(旧对话覆盖用户新消息) === 新建对话 === - 手动/自动两种模式(设置窗口可切换) - 自动模式:total > 阈值时隐藏按钮,下次输入自动新建 - 自动阈值在设置窗口可配置(默认 40000) === 弹窗 UI 统一 === - 语音输入按 Fn 即显示刘海弹窗(和打字输入统一) - 语音识别内容自动填入并发送(可关闭自动发送) - 工作期间(thinking/running)隐藏输入框,idle/done 时显示 - 工具显示三种模式:全部显示/最后隐藏/一直不显示 === 设置窗口增强 === - 助手名字 / 用户称呼 / 语气可配置 - 工具显示模式 / 新建对话模式 / 自动阈值 - 语音输入后自动发送开关(默认开) - 记忆管理窗口可视化编辑(Dock 可访问) - 窗口高度 840→860 === 菜单增强 === - Agent LLM 二级菜单切换语音/打字输入 - 助手设置和记忆管理入口(Dock 可访问) === 修复 === - Fn 按下时停止正在进行的朗读 - 弹窗已存在时不再覆盖 isTextInputMode - voiceInputText 加 @Published 以触发 UI 更新
121 lines
3.9 KiB
Swift
121 lines
3.9 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 // 打字输入模式标记(外部控制)
|
||
@Published var autoNewTriggered: Bool = false // 自动新建对话已触发
|
||
@Published var voiceInputText: String? = nil // 语音识别文本(不自动发送时填入输入框)
|
||
var toolsMode: String = "all" // all / hide_final / never
|
||
var newConversationMode: String = "manual" // manual / auto
|
||
var totalTokens: Int = 0
|
||
var manualNewRequested: Bool = false // 手动点击了新建对话
|
||
|
||
private var timer: Timer?
|
||
private var startTime: Date = Date()
|
||
private var toolPhaseActive = false // 工具后首段内容应替换
|
||
|
||
func start() {
|
||
startTime = Date()
|
||
elapsedSeconds = 0
|
||
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, totalTokens: Int = 0) {
|
||
mode = "done"
|
||
finalContent = content
|
||
self.totalTokens = totalTokens
|
||
if toolsMode == "hide_final" {
|
||
toolEntries = []
|
||
}
|
||
// 自动模式:total > 阈值时标记自动新建
|
||
if newConversationMode == "auto" && totalTokens > Config.shared.agentAutoNewTokenThreshold {
|
||
autoNewTriggered = true
|
||
}
|
||
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)"
|
||
}
|
||
}
|
||
}
|