agentix-cli 0.27.0 → 0.28.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/README.md +67 -118
- package/apps/mac-helper/Sources/AgentXHelper/AXTree.swift +159 -0
- package/apps/mac-helper/Sources/AgentXHelper/Brand.swift +63 -0
- package/apps/mac-helper/Sources/AgentXHelper/Focus.swift +97 -0
- package/apps/mac-helper/Sources/AgentXHelper/HUD.swift +179 -0
- package/apps/mac-helper/Sources/AgentXHelper/OCR.swift +117 -0
- package/apps/mac-helper/Sources/AgentXHelper/Pointer.swift +309 -0
- package/apps/mac-helper/Sources/AgentXHelper/Screens.swift +67 -0
- package/apps/mac-helper/Sources/AgentXHelper/Typer.swift +185 -0
- package/apps/mac-helper/Sources/AgentXHelper/Vision.swift +201 -0
- package/apps/mac-helper/Sources/AgentXHelper/main.swift +265 -0
- package/apps/mac-helper/build.sh +39 -0
- package/apps/mac-voice/Resources/Info.plist +18 -0
- package/apps/mac-voice/Sources/AgentXVoice/AgentClient.swift +108 -0
- package/apps/mac-voice/Sources/AgentXVoice/Brand.swift +123 -0
- package/apps/mac-voice/Sources/AgentXVoice/Config.swift +68 -0
- package/apps/mac-voice/Sources/AgentXVoice/Hold.swift +74 -0
- package/apps/mac-voice/Sources/AgentXVoice/Hotkey.swift +76 -0
- package/apps/mac-voice/Sources/AgentXVoice/Markdown.swift +140 -0
- package/apps/mac-voice/Sources/AgentXVoice/Panel.swift +275 -0
- package/apps/mac-voice/Sources/AgentXVoice/Progress.swift +147 -0
- package/apps/mac-voice/Sources/AgentXVoice/Recorder.swift +139 -0
- package/apps/mac-voice/Sources/AgentXVoice/ResultCard.swift +162 -0
- package/apps/mac-voice/Sources/AgentXVoice/SmartPaste.swift +72 -0
- package/apps/mac-voice/Sources/AgentXVoice/Speech.swift +192 -0
- package/apps/mac-voice/Sources/AgentXVoice/StepLabel.swift +159 -0
- package/apps/mac-voice/Sources/AgentXVoice/main.swift +380 -0
- package/apps/mac-voice/build.sh +32 -0
- package/dist/{App-U52IPUK2.js → App-CKPH3HHF.js} +2 -2
- package/dist/{ChatApp-XP4OMHPL.js → ChatApp-MX7IG4K2.js} +2 -2
- package/dist/{agent-V2CA4TXD.js → agent-2U574KBZ.js} +2 -2
- package/dist/{board-dashboard-TTMAYRLG.js → board-dashboard-QR7CVTKA.js} +8 -10
- package/dist/board-dashboard-QR7CVTKA.js.map +1 -0
- package/dist/{chunk-UFAGYX73.js → chunk-3LYM22IV.js} +8 -8
- package/dist/chunk-3LYM22IV.js.map +1 -0
- package/dist/{chunk-VTE7L6LO.js → chunk-5IA2CYHN.js} +2 -2
- package/dist/{chunk-GDWJUGB5.js → chunk-64CAIB3C.js} +2 -2
- package/dist/{chunk-WZ72HX5C.js → chunk-E3UJHQGN.js} +145 -138
- package/dist/chunk-E3UJHQGN.js.map +1 -0
- package/dist/{chunk-IB3F7VBE.js → chunk-G2HMRDUQ.js} +3 -3
- package/dist/chunk-G2HMRDUQ.js.map +1 -0
- package/dist/{chunk-IBUSGCLQ.js → chunk-H2QQU2XV.js} +2 -2
- package/dist/{chunk-BPIRO4YD.js → chunk-MHIMGM4G.js} +2 -2
- package/dist/{chunk-CJQQIRPD.js → chunk-P5ZBBEPL.js} +7 -7
- package/dist/{chunk-CJQQIRPD.js.map → chunk-P5ZBBEPL.js.map} +1 -1
- package/dist/{chunk-L2IZPR6P.js → chunk-QHTQPBHU.js} +2 -2
- package/dist/chunk-S2MNL5V6.js +2 -0
- package/dist/chunk-S2MNL5V6.js.map +1 -0
- package/dist/{chunk-2T5W3ETS.js → chunk-SCTIUFL4.js} +2 -2
- package/dist/{chunk-OPHIXXGI.js → chunk-ULLAE6LE.js} +52 -52
- package/dist/chunk-ULLAE6LE.js.map +1 -0
- package/dist/{chunk-LNRB5MCU.js → chunk-VIH7ARPF.js} +151 -134
- package/dist/chunk-VIH7ARPF.js.map +1 -0
- package/dist/{chunk-DCDQZR7I.js → chunk-YLBERT7N.js} +6 -6
- package/dist/chunk-YLBERT7N.js.map +1 -0
- package/dist/{chunk-AX2PWSRT.js → chunk-YLGIZRHV.js} +2 -2
- package/dist/{chunk-FIEJDZVC.js → chunk-ZMYGXHPT.js} +2 -2
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/client-ZLSXGPTV.js +2 -0
- package/dist/config-HMW3EBZL.js +2 -0
- package/dist/config-mutator-4SZ5YEVD.js +2 -0
- package/dist/doctor-LRYV3QLK.js +2 -0
- package/dist/index.d.ts +37 -12
- package/dist/index.js +12 -19
- package/dist/index.js.map +1 -1
- package/dist/program-GSZ5AYAM.js +2 -0
- package/dist/query-SQDHE4HV.js +2 -0
- package/dist/registry-LE6FWYZC.js +2 -0
- package/dist/{review-candidates-AEUXRQQC.js → review-candidates-RQU5KNZM.js} +2 -2
- package/dist/routing-ILERJXB6.js +2 -0
- package/dist/scheduler-BFM3EINS.js +2 -0
- package/dist/{seat-VZ7AVO27.js → seat-7WM7II2B.js} +2 -2
- package/dist/{select-F5I5PO2O.js → select-X4KVMOP5.js} +2 -2
- package/dist/wiki-2AUFSIKO.js +2 -0
- package/dist/wiki-2AUFSIKO.js.map +1 -0
- package/package.json +14 -3
- package/scripts/postinstall.mjs +80 -0
- package/dist/board-dashboard-TTMAYRLG.js.map +0 -1
- package/dist/chunk-DCDQZR7I.js.map +0 -1
- package/dist/chunk-IB3F7VBE.js.map +0 -1
- package/dist/chunk-LNRB5MCU.js.map +0 -1
- package/dist/chunk-OPHIXXGI.js.map +0 -1
- package/dist/chunk-UFAGYX73.js.map +0 -1
- package/dist/chunk-WZ72HX5C.js.map +0 -1
- package/dist/client-PX25MD47.js +0 -2
- package/dist/config-HL5F36V4.js +0 -2
- package/dist/config-mutator-HBX53OMJ.js +0 -2
- package/dist/doctor-PBAH6XL3.js +0 -2
- package/dist/program-YGXVCV2Y.js +0 -2
- package/dist/query-ZKTFJMNE.js +0 -2
- package/dist/registry-PSZDPFZK.js +0 -2
- package/dist/routing-4PV66BK6.js +0 -2
- package/dist/routing-4PV66BK6.js.map +0 -1
- package/dist/scheduler-GZZAN6ZG.js +0 -2
- package/dist/wiki-N5SMP3DT.js +0 -2
- /package/dist/{App-U52IPUK2.js.map → App-CKPH3HHF.js.map} +0 -0
- /package/dist/{ChatApp-XP4OMHPL.js.map → ChatApp-MX7IG4K2.js.map} +0 -0
- /package/dist/{agent-V2CA4TXD.js.map → agent-2U574KBZ.js.map} +0 -0
- /package/dist/{chunk-VTE7L6LO.js.map → chunk-5IA2CYHN.js.map} +0 -0
- /package/dist/{chunk-GDWJUGB5.js.map → chunk-64CAIB3C.js.map} +0 -0
- /package/dist/{chunk-IBUSGCLQ.js.map → chunk-H2QQU2XV.js.map} +0 -0
- /package/dist/{chunk-BPIRO4YD.js.map → chunk-MHIMGM4G.js.map} +0 -0
- /package/dist/{chunk-L2IZPR6P.js.map → chunk-QHTQPBHU.js.map} +0 -0
- /package/dist/{chunk-2T5W3ETS.js.map → chunk-SCTIUFL4.js.map} +0 -0
- /package/dist/{chunk-AX2PWSRT.js.map → chunk-YLGIZRHV.js.map} +0 -0
- /package/dist/{chunk-FIEJDZVC.js.map → chunk-ZMYGXHPT.js.map} +0 -0
- /package/dist/{client-PX25MD47.js.map → client-ZLSXGPTV.js.map} +0 -0
- /package/dist/{config-HL5F36V4.js.map → config-HMW3EBZL.js.map} +0 -0
- /package/dist/{config-mutator-HBX53OMJ.js.map → config-mutator-4SZ5YEVD.js.map} +0 -0
- /package/dist/{doctor-PBAH6XL3.js.map → doctor-LRYV3QLK.js.map} +0 -0
- /package/dist/{program-YGXVCV2Y.js.map → program-GSZ5AYAM.js.map} +0 -0
- /package/dist/{query-ZKTFJMNE.js.map → query-SQDHE4HV.js.map} +0 -0
- /package/dist/{registry-PSZDPFZK.js.map → registry-LE6FWYZC.js.map} +0 -0
- /package/dist/{review-candidates-AEUXRQQC.js.map → review-candidates-RQU5KNZM.js.map} +0 -0
- /package/dist/{scheduler-GZZAN6ZG.js.map → routing-ILERJXB6.js.map} +0 -0
- /package/dist/{seat-VZ7AVO27.js.map → scheduler-BFM3EINS.js.map} +0 -0
- /package/dist/{wiki-N5SMP3DT.js.map → seat-7WM7II2B.js.map} +0 -0
- /package/dist/{select-F5I5PO2O.js.map → select-X4KVMOP5.js.map} +0 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import Carbon.HIToolbox
|
|
3
|
+
|
|
4
|
+
/// AgentX Voice — push-to-talk widget over the daemon's /ask endpoint.
|
|
5
|
+
///
|
|
6
|
+
/// Hold ⌥Space, speak, release. Transcribe (ElevenLabs, falling back to
|
|
7
|
+
/// on-device mlx-whisper), POST to /ask, speak the answer (ElevenLabs,
|
|
8
|
+
/// falling back to `say`).
|
|
9
|
+
///
|
|
10
|
+
/// Deliberately has NO computer-use capability in this slice. The point is
|
|
11
|
+
/// to find out whether talking to an agent this way is actually pleasant
|
|
12
|
+
/// before building the part that can click things.
|
|
13
|
+
/// Main-actor isolated in full. An NSApplicationDelegate touches AppKit in
|
|
14
|
+
/// every method, and the crash that prompted this was UI state reached from
|
|
15
|
+
/// a background queue. Isolating the whole class turns that from a runtime
|
|
16
|
+
/// heap corruption into a compile error.
|
|
17
|
+
@MainActor
|
|
18
|
+
final class App: NSObject, NSApplicationDelegate {
|
|
19
|
+
private let panel = Panel()
|
|
20
|
+
private let card = ResultCard()
|
|
21
|
+
private let recorder = Recorder()
|
|
22
|
+
private var hotkey: Hotkey?
|
|
23
|
+
private var pasteHotkey: Hotkey?
|
|
24
|
+
private var busy = false
|
|
25
|
+
private var progress: Progress?
|
|
26
|
+
private var ticker: Timer?
|
|
27
|
+
private var startedAt = Date()
|
|
28
|
+
private var lastStep = "Thinking…"
|
|
29
|
+
private var spokenSteps = Set<String>()
|
|
30
|
+
private var lastSpokeAt = Date.distantPast
|
|
31
|
+
|
|
32
|
+
/// ⌘⌥V. Everything except the hotkey lives in `agentx paste`.
|
|
33
|
+
private func smartPaste() {
|
|
34
|
+
guard !busy else { return }
|
|
35
|
+
busy = true
|
|
36
|
+
panel.render(.working("Pasting…", 0))
|
|
37
|
+
SmartPaste.run { [weak self] result in
|
|
38
|
+
guard let self else { return }
|
|
39
|
+
self.busy = false
|
|
40
|
+
guard let result else {
|
|
41
|
+
self.panel.render(.error("Smart paste unavailable"))
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
// Quiet when nothing changed: "pasted as copied" is the common
|
|
45
|
+
// case and does not deserve an announcement.
|
|
46
|
+
if let what = SmartPaste.summary(result) {
|
|
47
|
+
self.panel.render(.working(what, 0))
|
|
48
|
+
}
|
|
49
|
+
self.panel.render(.idle)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Only one widget, however it was started.
|
|
54
|
+
///
|
|
55
|
+
/// Two pills were sitting on screen at once: the installed copy in
|
|
56
|
+
/// /Applications and a freshly built one from the source tree. Same
|
|
57
|
+
/// bundle identifier, different paths, so macOS is happy to run both —
|
|
58
|
+
/// and the older one keeps rendering an older UI, which looks exactly
|
|
59
|
+
/// like a bug in the new build.
|
|
60
|
+
///
|
|
61
|
+
/// The NEWEST wins. After an upgrade or a rebuild the thing you just
|
|
62
|
+
/// made is the one you want; a stale copy showing yesterday's
|
|
63
|
+
/// behaviour has no claim to the screen.
|
|
64
|
+
private func retireOlderInstances() {
|
|
65
|
+
let me = ProcessInfo.processInfo.processIdentifier
|
|
66
|
+
let mine = Bundle.main.bundleIdentifier
|
|
67
|
+
for app in NSWorkspace.shared.runningApplications {
|
|
68
|
+
guard app.bundleIdentifier == mine,
|
|
69
|
+
app.processIdentifier != me else { continue }
|
|
70
|
+
Log.info("retiring an older instance (pid \(app.processIdentifier))")
|
|
71
|
+
if !app.terminate() { app.forceTerminate() }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func applicationDidFinishLaunching(_ note: Notification) {
|
|
76
|
+
NSApp.setActivationPolicy(.accessory)
|
|
77
|
+
retireOlderInstances()
|
|
78
|
+
panel.orderFrontRegardless()
|
|
79
|
+
|
|
80
|
+
hotkey = Hotkey(
|
|
81
|
+
id: 1,
|
|
82
|
+
onPress: { [weak self] in self?.startListening() },
|
|
83
|
+
onRelease: { [weak self] in self?.stopAndSend() })
|
|
84
|
+
hotkey?.register()
|
|
85
|
+
panel.onClick = { [weak self] in self?.toggleListening() }
|
|
86
|
+
|
|
87
|
+
// Turning the hold OFF is the moment anything that piled up
|
|
88
|
+
// becomes welcome. Without this the queue is a hole rather than a
|
|
89
|
+
// delay — the daemon's watcher only notices SYSTEM Focus ending,
|
|
90
|
+
// and this switch is not that.
|
|
91
|
+
panel.onHoldChanged = { [weak self] nowOn in
|
|
92
|
+
guard let self else { return }
|
|
93
|
+
self.panel.render(.idle)
|
|
94
|
+
if !nowOn { Hold.flushHeld() }
|
|
95
|
+
Log.info(nowOn ? "notifications held" : "notifications delivering")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ⌘⌥V: reshape the clipboard for wherever the caret is, then paste.
|
|
99
|
+
// Fires on RELEASE so the modifiers are up before cmd-V is sent —
|
|
100
|
+
// pressing it while ⌘⌥ are still held produces a different chord
|
|
101
|
+
// in the destination app.
|
|
102
|
+
pasteHotkey = Hotkey(
|
|
103
|
+
id: 2,
|
|
104
|
+
onPress: {},
|
|
105
|
+
onRelease: { [weak self] in self?.smartPaste() })
|
|
106
|
+
pasteHotkey?.register(keyCode: UInt32(kVK_ANSI_V),
|
|
107
|
+
modifiers: UInt32(cmdKey | optionKey))
|
|
108
|
+
|
|
109
|
+
recorder.requestPermission { [weak self] granted in
|
|
110
|
+
Task { @MainActor in
|
|
111
|
+
guard let self else { return }
|
|
112
|
+
if !granted { self.panel.render(.error("Microphone denied")) }
|
|
113
|
+
else { Log.info("ready — hold ⌥Space to talk (agent: \(Config.agentID))") }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- Hands-free listening ---
|
|
119
|
+
//
|
|
120
|
+
// Holding a chord is fine for one question and wrong for a
|
|
121
|
+
// conversation: the reply arrives, you want to say one more thing, and
|
|
122
|
+
// you have to find ⌥Space again while the assistant sits idle. So the
|
|
123
|
+
// microphone reopens by itself after every answer, and closes again on
|
|
124
|
+
// its own if you say nothing.
|
|
125
|
+
//
|
|
126
|
+
// Ending on SILENCE rather than a timer is the whole trick. A fixed
|
|
127
|
+
// window either cuts people off mid-sentence or leaves the mic open
|
|
128
|
+
// staring at them; the only honest signal that a sentence has finished
|
|
129
|
+
// is the room going quiet.
|
|
130
|
+
|
|
131
|
+
/// RMS above which the microphone is hearing a voice rather than a room.
|
|
132
|
+
private let speechLevel: Float = 0.02
|
|
133
|
+
/// Quiet this long after speech ends the turn.
|
|
134
|
+
private let endSilence: TimeInterval = 1.2
|
|
135
|
+
/// How long an unprompted follow-up window waits before giving up.
|
|
136
|
+
private let followUpPatience: TimeInterval = 4.0
|
|
137
|
+
/// How long a clicked session waits for you to start talking.
|
|
138
|
+
private let clickPatience: TimeInterval = 8.0
|
|
139
|
+
|
|
140
|
+
private var listenPoll: Timer?
|
|
141
|
+
private var heardSpeech = false
|
|
142
|
+
private var quietSince: Date?
|
|
143
|
+
private var openedAt = Date()
|
|
144
|
+
private var patience: TimeInterval = 8.0
|
|
145
|
+
/// True when nothing was said and the window should close in silence.
|
|
146
|
+
private var silentClose = false
|
|
147
|
+
|
|
148
|
+
/// Open the microphone with no key held.
|
|
149
|
+
private func listenHandsFree(followUp: Bool) {
|
|
150
|
+
guard !busy, !recorder.isRecording else { return }
|
|
151
|
+
do {
|
|
152
|
+
try recorder.start()
|
|
153
|
+
} catch {
|
|
154
|
+
panel.render(.error(error.localizedDescription))
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
heardSpeech = false
|
|
158
|
+
quietSince = nil
|
|
159
|
+
openedAt = Date()
|
|
160
|
+
patience = followUp ? followUpPatience : clickPatience
|
|
161
|
+
silentClose = followUp
|
|
162
|
+
panel.render(.listening)
|
|
163
|
+
|
|
164
|
+
listenPoll?.invalidate()
|
|
165
|
+
// Timer fires on the run loop but its closure is not main-actor
|
|
166
|
+
// isolated, and pollLevel touches UI. Hopping explicitly keeps it
|
|
167
|
+
// correct under Swift 6 rather than relying on the timer happening
|
|
168
|
+
// to run where we want it.
|
|
169
|
+
listenPoll = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
|
|
170
|
+
Task { @MainActor [weak self] in self?.pollLevel() }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private func pollLevel() {
|
|
175
|
+
guard recorder.isRecording else { stopPolling(); return }
|
|
176
|
+
let level = recorder.level
|
|
177
|
+
|
|
178
|
+
if level > speechLevel {
|
|
179
|
+
heardSpeech = true
|
|
180
|
+
quietSince = nil
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
if heardSpeech {
|
|
184
|
+
// Speech has stopped. Give it a beat before deciding the
|
|
185
|
+
// sentence is over — people pause inside sentences.
|
|
186
|
+
let since = quietSince ?? Date()
|
|
187
|
+
quietSince = since
|
|
188
|
+
if Date().timeIntervalSince(since) >= endSilence {
|
|
189
|
+
stopPolling()
|
|
190
|
+
stopAndSend()
|
|
191
|
+
}
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
// Nothing said yet. Close quietly rather than making the person
|
|
195
|
+
// dismiss a window they did not ask for.
|
|
196
|
+
if Date().timeIntervalSince(openedAt) >= patience {
|
|
197
|
+
stopPolling()
|
|
198
|
+
_ = recorder.stop()
|
|
199
|
+
panel.render(silentClose ? .idle : .error("Didn't catch that"))
|
|
200
|
+
if !silentClose { resetSoon() }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private func stopPolling() {
|
|
205
|
+
listenPoll?.invalidate()
|
|
206
|
+
listenPoll = nil
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// The pill was clicked: start if idle, finish early if already listening.
|
|
210
|
+
private func toggleListening() {
|
|
211
|
+
if recorder.isRecording {
|
|
212
|
+
stopPolling()
|
|
213
|
+
stopAndSend()
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
listenHandsFree(followUp: false)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private func startListening() {
|
|
220
|
+
// A held key overrides any hands-free window that is open, so the
|
|
221
|
+
// two ways of talking never fight over the microphone.
|
|
222
|
+
stopPolling()
|
|
223
|
+
// A turn already in flight must not be interrupted by a stray
|
|
224
|
+
// keypress; the answer is still coming and will be spoken.
|
|
225
|
+
guard !busy, !recorder.isRecording else { return }
|
|
226
|
+
do {
|
|
227
|
+
try recorder.start()
|
|
228
|
+
panel.render(.listening)
|
|
229
|
+
} catch {
|
|
230
|
+
panel.render(.error(error.localizedDescription))
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private func stopAndSend() {
|
|
235
|
+
guard recorder.isRecording else { return }
|
|
236
|
+
guard let wav = recorder.stop() else {
|
|
237
|
+
panel.render(.error("Too short — hold while speaking"))
|
|
238
|
+
resetSoon()
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
busy = true
|
|
242
|
+
panel.render(.thinking)
|
|
243
|
+
|
|
244
|
+
Task { @MainActor in
|
|
245
|
+
do {
|
|
246
|
+
let heard = try await Speech.transcribe(wav: wav)
|
|
247
|
+
guard !heard.isEmpty else {
|
|
248
|
+
panel.render(.error("Didn't catch that"))
|
|
249
|
+
busy = false; resetSoon(); return
|
|
250
|
+
}
|
|
251
|
+
Log.info("heard: \(heard)")
|
|
252
|
+
beginNarration()
|
|
253
|
+
|
|
254
|
+
let answer = try await AgentClient.ask(heard)
|
|
255
|
+
endNarration()
|
|
256
|
+
Log.info("answer: \(answer.text)")
|
|
257
|
+
// Show BEFORE speaking, but only when there is something
|
|
258
|
+
// the speech cannot deliver — a link, an image, or more
|
|
259
|
+
// text than was read aloud. A card that opens on every
|
|
260
|
+
// "Ok." teaches you to ignore it.
|
|
261
|
+
if ResultCard.isWorthShowing(spoken: answer.text, written: answer.written,
|
|
262
|
+
buttons: answer.buttons, imageURL: answer.imageURL) {
|
|
263
|
+
card.show(spoken: answer.text, written: answer.written,
|
|
264
|
+
buttons: answer.buttons, imageURL: answer.imageURL)
|
|
265
|
+
} else {
|
|
266
|
+
card.orderOut(nil)
|
|
267
|
+
}
|
|
268
|
+
// Scroll the sentence being spoken, so it can be read as
|
|
269
|
+
// well as heard — and re-read after, which speech cannot do.
|
|
270
|
+
panel.render(.saying(answer.text))
|
|
271
|
+
await Speech.speak(answer.text)
|
|
272
|
+
panel.render(.idle)
|
|
273
|
+
// Leave the microphone open for a moment. Say nothing and
|
|
274
|
+
// it closes itself; start talking and the conversation
|
|
275
|
+
// simply continues.
|
|
276
|
+
busy = false
|
|
277
|
+
listenHandsFree(followUp: true)
|
|
278
|
+
} catch {
|
|
279
|
+
endNarration()
|
|
280
|
+
Log.warn("turn failed: \(error.localizedDescription)")
|
|
281
|
+
panel.render(.error(short(error.localizedDescription)))
|
|
282
|
+
// Say it aloud too — a voice assistant that fails only in
|
|
283
|
+
// a 230px label has failed silently for anyone not looking.
|
|
284
|
+
await Speech.speak("Sorry, that didn't work.")
|
|
285
|
+
resetSoon()
|
|
286
|
+
}
|
|
287
|
+
busy = false
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// Narrate the wait using what the daemon says it is actually doing.
|
|
292
|
+
///
|
|
293
|
+
/// Two sources, because neither alone is enough: `task:step` events
|
|
294
|
+
/// say what the agent is doing but arrive irregularly, and a 1-second
|
|
295
|
+
/// ticker proves the thing is still alive between them. Together they
|
|
296
|
+
/// answer both "what is it doing" and "is it stuck".
|
|
297
|
+
private func beginNarration() {
|
|
298
|
+
startedAt = Date()
|
|
299
|
+
lastStep = "Thinking…"
|
|
300
|
+
spokenSteps.removeAll()
|
|
301
|
+
lastSpokeAt = Date()
|
|
302
|
+
panel.render(.working(lastStep, 0))
|
|
303
|
+
|
|
304
|
+
// Progress delivers on its own serial queue; hop to main before
|
|
305
|
+
// touching any view.
|
|
306
|
+
progress = Progress(agentID: Config.agentID) { [weak self] step in
|
|
307
|
+
Task { @MainActor in
|
|
308
|
+
guard let self, self.busy else { return }
|
|
309
|
+
self.lastStep = step
|
|
310
|
+
Log.info("step: \(step)")
|
|
311
|
+
self.panel.render(.working(step, Int(Date().timeIntervalSince(self.startedAt))))
|
|
312
|
+
self.speakStepIfDue(step)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
progress?.start()
|
|
316
|
+
|
|
317
|
+
ticker = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
|
318
|
+
Task { @MainActor in
|
|
319
|
+
guard let self, self.busy else { return }
|
|
320
|
+
self.panel.render(.working(self.lastStep, Int(Date().timeIntervalSince(self.startedAt))))
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private func endNarration() {
|
|
326
|
+
ticker?.invalidate(); ticker = nil
|
|
327
|
+
progress?.stop(); progress = nil
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/// Say what it is doing, sparingly.
|
|
331
|
+
///
|
|
332
|
+
/// Silence during a long turn reads as a broken assistant, and a
|
|
333
|
+
/// visible label does not help someone who asked by voice precisely
|
|
334
|
+
/// because they were not looking at the screen. But narrating every
|
|
335
|
+
/// step would talk over itself and be worse than silence — so: nothing
|
|
336
|
+
/// for the first stretch, then at most one short line every 15
|
|
337
|
+
/// seconds, and never the same step twice.
|
|
338
|
+
private func speakStepIfDue(_ step: String) {
|
|
339
|
+
let elapsed = Date().timeIntervalSince(startedAt)
|
|
340
|
+
guard elapsed > 12 else { return }
|
|
341
|
+
guard Date().timeIntervalSince(lastSpokeAt) > 15 else { return }
|
|
342
|
+
|
|
343
|
+
// The step is "Tool: detail". Both halves go to the daemon, which
|
|
344
|
+
// decides whether it is worth saying and in what words. Reading the
|
|
345
|
+
// detail aloud directly is what produced "still working, osascript
|
|
346
|
+
// tell application Calendar".
|
|
347
|
+
let parts = step.split(separator: ":", maxSplits: 1).map(String.init)
|
|
348
|
+
let tool = parts.first?.trimmingCharacters(in: .whitespaces) ?? step
|
|
349
|
+
let detail = parts.count > 1 ? parts[1].trimmingCharacters(in: .whitespaces) : ""
|
|
350
|
+
|
|
351
|
+
// Reserve the slot before awaiting, or two steps arriving together
|
|
352
|
+
// both pass the interval check and talk over each other.
|
|
353
|
+
lastSpokeAt = Date()
|
|
354
|
+
Task { @MainActor in
|
|
355
|
+
guard let phrase = await AgentClient.phrase(
|
|
356
|
+
tool: tool, detail: detail, elapsed: Int(elapsed)) else { return }
|
|
357
|
+
guard !self.spokenSteps.contains(phrase) else { return }
|
|
358
|
+
self.spokenSteps.insert(phrase)
|
|
359
|
+
await Speech.speak(phrase.prefix(1).capitalized + phrase.dropFirst() + ".")
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private func short(_ s: String) -> String {
|
|
364
|
+
s.count > 40 ? String(s.prefix(38)) + "…" : s
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private func resetSoon() {
|
|
368
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
|
369
|
+
MainActor.assumeIsolated {
|
|
370
|
+
guard let self, !self.recorder.isRecording, !self.busy else { return }
|
|
371
|
+
self.panel.render(.idle)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let app = NSApplication.shared
|
|
378
|
+
let delegate = MainActor.assumeIsolated { App() }
|
|
379
|
+
app.delegate = delegate
|
|
380
|
+
app.run()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Build AgentX Voice into a .app bundle.
|
|
3
|
+
#
|
|
4
|
+
# swiftc directly, not SwiftPM: a CommandLineTools-only install ships a
|
|
5
|
+
# broken PackageDescription ManifestAPI, and this target has no
|
|
6
|
+
# dependencies, so the package manager buys nothing here.
|
|
7
|
+
#
|
|
8
|
+
# The .app wrapper is not cosmetic — macOS grants microphone access to
|
|
9
|
+
# bundles with an Info.plist, so a bare executable can never get a TCC
|
|
10
|
+
# prompt and will fail with an inscrutable silence instead.
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
cd "$(dirname "$0")"
|
|
13
|
+
|
|
14
|
+
APP="build/AgentX Voice.app"
|
|
15
|
+
BIN="$APP/Contents/MacOS"
|
|
16
|
+
rm -rf build && mkdir -p "$BIN" "$APP/Contents/Resources"
|
|
17
|
+
|
|
18
|
+
swiftc -O \
|
|
19
|
+
-o "$BIN/AgentXVoice" \
|
|
20
|
+
Sources/AgentXVoice/*.swift \
|
|
21
|
+
-framework AppKit -framework AVFoundation -framework Carbon \
|
|
22
|
+
-target arm64-apple-macosx14.0
|
|
23
|
+
|
|
24
|
+
cp Resources/Info.plist "$APP/Contents/Info.plist"
|
|
25
|
+
|
|
26
|
+
# Ad-hoc signature. Unsigned bundles get a fresh TCC identity on every
|
|
27
|
+
# rebuild, so macOS re-asks for the microphone every single launch.
|
|
28
|
+
codesign --force --sign - --identifier tn.acme.agentx.voice "$APP" 2>/dev/null \
|
|
29
|
+
|| echo "warning: codesign failed; expect repeated microphone prompts"
|
|
30
|
+
|
|
31
|
+
echo "built: $APP"
|
|
32
|
+
echo "run: open \"$APP\" (or ./build/AgentX\\ Voice.app/Contents/MacOS/AgentXVoice for logs)"
|
|
@@ -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-5IA2CYHN.js";import"./chunk-P5ZBBEPL.js";import"./chunk-FIOB3MEX.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-CKPH3HHF.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-5IA2CYHN.js";import"./chunk-P5ZBBEPL.js";import"./chunk-FIOB3MEX.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-MX7IG4K2.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-MHIMGM4G.js";import"./chunk-SFQUP3BP.js";import"./chunk-QOYAX2FT.js";import"./chunk-LCIREYTD.js";import"./chunk-L2WSPY6S.js";import"./chunk-VDMYBPYB.js";import"./chunk-DSYKYZMT.js";import"./chunk-FIOB3MEX.js";export{a as createAgentContext,b as generate,c as generateStream};
|
|
2
|
+
//# sourceMappingURL=agent-2U574KBZ.js.map
|