agentix-cli 0.58.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/mac-voice/Sources/AgentXVoice/HistoryClient.swift +55 -0
- package/apps/mac-voice/Sources/AgentXVoice/HistoryModel.swift +153 -0
- package/apps/mac-voice/Sources/AgentXVoice/HistoryWindow.swift +301 -0
- package/apps/mac-voice/Sources/AgentXVoice/LocalSTT.swift +79 -0
- package/apps/mac-voice/Sources/AgentXVoice/ModelStore.swift +184 -0
- package/apps/mac-voice/Sources/AgentXVoice/Parakeet.swift +264 -0
- package/apps/mac-voice/Sources/AgentXVoice/Recorder.swift +81 -3
- package/apps/mac-voice/Sources/AgentXVoice/SettingsWindow.swift +10 -2
- package/apps/mac-voice/Sources/AgentXVoice/SileroVAD.swift +92 -0
- package/apps/mac-voice/Sources/AgentXVoice/Speech.swift +7 -5
- package/apps/mac-voice/Sources/AgentXVoice/StatusMenu.swift +5 -1
- package/apps/mac-voice/Sources/AgentXVoice/TurnEnd.swift +110 -0
- package/apps/mac-voice/Sources/AgentXVoice/VoiceSettings.swift +23 -0
- package/apps/mac-voice/Sources/AgentXVoice/main.swift +12 -26
- package/apps/mac-voice/build.sh +10 -1
- package/dist/{App-Z5VOAUXB.js → App-QEJGXZTY.js} +2 -2
- package/dist/{ChatApp-BE7ONQKS.js → ChatApp-VKWQN5GM.js} +2 -2
- package/dist/{agent-Q74EISKP.js → agent-MJKLI2L5.js} +2 -2
- package/dist/{board-dashboard-A7PQOI6C.js → board-dashboard-2KVT72J7.js} +221 -138
- package/dist/board-dashboard-2KVT72J7.js.map +1 -0
- package/dist/{chunk-I2LYULR4.js → chunk-2XZGHIVP.js} +5 -5
- package/dist/{chunk-5LLGIX5M.js → chunk-3EVCHRJF.js} +2 -2
- package/dist/{chunk-6QNPCVRN.js → chunk-4ZTV6JV7.js} +3 -3
- package/dist/chunk-4ZTV6JV7.js.map +1 -0
- package/dist/{chunk-VI7GBDKV.js → chunk-5OF3EGCZ.js} +2 -2
- package/dist/{chunk-RXEAACVH.js → chunk-6DUTIPTY.js} +2 -2
- package/dist/{chunk-CQUVUVXR.js → chunk-6ZV2QFXE.js} +2 -2
- package/dist/{chunk-2MIOMYT2.js → chunk-DAAWUFO4.js} +2 -2
- package/dist/{chunk-547G3HYQ.js → chunk-EJQOKSKP.js} +2 -2
- package/dist/{chunk-POL7RAJQ.js → chunk-ILXW3ERG.js} +117 -117
- package/dist/chunk-ILXW3ERG.js.map +1 -0
- package/dist/{chunk-ERL6AS3O.js → chunk-IW4XIZAZ.js} +8 -8
- package/dist/{chunk-ERL6AS3O.js.map → chunk-IW4XIZAZ.js.map} +1 -1
- package/dist/{chunk-Q6SMLSRO.js → chunk-JUY77L7C.js} +2 -2
- package/dist/{chunk-KW7TIOS3.js → chunk-K62U5EJJ.js} +2 -2
- package/dist/{chunk-KKGFZEXS.js → chunk-L5OCF33B.js} +37 -36
- package/dist/chunk-L5OCF33B.js.map +1 -0
- package/dist/{chunk-XTD3SZIP.js → chunk-N5GOBLUI.js} +2 -2
- package/dist/{chunk-VVBGPT6W.js → chunk-OIDMPPV5.js} +2 -2
- package/dist/{chunk-OZZPJ2YG.js → chunk-SDBU6747.js} +190 -177
- package/dist/chunk-SDBU6747.js.map +1 -0
- package/dist/{chunk-NKMFHEGV.js → chunk-WFPRVZBA.js} +2 -2
- package/dist/{chunk-6OABTCRA.js → chunk-WI5R64WO.js} +2 -2
- package/dist/{chunk-5W7NJHCS.js → chunk-YCIZ2WHX.js} +2 -2
- package/dist/{chunk-U5YBQJ5Q.js → chunk-YQU74AOL.js} +2 -2
- package/dist/{chunk-V7ZK6ZIO.js → chunk-YRB75GMN.js} +2 -2
- package/dist/cli.js +1 -1
- package/dist/client-X2PTYKVX.js +2 -0
- package/dist/{config-P6YXC6XZ.js → config-XB2ORKWS.js} +2 -2
- package/dist/config-mutator-KMXR3XDG.js +2 -0
- package/dist/doctor-6BIMNEGU.js +2 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +1 -1
- package/dist/program-DHDC6XYB.js +2 -0
- package/dist/{promote-KVPLDSKT.js → promote-GBR6YSQW.js} +2 -2
- package/dist/query-672RGMXO.js +2 -0
- package/dist/{registry-AUOI4TW2.js → registry-P535FNLB.js} +2 -2
- package/dist/{request-planner-LMZ5ZAXC.js → request-planner-KHTCHF7N.js} +2 -2
- package/dist/{review-candidates-WCBXLLDS.js → review-candidates-Y5KCRUNU.js} +2 -2
- package/dist/{routing-FBASYKUL.js → routing-RWCCDJ4D.js} +2 -2
- package/dist/schedule-tool-H46AK53H.js +2 -0
- package/dist/scheduler-3XLWBCX7.js +2 -0
- package/dist/{seat-DBFW3Z5P.js → seat-PJPKLH6L.js} +2 -2
- package/dist/{select-6CNVVFA5.js → select-VMTJAX5X.js} +2 -2
- package/dist/{set-enabled-AYZH3KSB.js → set-enabled-LPXH3ZQP.js} +2 -2
- package/dist/{tool-PKS4NBZ6.js → tool-MWXG44OT.js} +2 -2
- package/dist/wiki-QHMHVFWU.js +2 -0
- package/package.json +1 -1
- package/dist/board-dashboard-A7PQOI6C.js.map +0 -1
- package/dist/chunk-6QNPCVRN.js.map +0 -1
- package/dist/chunk-KKGFZEXS.js.map +0 -1
- package/dist/chunk-OZZPJ2YG.js.map +0 -1
- package/dist/chunk-POL7RAJQ.js.map +0 -1
- package/dist/client-EFP2PKA3.js +0 -2
- package/dist/config-mutator-3GKXLTHH.js +0 -2
- package/dist/doctor-JEWMHXYH.js +0 -2
- package/dist/program-7DUQJDSQ.js +0 -2
- package/dist/query-AGOMCYTT.js +0 -2
- package/dist/schedule-tool-RJRJ63SS.js +0 -2
- package/dist/scheduler-ATA7Z44S.js +0 -2
- package/dist/wiki-QW6BDNEQ.js +0 -2
- /package/dist/{App-Z5VOAUXB.js.map → App-QEJGXZTY.js.map} +0 -0
- /package/dist/{ChatApp-BE7ONQKS.js.map → ChatApp-VKWQN5GM.js.map} +0 -0
- /package/dist/{agent-Q74EISKP.js.map → agent-MJKLI2L5.js.map} +0 -0
- /package/dist/{chunk-I2LYULR4.js.map → chunk-2XZGHIVP.js.map} +0 -0
- /package/dist/{chunk-5LLGIX5M.js.map → chunk-3EVCHRJF.js.map} +0 -0
- /package/dist/{chunk-VI7GBDKV.js.map → chunk-5OF3EGCZ.js.map} +0 -0
- /package/dist/{chunk-RXEAACVH.js.map → chunk-6DUTIPTY.js.map} +0 -0
- /package/dist/{chunk-CQUVUVXR.js.map → chunk-6ZV2QFXE.js.map} +0 -0
- /package/dist/{chunk-2MIOMYT2.js.map → chunk-DAAWUFO4.js.map} +0 -0
- /package/dist/{chunk-547G3HYQ.js.map → chunk-EJQOKSKP.js.map} +0 -0
- /package/dist/{chunk-Q6SMLSRO.js.map → chunk-JUY77L7C.js.map} +0 -0
- /package/dist/{chunk-KW7TIOS3.js.map → chunk-K62U5EJJ.js.map} +0 -0
- /package/dist/{chunk-XTD3SZIP.js.map → chunk-N5GOBLUI.js.map} +0 -0
- /package/dist/{chunk-VVBGPT6W.js.map → chunk-OIDMPPV5.js.map} +0 -0
- /package/dist/{chunk-NKMFHEGV.js.map → chunk-WFPRVZBA.js.map} +0 -0
- /package/dist/{chunk-6OABTCRA.js.map → chunk-WI5R64WO.js.map} +0 -0
- /package/dist/{chunk-5W7NJHCS.js.map → chunk-YCIZ2WHX.js.map} +0 -0
- /package/dist/{chunk-U5YBQJ5Q.js.map → chunk-YQU74AOL.js.map} +0 -0
- /package/dist/{chunk-V7ZK6ZIO.js.map → chunk-YRB75GMN.js.map} +0 -0
- /package/dist/{client-EFP2PKA3.js.map → client-X2PTYKVX.js.map} +0 -0
- /package/dist/{config-P6YXC6XZ.js.map → config-XB2ORKWS.js.map} +0 -0
- /package/dist/{config-mutator-3GKXLTHH.js.map → config-mutator-KMXR3XDG.js.map} +0 -0
- /package/dist/{doctor-JEWMHXYH.js.map → doctor-6BIMNEGU.js.map} +0 -0
- /package/dist/{program-7DUQJDSQ.js.map → program-DHDC6XYB.js.map} +0 -0
- /package/dist/{promote-KVPLDSKT.js.map → promote-GBR6YSQW.js.map} +0 -0
- /package/dist/{query-AGOMCYTT.js.map → query-672RGMXO.js.map} +0 -0
- /package/dist/{registry-AUOI4TW2.js.map → registry-P535FNLB.js.map} +0 -0
- /package/dist/{request-planner-LMZ5ZAXC.js.map → request-planner-KHTCHF7N.js.map} +0 -0
- /package/dist/{review-candidates-WCBXLLDS.js.map → review-candidates-Y5KCRUNU.js.map} +0 -0
- /package/dist/{routing-FBASYKUL.js.map → routing-RWCCDJ4D.js.map} +0 -0
- /package/dist/{schedule-tool-RJRJ63SS.js.map → schedule-tool-H46AK53H.js.map} +0 -0
- /package/dist/{scheduler-ATA7Z44S.js.map → scheduler-3XLWBCX7.js.map} +0 -0
- /package/dist/{seat-DBFW3Z5P.js.map → seat-PJPKLH6L.js.map} +0 -0
- /package/dist/{select-6CNVVFA5.js.map → select-VMTJAX5X.js.map} +0 -0
- /package/dist/{set-enabled-AYZH3KSB.js.map → set-enabled-LPXH3ZQP.js.map} +0 -0
- /package/dist/{tool-PKS4NBZ6.js.map → tool-MWXG44OT.js.map} +0 -0
- /package/dist/{wiki-QW6BDNEQ.js.map → wiki-QHMHVFWU.js.map} +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import CoreML
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// Silero VAD on Core ML: how likely each 32 ms of 16 kHz audio is speech.
|
|
5
|
+
///
|
|
6
|
+
/// Core ML rather than ONNX Runtime: the model is 0.9 MB, macOS already
|
|
7
|
+
/// ships the runtime, and the app keeps building with plain swiftc and no
|
|
8
|
+
/// package dependency. The weights come from ModelStore.vad on first use.
|
|
9
|
+
///
|
|
10
|
+
/// Streaming: the model is an LSTM, so each call carries the previous
|
|
11
|
+
/// call's state and the last 64 samples as context, exactly as Silero's
|
|
12
|
+
/// own ONNX wrapper does. One `SileroVAD` per recording; the loaded model
|
|
13
|
+
/// is shared.
|
|
14
|
+
final class SileroVAD {
|
|
15
|
+
static let chunk = 512
|
|
16
|
+
static let context = 64
|
|
17
|
+
static let sampleRate = 16_000.0
|
|
18
|
+
static var frameDuration: TimeInterval { Double(chunk) / sampleRate }
|
|
19
|
+
|
|
20
|
+
private static let lock = NSLock()
|
|
21
|
+
nonisolated(unsafe) private static var cached: MLModel?
|
|
22
|
+
|
|
23
|
+
/// The shared model, loaded once. Nil when it is not installed or will
|
|
24
|
+
/// not load; the caller then falls back to the volume threshold.
|
|
25
|
+
static func loadModel() throws -> MLModel? {
|
|
26
|
+
lock.lock(); defer { lock.unlock() }
|
|
27
|
+
if let cached { return cached }
|
|
28
|
+
guard ModelStore.vad.isInstalled else { return nil }
|
|
29
|
+
let config = MLModelConfiguration()
|
|
30
|
+
// Tiny and called every 32 ms: the CPU answers faster than a trip
|
|
31
|
+
// to the Neural Engine.
|
|
32
|
+
config.computeUnits = .cpuOnly
|
|
33
|
+
let url = ModelStore.vad.url("silero-vad-unified-v6.0.0.mlmodelc")
|
|
34
|
+
let model = try MLModel(contentsOf: url, configuration: config)
|
|
35
|
+
cached = model
|
|
36
|
+
return model
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private let model: MLModel
|
|
40
|
+
private let audio: MLMultiArray
|
|
41
|
+
private var hidden: MLMultiArray
|
|
42
|
+
private var cell: MLMultiArray
|
|
43
|
+
private var pending: [Float] = []
|
|
44
|
+
private var tail = [Float](repeating: 0, count: SileroVAD.context)
|
|
45
|
+
|
|
46
|
+
init(model: MLModel) throws {
|
|
47
|
+
self.model = model
|
|
48
|
+
audio = try MLMultiArray(shape: [1, NSNumber(value: Self.context + Self.chunk)], dataType: .float32)
|
|
49
|
+
hidden = try Self.zeros()
|
|
50
|
+
cell = try Self.zeros()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private static func zeros() throws -> MLMultiArray {
|
|
54
|
+
let a = try MLMultiArray(shape: [1, 128], dataType: .float32)
|
|
55
|
+
for i in 0..<a.count { a[i] = 0 }
|
|
56
|
+
return a
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Feeds samples (16 kHz, -1…1) and returns one probability per
|
|
60
|
+
/// complete 512-sample frame; leftovers wait for the next call.
|
|
61
|
+
func process(_ samples: [Float]) throws -> [Float] {
|
|
62
|
+
pending.append(contentsOf: samples)
|
|
63
|
+
var out: [Float] = []
|
|
64
|
+
var start = 0
|
|
65
|
+
while pending.count - start >= Self.chunk {
|
|
66
|
+
let frame = pending[start..<start + Self.chunk]
|
|
67
|
+
out.append(try score(frame))
|
|
68
|
+
start += Self.chunk
|
|
69
|
+
}
|
|
70
|
+
pending.removeFirst(start)
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private func score(_ frame: ArraySlice<Float>) throws -> Float {
|
|
75
|
+
let ptr = audio.dataPointer.bindMemory(to: Float.self, capacity: audio.count)
|
|
76
|
+
for i in 0..<Self.context { ptr[i] = tail[i] }
|
|
77
|
+
var i = Self.context
|
|
78
|
+
for s in frame { ptr[i] = s; i += 1 }
|
|
79
|
+
tail = Array(frame.suffix(Self.context))
|
|
80
|
+
|
|
81
|
+
let input = try MLDictionaryFeatureProvider(dictionary: [
|
|
82
|
+
"audio_input": MLFeatureValue(multiArray: audio),
|
|
83
|
+
"hidden_state": MLFeatureValue(multiArray: hidden),
|
|
84
|
+
"cell_state": MLFeatureValue(multiArray: cell),
|
|
85
|
+
])
|
|
86
|
+
let result = try model.prediction(from: input)
|
|
87
|
+
if let h = result.featureValue(for: "new_hidden_state")?.multiArrayValue { hidden = h }
|
|
88
|
+
if let c = result.featureValue(for: "new_cell_state")?.multiArrayValue { cell = c }
|
|
89
|
+
guard let p = result.featureValue(for: "vad_output")?.multiArrayValue, p.count > 0 else { return 0 }
|
|
90
|
+
return p[0].floatValue
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -38,16 +38,18 @@ enum Speech {
|
|
|
38
38
|
// MARK: Speech to text
|
|
39
39
|
|
|
40
40
|
/// `engine` is agentx.json's `voice.stt`: "auto" and "elevenlabs" use
|
|
41
|
-
/// ElevenLabs when there is a key, falling back to
|
|
42
|
-
/// never sends audio off the Mac.
|
|
43
|
-
|
|
41
|
+
/// ElevenLabs when there is a key, falling back to the local engine;
|
|
42
|
+
/// "local" never sends audio off the Mac. `local` is `voice.localStt`:
|
|
43
|
+
/// "mlx-whisper" or "parakeet".
|
|
44
|
+
static func transcribe(wav: Data, engine: String = "auto", local: String = "mlx-whisper") async throws -> String {
|
|
44
45
|
if engine == "elevenlabs" && Config.elevenLabsKey == nil {
|
|
45
|
-
Log.warn("speech to text is set to ElevenLabs but no key is set; using
|
|
46
|
+
Log.warn("speech to text is set to ElevenLabs but no key is set; using \(local)")
|
|
46
47
|
}
|
|
47
48
|
if engine != "local", let key = Config.elevenLabsKey {
|
|
48
49
|
do { return try await elevenLabsSTT(wav: wav, key: key) }
|
|
49
|
-
catch { Log.warn("ElevenLabs STT failed (\(error.localizedDescription)); falling back to
|
|
50
|
+
catch { Log.warn("ElevenLabs STT failed (\(error.localizedDescription)); falling back to \(local)") }
|
|
50
51
|
}
|
|
52
|
+
if local == "parakeet", let text = await LocalSTT.parakeet(wav: wav) { return text }
|
|
51
53
|
return try await mlxWhisper(wav: wav)
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -23,6 +23,8 @@ final class StatusMenu: NSObject, NSMenuDelegate {
|
|
|
23
23
|
/// The daemon's speaking queue, polled while anything is in flight.
|
|
24
24
|
private var queue: AgentClient.QueueState?
|
|
25
25
|
private var queuePoll: Timer?
|
|
26
|
+
/// The last three voice exchanges, for a quick replay.
|
|
27
|
+
private let recent = RecentExchanges()
|
|
26
28
|
|
|
27
29
|
/// Questions this widget has in flight, per agent. Set by the app.
|
|
28
30
|
var thinking: [String: Int] = [:] {
|
|
@@ -76,6 +78,7 @@ final class StatusMenu: NSObject, NSMenuDelegate {
|
|
|
76
78
|
let agents = await AgentClient.agents()
|
|
77
79
|
roster = agents.map { .loaded($0) } ?? .down
|
|
78
80
|
queue = await AgentClient.queueState()
|
|
81
|
+
await recent.refresh()
|
|
79
82
|
rebuild()
|
|
80
83
|
showBadge()
|
|
81
84
|
if reopenAfterRefresh { reopenAfterRefresh = false; open() }
|
|
@@ -175,6 +178,7 @@ final class StatusMenu: NSObject, NSMenuDelegate {
|
|
|
175
178
|
menu.addItem(row)
|
|
176
179
|
}
|
|
177
180
|
}
|
|
181
|
+
recent.addItems(to: menu, name: name(of:))
|
|
178
182
|
|
|
179
183
|
menu.addItem(.separator())
|
|
180
184
|
menu.addItem(action("Stop speaking", #selector(stop), key: ".", modifiers: [.command, .option]))
|
|
@@ -251,7 +255,7 @@ final class StatusMenu: NSObject, NSMenuDelegate {
|
|
|
251
255
|
|
|
252
256
|
@objc private func openSettings() { onSettings?() }
|
|
253
257
|
@objc private func openDashboardHome() { openDashboard("/admin") }
|
|
254
|
-
@objc private func openHistory() {
|
|
258
|
+
@objc private func openHistory() { HistoryWindow.shared.show() }
|
|
255
259
|
|
|
256
260
|
private func openDashboard(_ path: String) {
|
|
257
261
|
guard let url = URL(string: Config.dashboardURL + path) else { return }
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// When a hands-free turn has ended: the person spoke, then stopped for
|
|
4
|
+
/// long enough that the sentence is over.
|
|
5
|
+
///
|
|
6
|
+
/// It is fed one audio frame at a time with how likely that frame is to be
|
|
7
|
+
/// speech (0…1) and how long it lasted. Two sources feed it:
|
|
8
|
+
///
|
|
9
|
+
/// - Silero VAD, 32 ms frames with a real probability. A voice model
|
|
10
|
+
/// tells a word from a fan, a keyboard or a café, so the noise of a room
|
|
11
|
+
/// neither holds the microphone open nor counts as talking.
|
|
12
|
+
/// - The old volume check when the VAD model is not available: 1 above
|
|
13
|
+
/// `speechLevel`, 0 below, per audio buffer.
|
|
14
|
+
///
|
|
15
|
+
/// Foundation only and time passed in, so the tests can drive it with
|
|
16
|
+
/// synthetic frames.
|
|
17
|
+
struct TurnEnd {
|
|
18
|
+
struct Tuning: Equatable {
|
|
19
|
+
/// A frame at or above this starts speech (Silero's own default).
|
|
20
|
+
var start: Float
|
|
21
|
+
/// Once talking, a frame at or above this still counts as speech.
|
|
22
|
+
/// Lower than `start`: the soft end of a word is not a pause.
|
|
23
|
+
var keep: Float
|
|
24
|
+
/// Speech must last this long in one go before the turn counts as
|
|
25
|
+
/// started, so a cough, a door or a click does not open it.
|
|
26
|
+
var minSpeech: TimeInterval
|
|
27
|
+
/// A burst this short inside a pause does not restart the pause:
|
|
28
|
+
/// one noisy frame must not hold the microphone open.
|
|
29
|
+
var resume: TimeInterval
|
|
30
|
+
/// A pause this long after speech ends the turn. People pause
|
|
31
|
+
/// inside sentences; this is longer than those pauses.
|
|
32
|
+
var endSilence: TimeInterval
|
|
33
|
+
|
|
34
|
+
/// For Silero VAD probabilities.
|
|
35
|
+
static let vad = Tuning(start: 0.5, keep: 0.35, minSpeech: 0.25, resume: 0.1, endSilence: 1.2)
|
|
36
|
+
|
|
37
|
+
/// The volume threshold as it always worked: any buffer above the
|
|
38
|
+
/// level is speech, and 1.2 s below it ends the turn.
|
|
39
|
+
static let level = Tuning(start: 0.5, keep: 0.5, minSpeech: 0, resume: 0, endSilence: 1.2)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
enum State: Equatable {
|
|
43
|
+
/// Nothing said yet.
|
|
44
|
+
case waiting
|
|
45
|
+
/// Speech heard; the turn is open (possibly in a short pause).
|
|
46
|
+
case speaking
|
|
47
|
+
/// Speech, then `endSilence` of quiet: send it.
|
|
48
|
+
case ended
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// RMS above which the microphone is hearing a voice rather than a
|
|
52
|
+
/// room. The fallback's whole notion of speech.
|
|
53
|
+
static let speechLevel: Float = 0.02
|
|
54
|
+
|
|
55
|
+
let tuning: Tuning
|
|
56
|
+
private(set) var state: State = .waiting
|
|
57
|
+
/// How long the current run of speech frames has lasted.
|
|
58
|
+
private var speechRun: TimeInterval = 0
|
|
59
|
+
/// How long the current pause has lasted (a short burst does not
|
|
60
|
+
/// reset it).
|
|
61
|
+
private var silence: TimeInterval = 0
|
|
62
|
+
|
|
63
|
+
init(_ tuning: Tuning) { self.tuning = tuning }
|
|
64
|
+
|
|
65
|
+
/// One frame: `probability` that it is speech, `duration` it covers.
|
|
66
|
+
@discardableResult
|
|
67
|
+
mutating func feed(probability: Float, duration: TimeInterval) -> State {
|
|
68
|
+
guard state != .ended else { return state }
|
|
69
|
+
let threshold = state == .speaking && silence == 0 ? tuning.keep : tuning.start
|
|
70
|
+
let speech = probability >= threshold
|
|
71
|
+
|
|
72
|
+
switch state {
|
|
73
|
+
case .waiting:
|
|
74
|
+
speechRun = speech ? speechRun + duration : 0
|
|
75
|
+
if speech && speechRun >= tuning.minSpeech {
|
|
76
|
+
state = .speaking
|
|
77
|
+
silence = 0
|
|
78
|
+
}
|
|
79
|
+
case .speaking:
|
|
80
|
+
if speech {
|
|
81
|
+
speechRun += duration
|
|
82
|
+
if silence == 0 || speechRun >= tuning.resume {
|
|
83
|
+
silence = 0
|
|
84
|
+
} else {
|
|
85
|
+
silence += duration
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
speechRun = 0
|
|
89
|
+
silence += duration
|
|
90
|
+
}
|
|
91
|
+
if silence >= tuning.endSilence { state = .ended }
|
|
92
|
+
case .ended:
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
return state
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// The fallback's frame: the smoothed microphone level.
|
|
99
|
+
@discardableResult
|
|
100
|
+
mutating func feed(level: Float, duration: TimeInterval) -> State {
|
|
101
|
+
feed(probability: level > Self.speechLevel ? 1 : 0, duration: duration)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// The microphone level after one buffer of loudness `rms`. Attack
|
|
105
|
+
/// fast, release slow: a level that drops instantly makes a pause
|
|
106
|
+
/// between words look like the end of a sentence.
|
|
107
|
+
static func smooth(level: Float, rms: Float) -> Float {
|
|
108
|
+
rms > level ? rms : level * 0.82 + rms * 0.18
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -12,7 +12,28 @@ struct VoiceSettings: Codable, Equatable {
|
|
|
12
12
|
var provider: String
|
|
13
13
|
var fallback: String
|
|
14
14
|
var stt: String
|
|
15
|
+
/// The on-device engine: "mlx-whisper" or "parakeet".
|
|
16
|
+
var localStt: String
|
|
17
|
+
/// How a hands-free turn's end is heard: "vad" or "volume".
|
|
18
|
+
var endOfTurn: String
|
|
15
19
|
var hotkeys: Hotkeys
|
|
20
|
+
|
|
21
|
+
init(provider: String, fallback: String, stt: String, localStt: String = "mlx-whisper",
|
|
22
|
+
endOfTurn: String = "vad", hotkeys: Hotkeys) {
|
|
23
|
+
self.provider = provider; self.fallback = fallback; self.stt = stt
|
|
24
|
+
self.localStt = localStt; self.endOfTurn = endOfTurn; self.hotkeys = hotkeys
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// A daemon from before these fields leaves them out: the defaults.
|
|
28
|
+
init(from decoder: Decoder) throws {
|
|
29
|
+
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
30
|
+
provider = try c.decode(String.self, forKey: .provider)
|
|
31
|
+
fallback = try c.decode(String.self, forKey: .fallback)
|
|
32
|
+
stt = try c.decode(String.self, forKey: .stt)
|
|
33
|
+
localStt = try c.decodeIfPresent(String.self, forKey: .localStt) ?? "mlx-whisper"
|
|
34
|
+
endOfTurn = try c.decodeIfPresent(String.self, forKey: .endOfTurn) ?? "vad"
|
|
35
|
+
hotkeys = try c.decode(Hotkeys.self, forKey: .hotkeys)
|
|
36
|
+
}
|
|
16
37
|
}
|
|
17
38
|
struct Voice: Codable, Equatable {
|
|
18
39
|
var provider: String?
|
|
@@ -55,6 +76,8 @@ struct VoiceSettings: Codable, Equatable {
|
|
|
55
76
|
var general: [String: Any] = [:]
|
|
56
77
|
if self.general.provider != old.general.provider { general["provider"] = self.general.provider }
|
|
57
78
|
if self.general.stt != old.general.stt { general["stt"] = self.general.stt }
|
|
79
|
+
if self.general.localStt != old.general.localStt { general["localStt"] = self.general.localStt }
|
|
80
|
+
if self.general.endOfTurn != old.general.endOfTurn { general["endOfTurn"] = self.general.endOfTurn }
|
|
58
81
|
var keys: [String: Any] = [:]
|
|
59
82
|
if self.general.hotkeys.talk != old.general.hotkeys.talk { keys["talk"] = self.general.hotkeys.talk }
|
|
60
83
|
if self.general.hotkeys.stop != old.general.hotkeys.stop { keys["stop"] = self.general.hotkeys.stop }
|
|
@@ -321,20 +321,14 @@ final class App: NSObject, NSApplicationDelegate {
|
|
|
321
321
|
// Ending on SILENCE rather than a timer is the whole trick. A fixed
|
|
322
322
|
// window either cuts people off mid-sentence or leaves the mic open
|
|
323
323
|
// staring at them; the only honest signal that a sentence has finished
|
|
324
|
-
// is the
|
|
324
|
+
// is the voice stopping. The recorder judges that (TurnEnd.swift).
|
|
325
325
|
|
|
326
|
-
/// RMS above which the microphone is hearing a voice rather than a room.
|
|
327
|
-
private let speechLevel: Float = 0.02
|
|
328
|
-
/// Quiet this long after speech ends the turn.
|
|
329
|
-
private let endSilence: TimeInterval = 1.2
|
|
330
326
|
/// How long an unprompted follow-up window waits before giving up.
|
|
331
327
|
private let followUpPatience: TimeInterval = 4.0
|
|
332
328
|
/// How long a clicked session waits for you to start talking.
|
|
333
329
|
private let clickPatience: TimeInterval = 8.0
|
|
334
330
|
|
|
335
331
|
private var listenPoll: Timer?
|
|
336
|
-
private var heardSpeech = false
|
|
337
|
-
private var quietSince: Date?
|
|
338
332
|
private var openedAt = Date()
|
|
339
333
|
private var patience: TimeInterval = 8.0
|
|
340
334
|
/// True when nothing was said and the window should close in silence.
|
|
@@ -343,14 +337,13 @@ final class App: NSObject, NSApplicationDelegate {
|
|
|
343
337
|
/// Open the microphone with no key held.
|
|
344
338
|
private func listenHandsFree(followUp: Bool) {
|
|
345
339
|
guard !busy, !recorder.isRecording else { return }
|
|
340
|
+
recorder.endOfTurn = settings?.general.endOfTurn ?? "vad"
|
|
346
341
|
do {
|
|
347
342
|
try recorder.start()
|
|
348
343
|
} catch {
|
|
349
344
|
panel.render(.error(error.localizedDescription))
|
|
350
345
|
return
|
|
351
346
|
}
|
|
352
|
-
heardSpeech = false
|
|
353
|
-
quietSince = nil
|
|
354
347
|
openedAt = Date()
|
|
355
348
|
patience = followUp ? followUpPatience : clickPatience
|
|
356
349
|
silentClose = followUp
|
|
@@ -369,23 +362,13 @@ final class App: NSObject, NSApplicationDelegate {
|
|
|
369
362
|
|
|
370
363
|
private func pollLevel() {
|
|
371
364
|
guard recorder.isRecording else { stopPolling(); return }
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return
|
|
378
|
-
}
|
|
379
|
-
if heardSpeech {
|
|
380
|
-
// Speech has stopped. Give it a beat before deciding the
|
|
381
|
-
// sentence is over — people pause inside sentences.
|
|
382
|
-
let since = quietSince ?? Date()
|
|
383
|
-
quietSince = since
|
|
384
|
-
if Date().timeIntervalSince(since) >= endSilence {
|
|
385
|
-
stopPolling()
|
|
386
|
-
stopAndSend()
|
|
387
|
-
}
|
|
365
|
+
switch recorder.turnState {
|
|
366
|
+
case .speaking: return
|
|
367
|
+
case .ended:
|
|
368
|
+
stopPolling()
|
|
369
|
+
stopAndSend()
|
|
388
370
|
return
|
|
371
|
+
case .waiting: break
|
|
389
372
|
}
|
|
390
373
|
// Nothing said yet. Close quietly rather than making the person
|
|
391
374
|
// dismiss a window they did not ask for.
|
|
@@ -444,6 +427,8 @@ final class App: NSObject, NSApplicationDelegate {
|
|
|
444
427
|
lastSpokeAt = Date()
|
|
445
428
|
talkCheck = Task { await AgentClient.hush() }
|
|
446
429
|
Log.info("door: opened\(busy ? " (a turn is running)" : "")")
|
|
430
|
+
// Push-to-talk ends on key release; no end-of-turn detection needed.
|
|
431
|
+
recorder.endOfTurn = "hold"
|
|
447
432
|
do {
|
|
448
433
|
try recorder.start()
|
|
449
434
|
asideSpeaker = nil
|
|
@@ -474,7 +459,8 @@ final class App: NSObject, NSApplicationDelegate {
|
|
|
474
459
|
|
|
475
460
|
Task { @MainActor in
|
|
476
461
|
var heard = ""
|
|
477
|
-
do { heard = try await Speech.transcribe(wav: wav, engine: settings?.general.stt ?? "auto"
|
|
462
|
+
do { heard = try await Speech.transcribe(wav: wav, engine: settings?.general.stt ?? "auto",
|
|
463
|
+
local: settings?.general.localStt ?? "mlx-whisper") }
|
|
478
464
|
catch { Log.warn("transcription failed: \(error.localizedDescription)") }
|
|
479
465
|
guard !heard.isEmpty else {
|
|
480
466
|
_ = await door.value
|
package/apps/mac-voice/build.sh
CHANGED
|
@@ -18,9 +18,18 @@ rm -rf build && mkdir -p "$BIN" "$APP/Contents/Resources"
|
|
|
18
18
|
swiftc -O \
|
|
19
19
|
-o "$BIN/AgentXVoice" \
|
|
20
20
|
Sources/AgentXVoice/*.swift \
|
|
21
|
-
-framework AppKit -framework AVFoundation -framework Carbon -framework SwiftUI \
|
|
21
|
+
-framework AppKit -framework AVFoundation -framework Carbon -framework CoreML -framework SwiftUI \
|
|
22
22
|
-target arm64-apple-macosx14.0
|
|
23
23
|
|
|
24
|
+
# The on-device models from a terminal: fetch them, transcribe a file, see
|
|
25
|
+
# where a turn would end. Same sources as the app; the models themselves
|
|
26
|
+
# are downloaded on first use into ~/.agentx/models, never bundled.
|
|
27
|
+
swiftc -O \
|
|
28
|
+
-o "$BIN/agentx-voice-local" \
|
|
29
|
+
Sources/AgentXVoice/ModelStore.swift Sources/AgentXVoice/SileroVAD.swift \
|
|
30
|
+
Sources/AgentXVoice/Parakeet.swift Sources/AgentXVoice/TurnEnd.swift Tools/LocalSTT/main.swift \
|
|
31
|
+
-framework CoreML -target arm64-apple-macosx14.0
|
|
32
|
+
|
|
24
33
|
cp Resources/Info.plist "$APP/Contents/Info.plist"
|
|
25
34
|
|
|
26
35
|
# Ad-hoc signature. Unsigned bundles get a fresh TCC identity on every
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{a as F,b as L,c as U,d as N,e as O,f as H,g as j}from"./chunk-U2CKL755.js";import{b as B,c as D,d as A,e as P,g as M}from"./chunk-
|
|
1
|
+
import{a as F,b as L,c as U,d as N,e as O,f as H,g as j}from"./chunk-U2CKL755.js";import{b as B,c as D,d as A,e as P,g as M}from"./chunk-YCIZ2WHX.js";import"./chunk-IW4XIZAZ.js";import"./chunk-QIJLFPWE.js";import{useEffect as R,useReducer as V,useRef as G}from"react";import{Box as i,Text as o,useApp as W,useInput as J}from"ink";import{randomUUID as z}from"crypto";async function*q(e){let r=[];e.type&&r.push(`type=${encodeURIComponent(e.type)}`);let t=`${e.baseUrl.replace(/\/+$/,"")}/events${r.length?`?${r.join("&")}`:""}`,n={Accept:"text/event-stream"};e.token&&(n.Authorization=`Bearer ${e.token}`);let s=await fetch(t,{headers:n,signal:e.signal});if(!s.ok||!s.body)throw new Error(`SSE connect failed: HTTP ${s.status}`);let u=s.body.getReader(),p=new TextDecoder,y="";try{for(;;){let{done:f,value:l}=await u.read();if(f)return;y+=p.decode(l,{stream:!0});let d;for(;(d=y.indexOf(`
|
|
2
2
|
|
|
3
3
|
`))>=0;){let h=y.slice(0,d);y=y.slice(d+2);let m=X(h);m&&(yield m)}}}finally{try{u.releaseLock()}catch{}}}function X(e){let r="message",t="";for(let s of e.split(`
|
|
4
4
|
`))s.startsWith("event: ")?r=s.slice(7):s.startsWith("data: ")&&(t=s.slice(6));if(!t)return null;let n;try{n=JSON.parse(t)}catch{n=t}return{event:r,data:n}}import{jsx as a,jsxs as c}from"react/jsx-runtime";function $(e,r){return r<=0||e<0?0:e>=r?r-1:e}function E(){return`tui:${z().slice(0,12)}`}function k(e,r){let t=[...e,r];return t.length>50&&t.splice(0,t.length-50),t}function Y(e,r){switch(r.type){case"agents":return{...e,agents:r.data,cursor:{...e.cursor,agents:$(e.cursor.agents,r.data.length)}};case"processes":return{...e,processes:r.data,cursor:{...e.cursor,processes:$(e.cursor.processes,r.data.length)}};case"crons":return{...e,crons:r.data};case"event":{let t=[r.row,...e.events];t.length>200&&(t.length=200);let n=e.channels;return r.channelTally&&(n=new Map(e.channels),n.set(r.channelTally.channel,r.channelTally)),{...e,events:t,channels:n}}case"connError":return{...e,connError:r.error};case"tick":return{...e,lastTick:Date.now()};case"focus":return{...e,focus:r.pane};case"cursor":{let t=r.pane==="agents"?e.agents.length:r.pane==="processes"?e.processes.length:e.events.length;return{...e,cursor:{...e.cursor,[r.pane]:$(e.cursor[r.pane]+r.delta,t)}}}case"bottomRight":return{...e,bottomRight:r.pane};case"chatStart":return{...e,chat:{active:!0,agentId:r.agentId,chatId:E(),history:[],text:"",status:"idle",error:null}};case"chatExit":return{...e,chat:{...e.chat,active:!1}};case"chatClear":return{...e,chat:{...e.chat,chatId:E(),history:[],text:"",status:"idle",error:null}};case"chatText":return{...e,chat:{...e.chat,text:r.text}};case"chatSubmitStart":return{...e,chat:{...e.chat,history:k(e.chat.history,r.you),text:"",status:"sending",error:null}};case"chatSubmitDone":return{...e,chat:{...e.chat,history:k(e.chat.history,r.reply),status:"idle",error:null}};case"chatSubmitError":return{...e,chat:{...e.chat,history:k(e.chat.history,{role:"error",text:r.error,at:Date.now()}),status:"idle",error:r.error}};case"chatStreamStart":{let t=k(e.chat.history,r.you),n=k(t,{role:"agent",text:"",at:Date.now(),streaming:!0,tools:[]});return{...e,chat:{...e.chat,history:n,text:"",status:"sending",error:null}}}case"chatStreamDelta":{let t=e.chat.history.slice(),n=t[t.length-1];return n?.role==="agent"&&n.streaming&&(t[t.length-1]={...n,text:n.text+r.text}),{...e,chat:{...e.chat,history:t}}}case"chatStreamThinking":{let t=e.chat.history.slice(),n=t[t.length-1];return n?.role==="agent"&&n.streaming&&(t[t.length-1]={...n,thinking:(n.thinking??"")+r.text}),{...e,chat:{...e.chat,history:t}}}case"chatStreamTool":{let t=e.chat.history.slice(),n=t[t.length-1];return n?.role==="agent"&&n.streaming&&(t[t.length-1]={...n,tools:[...n.tools??[],{name:r.name,arg:r.arg,error:r.error}]}),{...e,chat:{...e.chat,history:t}}}case"chatStreamEnd":{let t=e.chat.history.slice(),n=t[t.length-1];return n?.role==="agent"&&n.streaming&&(t[t.length-1]=r.error?{...n,streaming:!1,role:"error",text:r.error}:{...n,streaming:!1,elapsedMs:r.elapsedMs,outTokens:r.outTokens}),{...e,chat:{...e.chat,history:t,status:"idle",error:r.error??null}}}case"toast":return{...e,toast:{text:r.text,color:r.color,at:Date.now()}};case"clearToast":return{...e,toast:null}}}var _={agents:[],processes:[],crons:[],events:[],channels:new Map,connError:null,lastTick:0,focus:"agents",cursor:{agents:0,processes:0,events:0},bottomRight:"crons",chat:{active:!1,agentId:null,chatId:E(),history:[],text:"",status:"idle",error:null},toast:null};function be({conn:e,pollMs:r=3e3}){let[t,n]=V(Y,_),{exit:s}=W(),u=G(null),p=G(null),y=t.agents.map(l=>l.id),f=t.chat.active?L(t.chat.text,y,process.cwd()):null;return R(()=>{if(!t.toast)return;let l=setTimeout(()=>n({type:"clearToast"}),3500);return()=>clearTimeout(l)},[t.toast]),J((l,d)=>{if(t.chat.active){if(d.escape){n({type:"chatExit"});return}if(d.ctrl&&l==="l"){n({type:"chatClear"});return}if(d.tab){let m=U(t.chat.text,p,y,process.cwd());m!==null&&n({type:"chatText",text:m});return}if(d.backspace||d.delete){p.current=null,n({type:"chatText",text:t.chat.text.slice(0,-1)});return}if(d.ctrl||d.meta)return;let h=N(l??"",d.return,t.chat.text);if(h.kind==="paste"||h.kind==="type"){p.current=null,n({type:"chatText",text:t.chat.text+h.text});return}if(h.kind==="continue"){n({type:"chatText",text:h.buffer});return}if(h.kind==="submit"){let m=h.text.trim(),g=t.chat.agentId;if(!m||!g||t.chat.status==="sending"){n({type:"chatText",text:""});return}p.current=null;let b=Date.now(),w=t.chat.chatId;n({type:"chatStreamStart",you:{role:"you",text:m,at:b}}),(async()=>{try{let C=await M(e,g,m,{channel:"tui",chatId:w,onText:x=>n({type:"chatStreamDelta",text:x}),onThinking:x=>n({type:"chatStreamThinking",text:x}),onTool:x=>{x.status==="start"&&x.name?n({type:"chatStreamTool",name:x.name,arg:x.arg}):x.status==="result"&&x.error&&n({type:"chatStreamTool",name:x.name??"tool",error:!0})}});n({type:"chatStreamEnd",elapsedMs:Date.now()-b,outTokens:C.usage?.outputTokens,error:C.error})}catch(C){n({type:"chatStreamEnd",elapsedMs:Date.now()-b,error:C?.message||String(C)})}})();return}return}if(l==="q"||d.ctrl&&l==="c"){u.current?.abort(),s();return}if(d.tab){let h=["agents","processes","events"],m=h.indexOf(t.focus);n({type:"focus",pane:h[(m+1)%h.length]});return}if(l==="a"){n({type:"focus",pane:"agents"});return}if(l==="p"){n({type:"focus",pane:"processes"});return}if(l==="e"){n({type:"focus",pane:"events"});return}if(l==="c"){n({type:"bottomRight",pane:t.bottomRight==="crons"?"channels":"crons"});return}if(d.upArrow){n({type:"cursor",pane:t.focus,delta:-1});return}if(d.downArrow){n({type:"cursor",pane:t.focus,delta:1});return}if(d.return&&t.focus==="agents"){let h=t.agents[t.cursor.agents];if(!h){n({type:"toast",text:"no agent selected",color:"yellow"});return}n({type:"chatStart",agentId:h.id});return}if(l==="k"&&t.focus==="processes"){let h=t.processes[t.cursor.processes];if(!h){n({type:"toast",text:"no process selected",color:"yellow"});return}(async()=>{try{await P(e,h.key,"tui"),n({type:"toast",text:`killed ${h.key.agentId}/${h.key.channel}/${h.key.chatId}`,color:"green"})}catch(m){n({type:"toast",text:`kill failed: ${m?.message||m}`,color:"red"})}})();return}}),R(()=>{let l=new AbortController;u.current=l;let d=!1,h=async()=>{try{let[g,b,w]=await Promise.all([B(e,l.signal).catch(()=>[]),D(e,l.signal).catch(()=>[]),A(e,l.signal).catch(()=>[])]);if(d)return;n({type:"agents",data:g}),n({type:"processes",data:b}),n({type:"crons",data:w}),n({type:"tick"}),n({type:"connError",error:null})}catch(g){d||n({type:"connError",error:g?.message||String(g)})}};h();let m=setInterval(h,r);return()=>{d=!0,clearInterval(m),l.abort()}},[e.baseUrl,e.token,r]),R(()=>{let l=!1,d=new AbortController,h=new Map;return(async()=>{let m=500;for(;!l;){try{for await(let g of q({baseUrl:e.baseUrl,token:e.token,signal:d.signal})){let b=ae(g),w;if(g.event==="channel"&&typeof g.data?.channel=="string"){let S=h.get(g.data.channel)??{channel:g.data.channel,inbound:0,outbound:0,lastAt:0,lastPreview:""},C=S.inbound+(g.data.direction==="in"?1:0),x=S.outbound+(g.data.direction==="out"?1:0);w={channel:g.data.channel,inbound:C,outbound:x,lastAt:b.at,lastPreview:(g.data.textPreview||"").slice(0,60)},h.set(g.data.channel,w)}n({type:"event",row:b,channelTally:w}),m=500}}catch(g){if(l)return;n({type:"connError",error:`events: ${g?.message||g}`})}await new Promise(g=>setTimeout(g,m)),m=Math.min(m*2,1e4)}})(),()=>{l=!0,d.abort()}},[e.baseUrl,e.token]),c(i,{flexDirection:"column",width:"100%",children:[a(K,{conn:e,lastTick:t.lastTick,connError:t.connError,chat:t.chat}),c(i,{flexDirection:"row",flexGrow:1,children:[c(i,{flexDirection:"column",width:"50%",children:[a(v,{title:"AGENTS",focused:!t.chat.active&&t.focus==="agents",children:a(Z,{rows:t.agents,cursor:t.cursor.agents,focused:!t.chat.active&&t.focus==="agents",talkingTo:t.chat.active?t.chat.agentId:null})}),a(v,{title:"PROCESSES",focused:!t.chat.active&&t.focus==="processes",children:a(ee,{rows:t.processes,cursor:t.cursor.processes,focused:!t.chat.active&&t.focus==="processes"})})]}),t.chat.active?a(oe,{chat:t.chat,suggest:f,agents:t.agents}):c(i,{flexDirection:"column",width:"50%",children:[a(v,{title:"LIVE EVENTS",focused:t.focus==="events",children:a(ne,{rows:t.events,cursor:t.cursor.events,focused:t.focus==="events"})}),a(v,{title:t.bottomRight==="crons"?"CRONS / SCHEDULES":"CHANNELS",focused:!1,children:t.bottomRight==="crons"?a(te,{rows:t.crons}):a(re,{rows:Array.from(t.channels.values())})})]})]}),a(Q,{focus:t.focus,bottomRight:t.bottomRight,chat:t.chat,toast:t.toast})]})}function K({conn:e,lastTick:r,connError:t,chat:n}){let s=t?c(o,{color:"red",children:["\u25CF ",t]}):r===0?a(o,{color:"yellow",children:"\u25CF connecting\u2026"}):a(o,{color:"green",children:"\u25CF live"}),u=n.active?c(o,{color:"green",bold:!0,children:["CHAT @",n.agentId]}):a(o,{bold:!0,children:"agentx tui"});return c(i,{paddingX:1,justifyContent:"space-between",children:[a(i,{children:u}),a(o,{dimColor:!0,children:e.baseUrl}),a(i,{children:s})]})}function Q({focus:e,bottomRight:r,chat:t,toast:n}){let s=t.active?["[Enter] send","[Esc] exit chat","[Ctrl-L] new session"]:[`[Tab/a/p/e] focus (now: ${e})`,e==="agents"?"[Enter] chat":null,e==="processes"?"[k] kill":null,`[c] ${r==="crons"?"\u2192channels":"\u2192crons"}`,"[\u2191/\u2193] move","[q] quit"].filter(Boolean);return c(i,{paddingX:1,borderStyle:"single",borderTop:!0,borderBottom:!1,borderLeft:!1,borderRight:!1,justifyContent:"space-between",children:[a(o,{dimColor:!0,children:s.join(" \xB7 ")}),n?a(o,{color:n.color,children:n.text}):a(o,{children:" "})]})}function v({title:e,focused:r,children:t}){return c(i,{flexDirection:"column",width:"100%",borderStyle:r?"double":"single",borderColor:r?"cyan":void 0,paddingX:1,children:[c(o,{bold:!0,color:r?"cyan":"white",children:[e,r?" \u25CF":""]}),a(i,{flexDirection:"column",marginTop:1,children:t})]})}function I(e,r){return e&&r?{inverse:!0}:{}}function Z({rows:e,cursor:r,focused:t,talkingTo:n}){return e.length===0?a(o,{dimColor:!0,children:"no agents"}):a(i,{flexDirection:"column",children:e.slice(0,12).map((s,u)=>{let p=I(t,u===r),y=s.errors>0?"red":s.active>0?"yellow":"green",f=s.id===n;return c(i,{children:[a(o,{...p,color:y,children:"\u25CF"}),c(o,{...p,children:[" ",T(s.name,14)]}),c(o,{...p,children:[T(s.tier,6)," "]}),c(o,{...p,children:[s.active,"/",s.total]}),s.errors>0?c(o,{...p,color:"red",children:[" err=",s.errors]}):null,f?a(o,{color:"green",children:" \u2194 chatting"}):null]},s.id)})})}function ee({rows:e,cursor:r,focused:t}){if(e.length===0)return a(o,{dimColor:!0,children:"no live processes"});let n=Date.now();return a(i,{flexDirection:"column",children:e.slice(0,12).map((s,u)=>{let p=I(t,u===r),y=Math.max(0,Math.round((n-(s.lastTurnAt||s.spawnedAt))/1e3)),f=s.state==="dead"?"red":s.state==="warm-hot"?"green":"yellow";return c(i,{children:[a(o,{...p,color:f,children:T(s.state,10)}),a(o,{...p,children:T(s.key.agentId,12)}),a(o,{...p,children:T(s.key.channel,10)}),c(o,{...p,children:[s.turnCount,"t ",y,"s"]})]},`${s.key.agentId}-${s.key.channel}-${s.key.chatId}-${u}`)})})}function te({rows:e}){return e.length===0?a(o,{dimColor:!0,children:"no crons"}):a(i,{flexDirection:"column",children:e.slice(0,12).map(r=>{let t=r.enabled?r.consecutiveErrors>0?a(o,{color:"red",children:"err"}):a(o,{color:"green",children:"ok"}):a(o,{dimColor:!0,children:"off"}),n=r.nextRun?new Date(r.nextRun).toLocaleTimeString():"\u2014";return c(i,{children:[a(o,{children:T(r.id,16)}),a(o,{dimColor:!0,children:T(r.schedule,12)}),a(o,{children:T(n,10)}),a(i,{children:t})]},r.id)})})}function re({rows:e}){if(e.length===0)return a(o,{dimColor:!0,children:"(waiting for channel events\u2026)"});let r=[...e].sort((t,n)=>n.lastAt-t.lastAt);return a(i,{flexDirection:"column",children:r.slice(0,12).map(t=>c(i,{children:[a(o,{children:T(t.channel,12)}),c(o,{color:"cyan",children:["\u2190",T(String(t.inbound),4)]}),c(o,{color:"magenta",children:["\u2192",T(String(t.outbound),4)]}),a(o,{dimColor:!0,children:t.lastPreview})]},t.channel))})}function ne({rows:e,cursor:r,focused:t}){return e.length===0?a(o,{dimColor:!0,children:"(waiting for events\u2026)"}):a(i,{flexDirection:"column",children:e.slice(0,18).map((n,s)=>{let u=I(t,s===r);return c(i,{children:[a(o,{...u,dimColor:!0,children:new Date(n.at).toLocaleTimeString().padEnd(10)}),a(o,{...u,color:ce(n.kind),children:T(n.kind,8)}),a(o,{...u,children:n.line})]},`${n.at}-${s}`)})})}function oe({chat:e,suggest:r,agents:t}){let n=e.history.slice(-14),s=t.find(f=>f.id===e.agentId),u=H(s?.tier??"",s?.model),p=e.status==="sending"?[...e.history].reverse().find(f=>f.role==="agent"&&f.streaming):void 0,y=e.status==="sending"?a(O,{startedAt:p?.at??Date.now(),phase:p?.text?"responding":"thinking",indent:""}):e.error?c(o,{color:"red",children:["\u25CF ",e.error]}):a(o,{dimColor:!0,children:"\u25CF ready"});return c(i,{flexDirection:"column",width:"50%",borderStyle:"double",borderColor:"green",paddingX:1,children:[c(i,{justifyContent:"space-between",children:[c(o,{bold:!0,color:"green",children:["CHAT \xB7 @",e.agentId]}),a(i,{children:y})]}),c(o,{children:[c(o,{color:u.color,bold:!0,children:[u.glyph," ",u.label]}),s?.model?c(o,{dimColor:!0,children:[" ",j(s.model)]}):null,c(o,{dimColor:!0,children:[" \xB7 ",e.chatId]})]}),a(i,{flexDirection:"column",flexGrow:1,marginTop:1,children:n.length===0?a(o,{dimColor:!0,children:"(type a message and press Enter \u2014 Esc exits, Ctrl-L starts fresh)"}):n.map((f,l)=>a(se,{turn:f,agentId:e.agentId},`${f.at}-${l}`))}),r?c(o,{dimColor:!0,children:[a(o,{color:"cyan",children:"\u21B9"})," ",r.items.slice(0,5).map((f,l)=>l===0?a(o,{color:"cyan",children:f},l):c(o,{children:[" ",f]},l))]}):null,c(i,{children:[a(o,{color:"cyan",children:"you \u203A "}),a(o,{children:e.text}),a(o,{inverse:!0,children:" "})]})]})}function se({turn:e,agentId:r}){if(e.role==="you")return c(i,{flexDirection:"column",marginBottom:1,children:[a(o,{color:"cyan",children:"you"}),e.text.split(`
|
|
5
5
|
`).map((s,u)=>c(o,{children:[" ",s]},u))]});if(e.role==="error")return c(i,{flexDirection:"column",marginBottom:1,children:[a(o,{color:"red",children:"error"}),c(o,{color:"red",children:[" ",e.text]})]});let t=e.elapsedMs!=null?` \xB7 ${(e.elapsedMs/1e3).toFixed(1)}s${e.outTokens!=null?`, ${e.outTokens} tok`:""}`:e.streaming?" \xB7 \u2026":"",n=F(e.text,58,{balance:e.streaming});return c(i,{flexDirection:"column",marginBottom:1,children:[c(o,{color:"green",children:["@",r??"agent",a(o,{dimColor:!0,children:t})]}),e.thinking?e.thinking.split(`
|
|
6
6
|
`).filter(s=>s.trim()).slice(-2).map((s,u)=>c(o,{dimColor:!0,italic:!0,children:[" ",s]},`th${u}`)):null,(e.tools??[]).map((s,u)=>c(o,{color:s.error?"red":"green",children:[" \u25CF ",c(o,{bold:!s.error,dimColor:s.error,children:[s.name,s.arg?c(o,{dimColor:!0,children:["(",s.arg,")"]}):null,s.error?" failed":""]})]},`tool${u}`)),n.split(`
|
|
7
7
|
`).slice(0,14).map((s,u)=>c(o,{children:[" ",s]},u))]})}function ae(e){let r=e.data??{},t=typeof r.at=="string"||typeof r.at=="number"?new Date(r.at).getTime():Date.now(),n="";switch(e.event){case"run":n=`${r.phase??""} ${r.workflowId??""} ${(r.runId??"").slice(0,8)} ${r.nodeId??""}`.trim();break;case"task":n=`${r.phase??""} ${r.workflowId??""} ${Array.isArray(r.assignedTo)?r.assignedTo.join(","):""} ${r.title?`"${r.title}"`:""}`.trim();break;case"signal":n=`${r.name??""} (${r.scope??""}) ${r.workflowId??""}`.trim();break;case"mesh":n=`${r.peer??""} ${r.healthy?"healthy":"lost"} ${r.delta??""}`.trim();break;case"channel":{n=`${r.direction==="in"?"\u2190":"\u2192"} ${r.channel??""}:${r.chatId??""} ${r.textPreview??""}`.trim();break}case"status":n=`node=${r.node??""} agents=${r.agents??0} active=${(r.active??[]).length}`;break;default:n=typeof r=="string"?r:JSON.stringify(r).slice(0,80)}return{at:Number.isFinite(t)?t:Date.now(),kind:e.event,line:n}}function ce(e){switch(e){case"run":return"cyan";case"task":return"magenta";case"signal":return"green";case"mesh":return"blue";case"channel":return"yellow";default:return"white"}}function T(e,r){let t=(e??"").toString();return t.length>=r?t.slice(0,r-1)+" ":t+" ".repeat(r-t.length)}export{be as App};
|
|
8
|
-
//# sourceMappingURL=App-
|
|
8
|
+
//# sourceMappingURL=App-QEJGXZTY.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import{a as Y,b as Z,c as _,d as ee,e as te,f as ne,g as re}from"./chunk-U2CKL755.js";import{b as Q,g as X}from"./chunk-
|
|
1
|
+
import{a as Y,b as Z,c as _,d as ee,e as te,f as ne,g as re}from"./chunk-U2CKL755.js";import{b as Q,g as X}from"./chunk-YCIZ2WHX.js";import"./chunk-IW4XIZAZ.js";import"./chunk-QIJLFPWE.js";import{useCallback as oe,useMemo as H,useRef as P,useState as k}from"react";import{Box as g,Text as t,useApp as he,useInput as xe,useStdout as Te}from"ink";import{randomUUID as se}from"crypto";var m={accent:"cyan",agent:"green",working:"magenta",error:"red",muted:"gray"},q="\u25C7 agentx",F="\u258C";import{jsx as c,jsxs as o}from"react/jsx-runtime";var ie="/help /agent <id> /agents /clear /who /exit",be=[{cmd:"/agent",desc:"switch the target agent (/agent <id>)"},{cmd:"/agents",desc:"list registered agents"},{cmd:"/clear",desc:"start a fresh session"},{cmd:"/who",desc:"show agent + chat id"},{cmd:"/help",desc:"list commands"},{cmd:"/exit",desc:"quit the chat"}];function Pe({conn:n,agentId:p,channel:f,chatId:a,agents:h}){let{exit:w}=he(),{stdout:O}=Te(),V=Math.max(40,Math.min((O?.columns||80)-4,100)),[z,U]=k([]),[$,S]=k(null),[T,le]=k(p),[A,ae]=k(h),[E,ue]=k(a),[y,x]=k(""),[J,de]=k(""),W=P([]),u=P(-1),M=P(null),R=P(null),v=$?.live??!1,j=H(()=>Z(y,A.map(s=>s.id),process.cwd()),[y,A]),N=H(()=>{let s=/^\/(\w*)$/.exec(y);if(!s)return null;let e=be.filter(r=>r.cmd.slice(1).startsWith(s[1].toLowerCase()));return e.length?e:null},[y]),b=s=>de(s),K=oe(s=>{let e=se().slice(0,8),r=Date.now(),i=new AbortController;R.current=i;let d={id:e,you:s,agentId:T,segs:[],live:!0,startedAt:r};S({...d});let L=()=>S({...d,segs:d.segs.map(C=>({...C}))});(async()=>{try{let C=await X(n,T,s,{channel:f,chatId:E,signal:i.signal,onText:l=>{let I=d.segs[d.segs.length-1];I&&I.type==="text"?I.content=(I.content??"")+l:d.segs.push({type:"text",content:l}),L()},onThinking:l=>{d.thinking=(d.thinking??"")+l,L()},onTool:l=>{l.status==="start"&&l.name?(d.segs.push({type:"tool",name:l.name,arg:l.arg}),L()):l.status==="result"&&l.error&&(d.segs.push({type:"tool",name:l.name??"tool",error:!0}),L())}});U(l=>[...l,{...d,live:!1,elapsedMs:Date.now()-r,outTokens:C.usage?.outputTokens,error:C.error}]),S(null)}catch(C){let l=i.signal.aborted||C?.name==="AbortError";U(I=>[...I,{...d,live:!1,elapsedMs:Date.now()-r,interrupted:l,error:l?void 0:C?.message||String(C)}]),S(null)}finally{R.current===i&&(R.current=null)}})()},[n,T,f,E]),me=oe(s=>{let e=s.trim();if(e){if(W.current.push(e),u.current=-1,e==="/exit"||e==="/quit"){w();return}if(e==="/help"){b(ie);return}if(e==="/who"){b(`@${T} \xB7 chatId=${E} \xB7 channel=${f}`);return}if(e==="/clear"){let r=`chat-cli:${se().slice(0,12)}`;ue(r),U([]),b(`fresh chatId=${r} (warm process dropped)`);return}if(e==="/agents"){Q(n).then(r=>{ae(r),b(r.map(i=>i.id===T?`[${i.id}]`:i.id).join(" "))}).catch(r=>b(`failed to list agents: ${r?.message||r}`));return}if(e.startsWith("/agent ")){let r=e.slice(7).replace(/^@/,"").trim();if(!r){b("usage: /agent <id>");return}if(!A.find(i=>i.id===r)){b(`unknown agent: @${r}`);return}le(r),b(`now talking to @${r}`);return}if(e.startsWith("/")){b(`unknown command \xB7 ${ie}`);return}K(e)}},[n,T,A,f,E,w,K]);xe((s,e)=>{if(e.ctrl&&s==="c"){w();return}if(e.escape){if(v&&R.current){R.current.abort();return}w();return}if(e.tab){if(N){x(N[0].cmd+" ");return}let i=_(y,M,A.map(d=>d.id),process.cwd());i!==null&&(x(i),u.current=-1);return}if(e.upArrow){let i=W.current;if(!i.length)return;u.current=u.current<0?i.length-1:Math.max(0,u.current-1),x(i[u.current]??"");return}if(e.downArrow){let i=W.current;if(u.current<0)return;if(u.current=u.current+1,u.current>=i.length){u.current=-1,x("");return}x(i[u.current]??"");return}if(e.backspace||e.delete){M.current=null,x(i=>i.slice(0,-1));return}if(e.ctrl||e.meta)return;let r=ee(s??"",e.return,y);switch(r.kind){case"paste":M.current=null,x(i=>i+r.text),u.current=-1;return;case"type":M.current=null,x(i=>i+r.text),u.current=-1;return;case"continue":x(r.buffer);return;case"submit":if(v)return;M.current=null,x(""),me(r.text);return;case"none":return}});let G=v?m.working:m.accent,ge=y.length?y.split(`
|
|
2
2
|
`):[""],B=A.find(s=>s.id===T),D=ne(B?.tier??"",B?.model),pe=O?.rows||24,fe=z.slice(-40);return o(g,{flexDirection:"column",height:pe,children:[o(g,{flexDirection:"column",flexGrow:1,justifyContent:"flex-end",overflow:"hidden",children:[z.length===0&&!$?o(g,{flexDirection:"column",children:[o(t,{children:[c(t,{color:m.accent,bold:!0,children:q}),c(t,{dimColor:!0,children:" \u2014 chat with "}),o(t,{color:m.accent,children:["@",T]}),c(t,{dimColor:!0,children:" via "}),o(t,{color:D.color,bold:!0,children:[D.glyph," ",D.label]}),B?.model?o(t,{dimColor:!0,children:[" (",B.model,")"]}):null]}),c(t,{dimColor:!0,children:"streaming \xB7 markdown \xB7 tools \xB7 type a message, or / for commands"})]}):null,fe.map(s=>c(ce,{turn:s,width:V},s.id)),$?c(ce,{turn:$,width:V}):null]}),N?c(g,{flexDirection:"column",marginTop:1,children:N.map((s,e)=>o(g,{children:[c(t,{color:e===0?m.accent:m.muted,bold:e===0,children:s.cmd.padEnd(10)}),c(t,{dimColor:!0,children:s.desc})]},s.cmd))}):j?o(g,{marginTop:1,children:[c(t,{color:m.accent,children:"\u21B9 "}),j.items.map((s,e)=>o(t,{color:e===0?m.accent:m.muted,children:[s,e<j.items.length-1?" ":""]},e))]}):J?c(g,{marginTop:1,children:c(t,{dimColor:!0,children:J})}):null,c(g,{borderStyle:"single",borderColor:G,borderLeft:!1,borderRight:!1,borderDimColor:!v,width:"100%",children:c(g,{flexDirection:"column",width:"100%",children:ge.map((s,e,r)=>o(g,{children:[c(t,{color:G,bold:!0,children:e===0?"\u203A ":" "}),c(t,{children:s}),e===r.length-1&&!v?c(t,{color:G,children:"\u258F"}):null]},e))})}),o(g,{justifyContent:"space-between",children:[o(t,{children:[c(t,{color:m.accent,bold:!0,children:q}),o(t,{dimColor:!0,children:[" \xB7 @",T," \xB7 "]}),o(t,{color:D.color,bold:!0,children:[D.glyph," ",D.label]}),B?.model?o(t,{dimColor:!0,children:[" ",re(B.model)]}):null]}),o(t,{dimColor:!0,children:["\u21B5 send / cmds @ mention esc ",v?"stop":"exit"]})]})]})}function ce({turn:n,width:p}){let f=H(()=>ye(n,p),[n,p]),a=n.segs.some(h=>h.type==="text"&&h.content);return o(g,{flexDirection:"column",marginBottom:1,children:[o(t,{color:m.accent,bold:!0,children:[F," you"]}),n.you.split(`
|
|
3
3
|
`).map((h,w)=>o(t,{color:"white",children:[" ",h]},`u${w}`)),o(t,{color:m.agent,bold:!0,children:[F," ",n.agentId??"agent"]}),n.thinking?c(g,{flexDirection:"column",children:we(n.thinking,n.live?3:2).map((h,w)=>o(t,{dimColor:!0,italic:!0,children:[" ",h]},`th${w}`))}):null,f,n.live?c(te,{startedAt:n.startedAt,phase:a?"responding":"thinking",hint:"esc to interrupt"}):n.interrupted?o(t,{dimColor:!0,children:[" \u2298 interrupted \xB7 ",(n.elapsedMs/1e3).toFixed(1),"s"]}):n.error?o(t,{color:"red",children:[" \u2717 ",n.error]}):n.elapsedMs!=null?o(t,{dimColor:!0,children:[" \xB7 ",(n.elapsedMs/1e3).toFixed(1),"s",n.outTokens!=null?`, ${n.outTokens} tok`:""]}):null]})}function we(n,p){return n.split(`
|
|
4
4
|
`).filter(a=>a.trim()).slice(-p)}function ye(n,p){let f=[];return n.segs.forEach((a,h)=>{a.type==="tool"?f.push(o(t,{color:a.error?"red":"green",children:[" ","\u25CF ",o(t,{bold:!a.error,dimColor:a.error,children:[a.name,a.arg?o(t,{dimColor:!0,children:["(",a.arg,")"]}):null,a.error?" failed":""]})]},h)):a.content&&f.push(c(t,{children:Ce(Y(a.content,p,{balance:n.live}))},h))}),f}function Ce(n){return n.split(`
|
|
5
5
|
`).map(p=>p.length?" "+p:p).join(`
|
|
6
6
|
`)}export{Pe as ChatApp};
|
|
7
|
-
//# sourceMappingURL=ChatApp-
|
|
7
|
+
//# sourceMappingURL=ChatApp-VKWQN5GM.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{A as a,B as b,C as c}from"./chunk-
|
|
2
|
-
//# sourceMappingURL=agent-
|
|
1
|
+
import{A as a,B as b,C as c}from"./chunk-K62U5EJJ.js";import"./chunk-SFQUP3BP.js";import"./chunk-QOYAX2FT.js";import"./chunk-SZJMS7K4.js";import"./chunk-7I7IEX4S.js";import"./chunk-VDMYBPYB.js";import"./chunk-DSYKYZMT.js";import"./chunk-QIJLFPWE.js";export{a as createAgentContext,b as generate,c as generateStream};
|
|
2
|
+
//# sourceMappingURL=agent-MJKLI2L5.js.map
|