agentix-cli 0.59.0 → 0.60.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/AgentClient.swift +4 -0
- package/apps/mac-voice/Sources/AgentXVoice/AnswerView.swift +228 -0
- package/apps/mac-voice/Sources/AgentXVoice/Orb.swift +36 -9
- package/apps/mac-voice/Sources/AgentXVoice/Panel.swift +126 -55
- package/apps/mac-voice/Sources/AgentXVoice/PanelAnswer.swift +113 -0
- package/apps/mac-voice/Sources/AgentXVoice/PillPlacement.swift +28 -17
- package/apps/mac-voice/Sources/AgentXVoice/SettingsLook.swift +90 -0
- package/apps/mac-voice/Sources/AgentXVoice/SettingsWindow.swift +10 -3
- package/apps/mac-voice/Sources/AgentXVoice/StatusMenu.swift +9 -0
- package/apps/mac-voice/Sources/AgentXVoice/Surface.swift +105 -0
- package/apps/mac-voice/Sources/AgentXVoice/VoiceSettings.swift +32 -1
- package/apps/mac-voice/Sources/AgentXVoice/main.swift +23 -10
- package/dist/{App-QEJGXZTY.js → App-LQXWKBVY.js} +2 -2
- package/dist/{ChatApp-VKWQN5GM.js → ChatApp-75TL4LKW.js} +2 -2
- package/dist/{agent-MJKLI2L5.js → agent-DH4FMMMY.js} +2 -2
- package/dist/{board-dashboard-2KVT72J7.js → board-dashboard-BVRRDYAG.js} +6 -4
- package/dist/{board-dashboard-2KVT72J7.js.map → board-dashboard-BVRRDYAG.js.map} +1 -1
- package/dist/{chunk-WFPRVZBA.js → chunk-27P5NLTC.js} +2 -2
- package/dist/{chunk-3EVCHRJF.js → chunk-2VQIPJH5.js} +2 -2
- package/dist/{chunk-YRB75GMN.js → chunk-3XVTFNYM.js} +2 -2
- package/dist/{chunk-K62U5EJJ.js → chunk-B3DCYBP4.js} +2 -2
- package/dist/{chunk-EJQOKSKP.js → chunk-CRSXKQLZ.js} +2 -2
- package/dist/{chunk-YCIZ2WHX.js → chunk-DTEQNXK3.js} +2 -2
- package/dist/{chunk-N5GOBLUI.js → chunk-E2BJVHBR.js} +2 -2
- package/dist/{chunk-6DUTIPTY.js → chunk-E2PWCU2N.js} +2 -2
- package/dist/{chunk-2XZGHIVP.js → chunk-JO3PSLWL.js} +5 -5
- package/dist/{chunk-YQU74AOL.js → chunk-JORXRF4E.js} +2 -2
- package/dist/{chunk-DAAWUFO4.js → chunk-LXUG2X26.js} +2 -2
- package/dist/{chunk-JUY77L7C.js → chunk-N5KD3TVN.js} +2 -2
- package/dist/{chunk-5OF3EGCZ.js → chunk-NBGNPKAF.js} +2 -2
- package/dist/{chunk-4ZTV6JV7.js → chunk-PAOV37KQ.js} +2 -2
- package/dist/{chunk-SDBU6747.js → chunk-RYFUFX6F.js} +160 -160
- package/dist/chunk-RYFUFX6F.js.map +1 -0
- package/dist/chunk-S7TCVIET.js +15 -0
- package/dist/chunk-S7TCVIET.js.map +1 -0
- package/dist/{chunk-ILXW3ERG.js → chunk-TM762AXU.js} +159 -153
- package/dist/chunk-TM762AXU.js.map +1 -0
- package/dist/{chunk-WI5R64WO.js → chunk-WBGC7MWX.js} +2 -2
- package/dist/{chunk-6ZV2QFXE.js → chunk-WIBMNQKJ.js} +2 -2
- package/dist/{chunk-L5OCF33B.js → chunk-WOPFHKBF.js} +2 -2
- package/dist/{chunk-OIDMPPV5.js → chunk-YQCOX4R3.js} +2 -2
- package/dist/cli.js +1 -1
- package/dist/client-QVZIZFV4.js +2 -0
- package/dist/config-3XC7EYB7.js +2 -0
- package/dist/config-mutator-HKU63FJM.js +2 -0
- package/dist/doctor-YFA4ZSRH.js +2 -0
- package/dist/index.d.ts +49 -7
- package/dist/index.js +1 -1
- package/dist/program-BOCLU6ES.js +2 -0
- package/dist/{promote-GBR6YSQW.js → promote-UR2GSHKC.js} +2 -2
- package/dist/query-XUP3BMVS.js +2 -0
- package/dist/{registry-P535FNLB.js → registry-PGALUW5L.js} +2 -2
- package/dist/{request-planner-KHTCHF7N.js → request-planner-NHE3IRJN.js} +2 -2
- package/dist/{review-candidates-Y5KCRUNU.js → review-candidates-6GKRSRBQ.js} +2 -2
- package/dist/{routing-RWCCDJ4D.js → routing-AQKHAU3Z.js} +2 -2
- package/dist/schedule-tool-6NMCLTIH.js +2 -0
- package/dist/scheduler-RAYGNDWT.js +2 -0
- package/dist/{seat-PJPKLH6L.js → seat-6FMW2QI4.js} +2 -2
- package/dist/{select-VMTJAX5X.js → select-5VONJMLH.js} +2 -2
- package/dist/{set-enabled-LPXH3ZQP.js → set-enabled-YHD3IJJZ.js} +2 -2
- package/dist/{tool-MWXG44OT.js → tool-MXJGLJZ5.js} +2 -2
- package/dist/wiki-ZQLEYY24.js +2 -0
- package/package.json +1 -1
- package/apps/mac-voice/Sources/AgentXVoice/ResultCard.swift +0 -198
- package/dist/chunk-ILXW3ERG.js.map +0 -1
- package/dist/chunk-IW4XIZAZ.js +0 -15
- package/dist/chunk-IW4XIZAZ.js.map +0 -1
- package/dist/chunk-SDBU6747.js.map +0 -1
- package/dist/client-X2PTYKVX.js +0 -2
- package/dist/config-XB2ORKWS.js +0 -2
- package/dist/config-mutator-KMXR3XDG.js +0 -2
- package/dist/doctor-6BIMNEGU.js +0 -2
- package/dist/program-DHDC6XYB.js +0 -2
- package/dist/query-672RGMXO.js +0 -2
- package/dist/schedule-tool-H46AK53H.js +0 -2
- package/dist/scheduler-3XLWBCX7.js +0 -2
- package/dist/wiki-QHMHVFWU.js +0 -2
- /package/dist/{App-QEJGXZTY.js.map → App-LQXWKBVY.js.map} +0 -0
- /package/dist/{ChatApp-VKWQN5GM.js.map → ChatApp-75TL4LKW.js.map} +0 -0
- /package/dist/{agent-MJKLI2L5.js.map → agent-DH4FMMMY.js.map} +0 -0
- /package/dist/{chunk-WFPRVZBA.js.map → chunk-27P5NLTC.js.map} +0 -0
- /package/dist/{chunk-3EVCHRJF.js.map → chunk-2VQIPJH5.js.map} +0 -0
- /package/dist/{chunk-YRB75GMN.js.map → chunk-3XVTFNYM.js.map} +0 -0
- /package/dist/{chunk-K62U5EJJ.js.map → chunk-B3DCYBP4.js.map} +0 -0
- /package/dist/{chunk-EJQOKSKP.js.map → chunk-CRSXKQLZ.js.map} +0 -0
- /package/dist/{chunk-YCIZ2WHX.js.map → chunk-DTEQNXK3.js.map} +0 -0
- /package/dist/{chunk-N5GOBLUI.js.map → chunk-E2BJVHBR.js.map} +0 -0
- /package/dist/{chunk-6DUTIPTY.js.map → chunk-E2PWCU2N.js.map} +0 -0
- /package/dist/{chunk-2XZGHIVP.js.map → chunk-JO3PSLWL.js.map} +0 -0
- /package/dist/{chunk-YQU74AOL.js.map → chunk-JORXRF4E.js.map} +0 -0
- /package/dist/{chunk-DAAWUFO4.js.map → chunk-LXUG2X26.js.map} +0 -0
- /package/dist/{chunk-JUY77L7C.js.map → chunk-N5KD3TVN.js.map} +0 -0
- /package/dist/{chunk-5OF3EGCZ.js.map → chunk-NBGNPKAF.js.map} +0 -0
- /package/dist/{chunk-4ZTV6JV7.js.map → chunk-PAOV37KQ.js.map} +0 -0
- /package/dist/{chunk-WI5R64WO.js.map → chunk-WBGC7MWX.js.map} +0 -0
- /package/dist/{chunk-6ZV2QFXE.js.map → chunk-WIBMNQKJ.js.map} +0 -0
- /package/dist/{chunk-L5OCF33B.js.map → chunk-WOPFHKBF.js.map} +0 -0
- /package/dist/{chunk-OIDMPPV5.js.map → chunk-YQCOX4R3.js.map} +0 -0
- /package/dist/{client-X2PTYKVX.js.map → client-QVZIZFV4.js.map} +0 -0
- /package/dist/{config-XB2ORKWS.js.map → config-3XC7EYB7.js.map} +0 -0
- /package/dist/{config-mutator-KMXR3XDG.js.map → config-mutator-HKU63FJM.js.map} +0 -0
- /package/dist/{doctor-6BIMNEGU.js.map → doctor-YFA4ZSRH.js.map} +0 -0
- /package/dist/{program-DHDC6XYB.js.map → program-BOCLU6ES.js.map} +0 -0
- /package/dist/{promote-GBR6YSQW.js.map → promote-UR2GSHKC.js.map} +0 -0
- /package/dist/{query-672RGMXO.js.map → query-XUP3BMVS.js.map} +0 -0
- /package/dist/{registry-P535FNLB.js.map → registry-PGALUW5L.js.map} +0 -0
- /package/dist/{request-planner-KHTCHF7N.js.map → request-planner-NHE3IRJN.js.map} +0 -0
- /package/dist/{review-candidates-Y5KCRUNU.js.map → review-candidates-6GKRSRBQ.js.map} +0 -0
- /package/dist/{routing-RWCCDJ4D.js.map → routing-AQKHAU3Z.js.map} +0 -0
- /package/dist/{schedule-tool-H46AK53H.js.map → schedule-tool-6NMCLTIH.js.map} +0 -0
- /package/dist/{scheduler-3XLWBCX7.js.map → scheduler-RAYGNDWT.js.map} +0 -0
- /package/dist/{seat-PJPKLH6L.js.map → seat-6FMW2QI4.js.map} +0 -0
- /package/dist/{select-VMTJAX5X.js.map → select-5VONJMLH.js.map} +0 -0
- /package/dist/{set-enabled-LPXH3ZQP.js.map → set-enabled-YHD3IJJZ.js.map} +0 -0
- /package/dist/{tool-MWXG44OT.js.map → tool-MXJGLJZ5.js.map} +0 -0
- /package/dist/{wiki-QHMHVFWU.js.map → wiki-ZQLEYY24.js.map} +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
|
|
3
|
+
/// The pill growing into its answer, and collapsing back.
|
|
4
|
+
///
|
|
5
|
+
/// One surface, not a second window: the row with the orb stays where it
|
|
6
|
+
/// is and the widget grows up or down from it, whichever has more room.
|
|
7
|
+
/// It collapses on close or Esc, or `cardTimeout` seconds after the answer
|
|
8
|
+
/// has been spoken, and never while the pointer is over it.
|
|
9
|
+
extension Panel {
|
|
10
|
+
/// A gentle spring: a little past the target, then back. Reduce Motion
|
|
11
|
+
/// skips the animation entirely.
|
|
12
|
+
private static let spring = CAMediaTimingFunction(controlPoints: 0.3, 1.12, 0.5, 1)
|
|
13
|
+
|
|
14
|
+
/// Show an answer. The pill grows once it has been measured. A pill
|
|
15
|
+
/// the person dismissed stays dismissed.
|
|
16
|
+
@MainActor
|
|
17
|
+
func showAnswer(spoken: String, written: String?, buttons: [(String, String)], imageURL: String?) {
|
|
18
|
+
guard !dismissed else { return }
|
|
19
|
+
answer.show(spoken: spoken, written: written, buttons: buttons, imageURL: imageURL)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Grow, or resize while grown, to fit an answer `answerHeight` tall.
|
|
23
|
+
@MainActor
|
|
24
|
+
func grow(answerHeight: CGFloat) {
|
|
25
|
+
guard !dismissed else { return }
|
|
26
|
+
let pill = collapsedFrame()
|
|
27
|
+
guard let visible = visibleFrame(for: pill) else { return }
|
|
28
|
+
let height = min(Self.size.height + min(answerHeight, cardMaxHeight), visible.height)
|
|
29
|
+
let spot = PillPlacement.expanded(size: CGSize(width: Self.expandedWidth, height: height),
|
|
30
|
+
pill: pill, visible: visible)
|
|
31
|
+
if !expanded {
|
|
32
|
+
growth = (spot.above, spot.alignRight)
|
|
33
|
+
expanded = true
|
|
34
|
+
// Lay the row on its edge before the first frame, so it does
|
|
35
|
+
// not jump when the growth starts.
|
|
36
|
+
if let content = contentView { layoutContent(content.bounds) }
|
|
37
|
+
}
|
|
38
|
+
separator.isHidden = false
|
|
39
|
+
show()
|
|
40
|
+
animate(to: spot.frame)
|
|
41
|
+
armCollapse()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Back to the pill alone, where it was.
|
|
45
|
+
@MainActor
|
|
46
|
+
func collapse(animated: Bool = true) {
|
|
47
|
+
collapseTimer?.invalidate()
|
|
48
|
+
collapseTimer = nil
|
|
49
|
+
guard expanded else { return }
|
|
50
|
+
var pill = collapsedFrame()
|
|
51
|
+
if let visible = visibleFrame(for: pill) { pill = PillPlacement.inside(pill, visible) }
|
|
52
|
+
expanded = false
|
|
53
|
+
animate(to: pill, animated: animated) { [weak self] in
|
|
54
|
+
guard let self, !self.expanded else { return }
|
|
55
|
+
self.separator.isHidden = true
|
|
56
|
+
self.answer.clear()
|
|
57
|
+
// Idle and not kept on screen: it goes, now the answer has.
|
|
58
|
+
self.render(self.current)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Where the pill is, or will be once collapsed.
|
|
63
|
+
@MainActor
|
|
64
|
+
func collapsedFrame() -> NSRect {
|
|
65
|
+
guard expanded else { return frame }
|
|
66
|
+
return PillPlacement.collapsed(from: frame, size: Self.size, above: growth.above, alignRight: growth.alignRight)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Start the countdown to collapse, when the answer has been spoken
|
|
70
|
+
/// and nobody is reading it. Anything else cancels it.
|
|
71
|
+
@MainActor
|
|
72
|
+
func armCollapse() {
|
|
73
|
+
collapseTimer?.invalidate()
|
|
74
|
+
collapseTimer = nil
|
|
75
|
+
guard expanded, cardTimeout > 0, !hovering, current.orbPhase == .idle else { return }
|
|
76
|
+
collapseTimer = Timer.scheduledTimer(withTimeInterval: cardTimeout, repeats: false) { [weak self] _ in
|
|
77
|
+
MainActor.assumeIsolated { self?.collapse() }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// The visible frame of the screen `rect` overlaps most.
|
|
82
|
+
@MainActor
|
|
83
|
+
private func visibleFrame(for rect: NSRect) -> NSRect? {
|
|
84
|
+
let best = NSScreen.screens.max { a, b in
|
|
85
|
+
let x = a.frame.intersection(rect), y = b.frame.intersection(rect)
|
|
86
|
+
return (x.isNull ? 0 : x.width * x.height) < (y.isNull ? 0 : y.width * y.height)
|
|
87
|
+
}
|
|
88
|
+
return (best ?? NSScreen.main)?.visibleFrame
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@MainActor
|
|
92
|
+
private func animate(to target: NSRect, animated: Bool = true, then done: (() -> Void)? = nil) {
|
|
93
|
+
placing += 1
|
|
94
|
+
let finish = { [weak self] in
|
|
95
|
+
guard let self else { return }
|
|
96
|
+
self.placing -= 1
|
|
97
|
+
self.invalidateShadow()
|
|
98
|
+
done?()
|
|
99
|
+
}
|
|
100
|
+
if !animated || !isVisible || NSWorkspace.shared.accessibilityDisplayShouldReduceMotion {
|
|
101
|
+
setFrame(target, display: true)
|
|
102
|
+
finish()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
NSAnimationContext.runAnimationGroup({ ctx in
|
|
106
|
+
ctx.duration = 0.34
|
|
107
|
+
ctx.timingFunction = Self.spring
|
|
108
|
+
animator().setFrame(target, display: true)
|
|
109
|
+
}, completionHandler: {
|
|
110
|
+
MainActor.assumeIsolated { finish() }
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import CoreGraphics
|
|
2
2
|
|
|
3
|
-
/// Where the pill and
|
|
3
|
+
/// Where the pill goes on screen, and how it grows into its answer, kept free of AppKit so
|
|
4
4
|
/// the tests can check it without a window. Frames are in screen
|
|
5
5
|
/// coordinates, origin bottom-left, as AppKit uses them.
|
|
6
6
|
enum PillPlacement {
|
|
7
7
|
/// Distance from the screen's edges for the default corner.
|
|
8
8
|
static let inset: CGFloat = 24
|
|
9
|
-
/// Space between the pill and the answer card.
|
|
10
|
-
static let cardGap: CGFloat = 8
|
|
11
9
|
|
|
12
10
|
/// Bottom-right of `visible` (a screen's frame minus the menu bar and
|
|
13
11
|
/// Dock), clear of both.
|
|
@@ -38,23 +36,36 @@ enum PillPlacement {
|
|
|
38
36
|
return inside(frame, screen).origin
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
///
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
/// The pill grown into its answer: where the whole widget goes, and
|
|
40
|
+
/// which way it grew.
|
|
41
|
+
struct Expanded: Equatable {
|
|
42
|
+
let frame: CGRect
|
|
43
|
+
/// True: the answer opens above the pill's row. False: below it.
|
|
45
44
|
let above: Bool
|
|
45
|
+
/// True: right edges line up with the pill's. False: left edges.
|
|
46
|
+
let alignRight: Bool
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
///
|
|
49
|
-
///
|
|
50
|
-
///
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
let above =
|
|
55
|
-
let
|
|
56
|
-
let frame = CGRect(x: pill.maxX - size.width
|
|
57
|
-
|
|
49
|
+
/// The pill at `pill` grown to `size`. It grows up when there is at
|
|
50
|
+
/// least as much room above the pill as below, else down, so its row
|
|
51
|
+
/// stays where it was. It grows leftwards, keeping the right edge,
|
|
52
|
+
/// unless that would leave the screen. Too tall for the screen: kept
|
|
53
|
+
/// on it, top-left in view.
|
|
54
|
+
static func expanded(size: CGSize, pill: CGRect, visible: CGRect) -> Expanded {
|
|
55
|
+
let above = visible.maxY - pill.maxY >= pill.minY - visible.minY
|
|
56
|
+
let alignRight = pill.maxX - size.width >= visible.minX
|
|
57
|
+
let frame = CGRect(x: alignRight ? pill.maxX - size.width : pill.minX,
|
|
58
|
+
y: above ? pill.minY : pill.maxY - size.height,
|
|
59
|
+
width: size.width, height: size.height)
|
|
60
|
+
return Expanded(frame: inside(frame, visible), above: above, alignRight: alignRight)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Where the pill goes back to when the widget at `frame` collapses:
|
|
64
|
+
/// its row's end on the side it grew from.
|
|
65
|
+
static func collapsed(from frame: CGRect, size: CGSize, above: Bool, alignRight: Bool) -> CGRect {
|
|
66
|
+
CGRect(x: alignRight ? frame.maxX - size.width : frame.minX,
|
|
67
|
+
y: above ? frame.minY : frame.maxY - size.height,
|
|
68
|
+
width: size.width, height: size.height)
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
/// `frame` moved the least distance that puts it inside `bounds`. A
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
/// The settings window's look controls (#211): each agent's orb palette,
|
|
5
|
+
/// and how long and how tall the answer in the pill is. Both are saved in
|
|
6
|
+
/// agentx.json through the daemon, like everything else in the window.
|
|
7
|
+
|
|
8
|
+
/// A palette as a small gradient disc, for menus that only show images.
|
|
9
|
+
enum PaletteSwatch {
|
|
10
|
+
static func image(_ hexes: [String], size: CGFloat = 14) -> NSImage {
|
|
11
|
+
let colors = hexes.compactMap(OrbMath.parseHex).map { NSColor(srgbRed: $0.r, green: $0.g, blue: $0.b, alpha: 1) }
|
|
12
|
+
return NSImage(size: NSSize(width: size, height: size), flipped: false) { rect in
|
|
13
|
+
let disc = NSBezierPath(ovalIn: rect.insetBy(dx: 0.5, dy: 0.5))
|
|
14
|
+
NSGradient(colors: colors.isEmpty ? [.gray] : colors)?.draw(in: disc, angle: -45)
|
|
15
|
+
NSColor.separatorColor.setStroke()
|
|
16
|
+
disc.lineWidth = 0.5
|
|
17
|
+
disc.stroke()
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// "Orb palette": one of the nature palettes, or the one nearest the
|
|
24
|
+
/// agent's colour.
|
|
25
|
+
struct PalettePicker: View {
|
|
26
|
+
@Binding var agent: VoiceSettings.Agent
|
|
27
|
+
let palettes: [VoiceSettings.Palette]
|
|
28
|
+
|
|
29
|
+
private func palette(_ id: String?) -> VoiceSettings.Palette? { palettes.first { $0.id == id } }
|
|
30
|
+
|
|
31
|
+
var body: some View {
|
|
32
|
+
let shown = palette(agent.palette ?? agent.paletteDefault)
|
|
33
|
+
HStack {
|
|
34
|
+
Picker("Orb palette", selection: $agent.palette) {
|
|
35
|
+
if let fallback = palette(agent.paletteDefault) {
|
|
36
|
+
Label { Text("Match the colour: \(fallback.label)") } icon: { Image(nsImage: PaletteSwatch.image(fallback.colors)) }
|
|
37
|
+
.tag(String?.none)
|
|
38
|
+
} else {
|
|
39
|
+
Text("Match the colour").tag(String?.none)
|
|
40
|
+
}
|
|
41
|
+
Divider()
|
|
42
|
+
ForEach(palettes) { p in
|
|
43
|
+
Label { Text(p.label) } icon: { Image(nsImage: PaletteSwatch.image(p.colors)) }
|
|
44
|
+
.tag(String?.some(p.id))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if let shown {
|
|
48
|
+
Image(nsImage: PaletteSwatch.image(shown.colors, size: 20))
|
|
49
|
+
.accessibilityHidden(true)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// "Answer in the pill": how long it stays open once spoken, and how tall
|
|
56
|
+
/// it grows before it scrolls.
|
|
57
|
+
struct AnswerCardSection: View {
|
|
58
|
+
@Binding var card: VoiceSettings.Card
|
|
59
|
+
|
|
60
|
+
/// The usual choices, plus whatever agentx.json already says.
|
|
61
|
+
private var timeouts: [Double] {
|
|
62
|
+
let usual: [Double] = [10, 20, 30, 60, 120, 300]
|
|
63
|
+
return usual.contains(card.timeout) || card.timeout == 0 ? usual : (usual + [card.timeout]).sorted()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static func label(_ s: Double) -> String {
|
|
67
|
+
if s < 60 || s.truncatingRemainder(dividingBy: 60) != 0 { return "\(Int(s)) seconds" }
|
|
68
|
+
return s == 60 ? "1 minute" : "\(Int(s / 60)) minutes"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var body: some View {
|
|
72
|
+
Section {
|
|
73
|
+
Picker("Keep the answer open", selection: $card.timeout) {
|
|
74
|
+
ForEach(timeouts, id: \.self) { s in
|
|
75
|
+
Text(Self.label(s)).tag(s)
|
|
76
|
+
}
|
|
77
|
+
Divider()
|
|
78
|
+
Text("Until I close it").tag(0.0)
|
|
79
|
+
}
|
|
80
|
+
Stepper(value: $card.maxHeight, in: 120...800, step: 40) {
|
|
81
|
+
LabeledContent("Tallest answer", value: "\(Int(card.maxHeight)) points")
|
|
82
|
+
}
|
|
83
|
+
} header: {
|
|
84
|
+
Text("Answer in the pill")
|
|
85
|
+
} footer: {
|
|
86
|
+
Text("When an answer has a link, a picture or more to read than was spoken, the pill grows to show it. It closes this long after the answer has been spoken, but not while the pointer is over it. A longer answer scrolls.")
|
|
87
|
+
.font(.caption).foregroundStyle(.secondary)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -326,16 +326,19 @@ private struct AgentForm: View {
|
|
|
326
326
|
get: { Color(hex: agent.color) },
|
|
327
327
|
set: { agent.color = $0.hex; agent.colorSet = true })
|
|
328
328
|
let footer = agent.colorSet
|
|
329
|
-
? "Hold the shortcut and speak to ask this agent without changing who is ticked in the menu. The colour also paints this agent's pointer."
|
|
330
|
-
: "Hold the shortcut and speak to ask this agent without changing who is ticked in the menu. The colour comes from the agent's id until you pick one."
|
|
329
|
+
? "Hold the shortcut and speak to ask this agent without changing who is ticked in the menu. The colour also paints this agent's pointer, and picks the orb's palette unless you choose one."
|
|
330
|
+
: "Hold the shortcut and speak to ask this agent without changing who is ticked in the menu. The colour comes from the agent's id until you pick one, and picks the orb's palette unless you choose one."
|
|
331
331
|
return Section {
|
|
332
332
|
HotkeyField(title: "Ask with shortcut", value: $agent.voice.hotkey, optional: true, model: model, id: "agent.\(agent.id)")
|
|
333
333
|
HStack {
|
|
334
334
|
ColorPicker("Orb colour", selection: color, supportsOpacity: false)
|
|
335
335
|
Button("Use default") { agent.colorSet = false }.disabled(!agent.colorSet)
|
|
336
336
|
}
|
|
337
|
+
if let palettes = settings.palettes, !palettes.isEmpty {
|
|
338
|
+
PalettePicker(agent: $agent, palettes: palettes)
|
|
339
|
+
}
|
|
337
340
|
} header: {
|
|
338
|
-
Text("Shortcut and
|
|
341
|
+
Text("Shortcut and look")
|
|
339
342
|
} footer: {
|
|
340
343
|
Text(footer).font(.caption).foregroundStyle(.secondary)
|
|
341
344
|
}
|
|
@@ -385,6 +388,10 @@ private struct GeneralTab: View {
|
|
|
385
388
|
Text("Automatic uses ElevenLabs when a key is set, and the engine on this Mac otherwise. Parakeet has no Arabic; until its model has downloaded, Whisper answers instead. Voice detection ends a turn when you stop talking, not when the room goes quiet. The default provider is for agents that don't choose their own. All apply to the next question.")
|
|
386
389
|
.font(.caption).foregroundStyle(.secondary)
|
|
387
390
|
}
|
|
391
|
+
if draft.general.card != nil {
|
|
392
|
+
AnswerCardSection(card: Binding(get: { model.draft?.general.card ?? .standard },
|
|
393
|
+
set: { model.draft?.general.card = $0 }))
|
|
394
|
+
}
|
|
388
395
|
Section {
|
|
389
396
|
Toggle("Launch at login", isOn: Binding(get: { model.launchAtLogin }, set: { model.setLaunchAtLogin($0) }))
|
|
390
397
|
.disabled(LoginItem.managedByInstaller)
|
|
@@ -70,6 +70,15 @@ final class StatusMenu: NSObject, NSMenuDelegate {
|
|
|
70
70
|
return NSColor(srgbRed: c.r, green: c.g, blue: c.b, alpha: 1)
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/// The agent's orb palette, five colours deep to light, or nil when
|
|
74
|
+
/// the daemon sent none.
|
|
75
|
+
func palette(of id: String) -> [NSColor]? {
|
|
76
|
+
guard case .loaded(let agents) = roster,
|
|
77
|
+
let hexes = agents.first(where: { $0.id == id })?.palette?.colors, hexes.count == 5 else { return nil }
|
|
78
|
+
let colors = hexes.compactMap(OrbMath.parseHex).map { NSColor(srgbRed: $0.r, green: $0.g, blue: $0.b, alpha: 1) }
|
|
79
|
+
return colors.count == 5 ? colors : nil
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
/// Open the menu from the keyboard.
|
|
74
83
|
func open() { item.button?.performClick(nil) }
|
|
75
84
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
|
|
3
|
+
/// The pill's background, drawn the way macOS draws its own floating
|
|
4
|
+
/// surfaces: popover vibrancy, a continuous-corner shape, a hairline
|
|
5
|
+
/// border, and the window's soft shadow around it.
|
|
6
|
+
///
|
|
7
|
+
/// It follows light and dark, Increase Contrast and Reduce Transparency as
|
|
8
|
+
/// they change, since layer colours are fixed values that do not update
|
|
9
|
+
/// themselves.
|
|
10
|
+
final class Surface: NSVisualEffectView {
|
|
11
|
+
/// An opaque-ish wash over the blur, and it is not a style preference.
|
|
12
|
+
///
|
|
13
|
+
/// `blendingMode = .behindWindow` composites whatever is behind the
|
|
14
|
+
/// pill, so an editor's panel dividers came through it as a faint
|
|
15
|
+
/// rectangle that read as a border the widget was drawing. The wash
|
|
16
|
+
/// keeps enough blur to feel native while making the pill read as one
|
|
17
|
+
/// solid object wherever it is parked.
|
|
18
|
+
private let wash = NSView()
|
|
19
|
+
|
|
20
|
+
/// Lays out the pill's row and its answer on every size change. Set by
|
|
21
|
+
/// the panel: the row stays put while the widget grows around it, which
|
|
22
|
+
/// autoresizing masks cannot say.
|
|
23
|
+
var layoutContent: ((NSRect) -> Void)?
|
|
24
|
+
|
|
25
|
+
override init(frame: NSRect) {
|
|
26
|
+
super.init(frame: frame)
|
|
27
|
+
material = .popover
|
|
28
|
+
blendingMode = .behindWindow
|
|
29
|
+
state = .active
|
|
30
|
+
wantsLayer = true
|
|
31
|
+
layer?.cornerRadius = Brand.Radius.lg
|
|
32
|
+
layer?.cornerCurve = .continuous
|
|
33
|
+
layer?.masksToBounds = true
|
|
34
|
+
// The layer's corners clip the wash and the answer, but not the
|
|
35
|
+
// blur: the window server draws that behind the whole rectangle
|
|
36
|
+
// unless the view has a mask image of the same shape.
|
|
37
|
+
maskImage = Self.mask(radius: Brand.Radius.lg)
|
|
38
|
+
|
|
39
|
+
wash.wantsLayer = true
|
|
40
|
+
wash.frame = bounds
|
|
41
|
+
wash.autoresizingMask = [.width, .height]
|
|
42
|
+
addSubview(wash)
|
|
43
|
+
|
|
44
|
+
NSWorkspace.shared.notificationCenter.addObserver(
|
|
45
|
+
self, selector: #selector(displayOptionsChanged),
|
|
46
|
+
name: NSWorkspace.accessibilityDisplayOptionsDidChangeNotification, object: nil)
|
|
47
|
+
restyle()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@available(*, unavailable)
|
|
51
|
+
required init?(coder: NSCoder) { fatalError("not from a nib") }
|
|
52
|
+
|
|
53
|
+
override func resizeSubviews(withOldSize oldSize: NSSize) {
|
|
54
|
+
super.resizeSubviews(withOldSize: oldSize)
|
|
55
|
+
layoutContent?(bounds)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override func viewDidChangeEffectiveAppearance() {
|
|
59
|
+
super.viewDidChangeEffectiveAppearance()
|
|
60
|
+
restyle()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
override func viewDidChangeBackingProperties() {
|
|
64
|
+
super.viewDidChangeBackingProperties()
|
|
65
|
+
restyle()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@objc private func displayOptionsChanged() { restyle() }
|
|
69
|
+
|
|
70
|
+
/// A stretchable continuous-corner shape: the corners stay, the middle
|
|
71
|
+
/// stretches to any size.
|
|
72
|
+
private static func mask(radius: CGFloat) -> NSImage {
|
|
73
|
+
// A continuous corner starts bending before `radius` from the edge.
|
|
74
|
+
let cap = ceil(radius * 1.6)
|
|
75
|
+
let side = cap * 2 + 1
|
|
76
|
+
let image = NSImage(size: NSSize(width: side, height: side), flipped: false) { rect in
|
|
77
|
+
guard let ctx = NSGraphicsContext.current?.cgContext else { return false }
|
|
78
|
+
let shape = CALayer()
|
|
79
|
+
shape.frame = rect
|
|
80
|
+
shape.backgroundColor = NSColor.black.cgColor
|
|
81
|
+
shape.cornerRadius = radius
|
|
82
|
+
shape.cornerCurve = .continuous
|
|
83
|
+
shape.render(in: ctx)
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
image.capInsets = NSEdgeInsets(top: cap, left: cap, bottom: cap, right: cap)
|
|
87
|
+
image.resizingMode = .stretch
|
|
88
|
+
return image
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Colours for the current appearance and accessibility settings.
|
|
92
|
+
private func restyle() {
|
|
93
|
+
let ws = NSWorkspace.shared
|
|
94
|
+
let contrast = ws.accessibilityDisplayShouldIncreaseContrast
|
|
95
|
+
let solid = ws.accessibilityDisplayShouldReduceTransparency
|
|
96
|
+
effectiveAppearance.performAsCurrentDrawingAppearance {
|
|
97
|
+
wash.layer?.backgroundColor = NSColor.windowBackgroundColor
|
|
98
|
+
.withAlphaComponent(solid || contrast ? 0.96 : 0.72).cgColor
|
|
99
|
+
// One device pixel: a hairline, as system popovers have. With
|
|
100
|
+
// Increase Contrast it becomes a clear line in the text colour.
|
|
101
|
+
layer?.borderColor = (contrast ? NSColor.labelColor.withAlphaComponent(0.6) : NSColor.separatorColor).cgColor
|
|
102
|
+
layer?.borderWidth = (contrast ? 1.5 : 1) / (window?.backingScaleFactor ?? 2)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -8,6 +8,13 @@ struct VoiceSettings: Codable, Equatable {
|
|
|
8
8
|
var stop: String
|
|
9
9
|
var paste: String
|
|
10
10
|
}
|
|
11
|
+
/// The answer shown in the pill: seconds open once spoken (0: until
|
|
12
|
+
/// closed), and its tallest height in points.
|
|
13
|
+
struct Card: Codable, Equatable {
|
|
14
|
+
var timeout: Double
|
|
15
|
+
var maxHeight: Double
|
|
16
|
+
static let standard = Card(timeout: 30, maxHeight: 320)
|
|
17
|
+
}
|
|
11
18
|
struct General: Codable, Equatable {
|
|
12
19
|
var provider: String
|
|
13
20
|
var fallback: String
|
|
@@ -17,11 +24,14 @@ struct VoiceSettings: Codable, Equatable {
|
|
|
17
24
|
/// How a hands-free turn's end is heard: "vad" or "volume".
|
|
18
25
|
var endOfTurn: String
|
|
19
26
|
var hotkeys: Hotkeys
|
|
27
|
+
/// Nil from a daemon older than the card settings.
|
|
28
|
+
var card: Card?
|
|
20
29
|
|
|
21
30
|
init(provider: String, fallback: String, stt: String, localStt: String = "mlx-whisper",
|
|
22
|
-
endOfTurn: String = "vad", hotkeys: Hotkeys) {
|
|
31
|
+
endOfTurn: String = "vad", hotkeys: Hotkeys, card: Card? = nil) {
|
|
23
32
|
self.provider = provider; self.fallback = fallback; self.stt = stt
|
|
24
33
|
self.localStt = localStt; self.endOfTurn = endOfTurn; self.hotkeys = hotkeys
|
|
34
|
+
self.card = card
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
/// A daemon from before these fields leaves them out: the defaults.
|
|
@@ -33,6 +43,7 @@ struct VoiceSettings: Codable, Equatable {
|
|
|
33
43
|
localStt = try c.decodeIfPresent(String.self, forKey: .localStt) ?? "mlx-whisper"
|
|
34
44
|
endOfTurn = try c.decodeIfPresent(String.self, forKey: .endOfTurn) ?? "vad"
|
|
35
45
|
hotkeys = try c.decode(Hotkeys.self, forKey: .hotkeys)
|
|
46
|
+
card = try c.decodeIfPresent(Card.self, forKey: .card)
|
|
36
47
|
}
|
|
37
48
|
}
|
|
38
49
|
struct Voice: Codable, Equatable {
|
|
@@ -54,6 +65,10 @@ struct VoiceSettings: Codable, Equatable {
|
|
|
54
65
|
let name: String
|
|
55
66
|
var color: String
|
|
56
67
|
var colorSet: Bool
|
|
68
|
+
/// The chosen orb palette; nil follows the colour.
|
|
69
|
+
var palette: String?
|
|
70
|
+
/// The palette the colour gives when none is chosen.
|
|
71
|
+
let paletteDefault: String?
|
|
57
72
|
var voice: Voice
|
|
58
73
|
let speaks: Speaks
|
|
59
74
|
}
|
|
@@ -63,9 +78,18 @@ struct VoiceSettings: Codable, Equatable {
|
|
|
63
78
|
let locale: String
|
|
64
79
|
}
|
|
65
80
|
|
|
81
|
+
/// One of the orb's nature palettes: five colours, deep to light.
|
|
82
|
+
struct Palette: Codable, Equatable, Identifiable {
|
|
83
|
+
let id: String
|
|
84
|
+
let label: String
|
|
85
|
+
let colors: [String]
|
|
86
|
+
}
|
|
87
|
+
|
|
66
88
|
var general: General
|
|
67
89
|
var agents: [Agent]
|
|
68
90
|
let systemVoices: [SystemVoice]
|
|
91
|
+
/// Nil from a daemon older than the palettes.
|
|
92
|
+
let palettes: [Palette]?
|
|
69
93
|
let menuHotkey: String
|
|
70
94
|
|
|
71
95
|
/// The change from `old` to this, as POST /voice/settings takes it:
|
|
@@ -83,6 +107,12 @@ struct VoiceSettings: Codable, Equatable {
|
|
|
83
107
|
if self.general.hotkeys.stop != old.general.hotkeys.stop { keys["stop"] = self.general.hotkeys.stop }
|
|
84
108
|
if self.general.hotkeys.paste != old.general.hotkeys.paste { keys["paste"] = self.general.hotkeys.paste }
|
|
85
109
|
if !keys.isEmpty { general["hotkeys"] = keys }
|
|
110
|
+
if let card = self.general.card {
|
|
111
|
+
var c: [String: Any] = [:]
|
|
112
|
+
if card.timeout != old.general.card?.timeout { c["timeout"] = card.timeout }
|
|
113
|
+
if card.maxHeight != old.general.card?.maxHeight { c["maxHeight"] = card.maxHeight }
|
|
114
|
+
if !c.isEmpty { general["card"] = c }
|
|
115
|
+
}
|
|
86
116
|
if !general.isEmpty { out["general"] = general }
|
|
87
117
|
|
|
88
118
|
var agents: [String: Any] = [:]
|
|
@@ -100,6 +130,7 @@ struct VoiceSettings: Codable, Equatable {
|
|
|
100
130
|
field("narrate", agent.voice.narrate, before.voice.narrate)
|
|
101
131
|
field("priority", agent.voice.priority, before.voice.priority)
|
|
102
132
|
field("hotkey", agent.voice.hotkey, before.voice.hotkey)
|
|
133
|
+
field("palette", agent.palette, before.palette)
|
|
103
134
|
if agent.colorSet != before.colorSet || (agent.colorSet && agent.color != before.color) {
|
|
104
135
|
a["color"] = agent.colorSet ? agent.color as Any : NSNull()
|
|
105
136
|
}
|
|
@@ -18,7 +18,6 @@ import Carbon.HIToolbox
|
|
|
18
18
|
final class App: NSObject, NSApplicationDelegate {
|
|
19
19
|
private let panel = Panel()
|
|
20
20
|
private let statusMenu = StatusMenu()
|
|
21
|
-
private let card = ResultCard()
|
|
22
21
|
/// The agent whose by-name answer is on screen now, while no turn runs.
|
|
23
22
|
private var asideSpeaker: String?
|
|
24
23
|
private let recorder = Recorder()
|
|
@@ -155,6 +154,9 @@ final class App: NSObject, NSApplicationDelegate {
|
|
|
155
154
|
/// Settings from the daemon: shortcuts now, the rest on next use.
|
|
156
155
|
private func apply(_ saved: VoiceSettings) {
|
|
157
156
|
settings = saved
|
|
157
|
+
let card = saved.general.card ?? .standard
|
|
158
|
+
panel.cardTimeout = card.timeout
|
|
159
|
+
panel.cardMaxHeight = card.maxHeight
|
|
158
160
|
if settingsWindow.model.recording == nil { registerHotkeys() }
|
|
159
161
|
// Colours may have changed.
|
|
160
162
|
statusMenu.refresh()
|
|
@@ -170,6 +172,14 @@ final class App: NSObject, NSApplicationDelegate {
|
|
|
170
172
|
return busy && !turnAgent.isEmpty ? turnAgent : Config.effectiveAgentID
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
/// The answer's "Open in chat": the agent's page in the dashboard, with
|
|
176
|
+
/// its chat open.
|
|
177
|
+
private func openChat() {
|
|
178
|
+
let who = shownAgent
|
|
179
|
+
guard !who.isEmpty, let url = URL(string: "\(Config.dashboardURL)/admin/agents/\(who)#chat") else { return }
|
|
180
|
+
NSWorkspace.shared.open(url)
|
|
181
|
+
}
|
|
182
|
+
|
|
173
183
|
/// Close, Esc or "Hide pill": the pill goes and the voice stops, the
|
|
174
184
|
/// same stop as ⌘⌥. An open microphone closes without sending. The
|
|
175
185
|
/// next talk key brings the pill back.
|
|
@@ -246,8 +256,11 @@ final class App: NSObject, NSApplicationDelegate {
|
|
|
246
256
|
panel.orb.setAnimated(Config.animatedOrb)
|
|
247
257
|
panel.orb.levelSource = { [weak self] in self?.recorder.level ?? 0 }
|
|
248
258
|
panel.onDismiss = { [weak self] in self?.dismissPill() }
|
|
249
|
-
panel.
|
|
250
|
-
|
|
259
|
+
panel.agentPalette = { [weak self] in
|
|
260
|
+
guard let self else { return nil }
|
|
261
|
+
return self.statusMenu.palette(of: self.shownAgent)
|
|
262
|
+
}
|
|
263
|
+
panel.answer.onOpenChat = { [weak self] in self?.openChat() }
|
|
251
264
|
statusMenu.pillVisible = { [weak self] in self?.panel.isVisible ?? false }
|
|
252
265
|
statusMenu.onHidePill = { [weak self] in self?.dismissPill() }
|
|
253
266
|
statusMenu.onResetPosition = { [weak self] in self?.panel.resetPosition() }
|
|
@@ -552,10 +565,10 @@ final class App: NSObject, NSApplicationDelegate {
|
|
|
552
565
|
switch result {
|
|
553
566
|
case .success(let answer):
|
|
554
567
|
Log.info("aside answer (\(answer.agentID ?? agent)): \(answer.text)")
|
|
555
|
-
if
|
|
568
|
+
if AnswerView.isWorthShowing(spoken: answer.text, written: answer.written,
|
|
556
569
|
buttons: answer.buttons, imageURL: answer.imageURL) {
|
|
557
|
-
|
|
558
|
-
|
|
570
|
+
panel.showAnswer(spoken: answer.text, written: answer.written,
|
|
571
|
+
buttons: answer.buttons, imageURL: answer.imageURL)
|
|
559
572
|
}
|
|
560
573
|
// Nothing else on screen: the pill shows this answer, its
|
|
561
574
|
// orb in this agent's colour, while it is spoken.
|
|
@@ -617,12 +630,12 @@ final class App: NSObject, NSApplicationDelegate {
|
|
|
617
630
|
// the speech cannot deliver — a link, an image, or more
|
|
618
631
|
// text than was read aloud. A card that opens on every
|
|
619
632
|
// "Ok." teaches you to ignore it.
|
|
620
|
-
if
|
|
633
|
+
if AnswerView.isWorthShowing(spoken: answer.text, written: answer.written,
|
|
621
634
|
buttons: answer.buttons, imageURL: answer.imageURL) {
|
|
622
|
-
|
|
623
|
-
|
|
635
|
+
panel.showAnswer(spoken: answer.text, written: answer.written,
|
|
636
|
+
buttons: answer.buttons, imageURL: answer.imageURL)
|
|
624
637
|
} else {
|
|
625
|
-
|
|
638
|
+
panel.collapse()
|
|
626
639
|
}
|
|
627
640
|
// Stopped while it was thinking: the answer is not spoken.
|
|
628
641
|
if silenced { silenced = false; panel.render(.idle); busy = false; return }
|
|
@@ -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-DTEQNXK3.js";import"./chunk-S7TCVIET.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-LQXWKBVY.js.map
|