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.
Files changed (116) hide show
  1. package/apps/mac-voice/Sources/AgentXVoice/AgentClient.swift +4 -0
  2. package/apps/mac-voice/Sources/AgentXVoice/AnswerView.swift +228 -0
  3. package/apps/mac-voice/Sources/AgentXVoice/Orb.swift +36 -9
  4. package/apps/mac-voice/Sources/AgentXVoice/Panel.swift +126 -55
  5. package/apps/mac-voice/Sources/AgentXVoice/PanelAnswer.swift +113 -0
  6. package/apps/mac-voice/Sources/AgentXVoice/PillPlacement.swift +28 -17
  7. package/apps/mac-voice/Sources/AgentXVoice/SettingsLook.swift +90 -0
  8. package/apps/mac-voice/Sources/AgentXVoice/SettingsWindow.swift +10 -3
  9. package/apps/mac-voice/Sources/AgentXVoice/StatusMenu.swift +9 -0
  10. package/apps/mac-voice/Sources/AgentXVoice/Surface.swift +105 -0
  11. package/apps/mac-voice/Sources/AgentXVoice/VoiceSettings.swift +32 -1
  12. package/apps/mac-voice/Sources/AgentXVoice/main.swift +23 -10
  13. package/dist/{App-QEJGXZTY.js → App-LQXWKBVY.js} +2 -2
  14. package/dist/{ChatApp-VKWQN5GM.js → ChatApp-75TL4LKW.js} +2 -2
  15. package/dist/{agent-MJKLI2L5.js → agent-DH4FMMMY.js} +2 -2
  16. package/dist/{board-dashboard-2KVT72J7.js → board-dashboard-BVRRDYAG.js} +6 -4
  17. package/dist/{board-dashboard-2KVT72J7.js.map → board-dashboard-BVRRDYAG.js.map} +1 -1
  18. package/dist/{chunk-WFPRVZBA.js → chunk-27P5NLTC.js} +2 -2
  19. package/dist/{chunk-3EVCHRJF.js → chunk-2VQIPJH5.js} +2 -2
  20. package/dist/{chunk-YRB75GMN.js → chunk-3XVTFNYM.js} +2 -2
  21. package/dist/{chunk-K62U5EJJ.js → chunk-B3DCYBP4.js} +2 -2
  22. package/dist/{chunk-EJQOKSKP.js → chunk-CRSXKQLZ.js} +2 -2
  23. package/dist/{chunk-YCIZ2WHX.js → chunk-DTEQNXK3.js} +2 -2
  24. package/dist/{chunk-N5GOBLUI.js → chunk-E2BJVHBR.js} +2 -2
  25. package/dist/{chunk-6DUTIPTY.js → chunk-E2PWCU2N.js} +2 -2
  26. package/dist/{chunk-2XZGHIVP.js → chunk-JO3PSLWL.js} +5 -5
  27. package/dist/{chunk-YQU74AOL.js → chunk-JORXRF4E.js} +2 -2
  28. package/dist/{chunk-DAAWUFO4.js → chunk-LXUG2X26.js} +2 -2
  29. package/dist/{chunk-JUY77L7C.js → chunk-N5KD3TVN.js} +2 -2
  30. package/dist/{chunk-5OF3EGCZ.js → chunk-NBGNPKAF.js} +2 -2
  31. package/dist/{chunk-4ZTV6JV7.js → chunk-PAOV37KQ.js} +2 -2
  32. package/dist/{chunk-SDBU6747.js → chunk-RYFUFX6F.js} +160 -160
  33. package/dist/chunk-RYFUFX6F.js.map +1 -0
  34. package/dist/chunk-S7TCVIET.js +15 -0
  35. package/dist/chunk-S7TCVIET.js.map +1 -0
  36. package/dist/{chunk-ILXW3ERG.js → chunk-TM762AXU.js} +159 -153
  37. package/dist/chunk-TM762AXU.js.map +1 -0
  38. package/dist/{chunk-WI5R64WO.js → chunk-WBGC7MWX.js} +2 -2
  39. package/dist/{chunk-6ZV2QFXE.js → chunk-WIBMNQKJ.js} +2 -2
  40. package/dist/{chunk-L5OCF33B.js → chunk-WOPFHKBF.js} +2 -2
  41. package/dist/{chunk-OIDMPPV5.js → chunk-YQCOX4R3.js} +2 -2
  42. package/dist/cli.js +1 -1
  43. package/dist/client-QVZIZFV4.js +2 -0
  44. package/dist/config-3XC7EYB7.js +2 -0
  45. package/dist/config-mutator-HKU63FJM.js +2 -0
  46. package/dist/doctor-YFA4ZSRH.js +2 -0
  47. package/dist/index.d.ts +49 -7
  48. package/dist/index.js +1 -1
  49. package/dist/program-BOCLU6ES.js +2 -0
  50. package/dist/{promote-GBR6YSQW.js → promote-UR2GSHKC.js} +2 -2
  51. package/dist/query-XUP3BMVS.js +2 -0
  52. package/dist/{registry-P535FNLB.js → registry-PGALUW5L.js} +2 -2
  53. package/dist/{request-planner-KHTCHF7N.js → request-planner-NHE3IRJN.js} +2 -2
  54. package/dist/{review-candidates-Y5KCRUNU.js → review-candidates-6GKRSRBQ.js} +2 -2
  55. package/dist/{routing-RWCCDJ4D.js → routing-AQKHAU3Z.js} +2 -2
  56. package/dist/schedule-tool-6NMCLTIH.js +2 -0
  57. package/dist/scheduler-RAYGNDWT.js +2 -0
  58. package/dist/{seat-PJPKLH6L.js → seat-6FMW2QI4.js} +2 -2
  59. package/dist/{select-VMTJAX5X.js → select-5VONJMLH.js} +2 -2
  60. package/dist/{set-enabled-LPXH3ZQP.js → set-enabled-YHD3IJJZ.js} +2 -2
  61. package/dist/{tool-MWXG44OT.js → tool-MXJGLJZ5.js} +2 -2
  62. package/dist/wiki-ZQLEYY24.js +2 -0
  63. package/package.json +1 -1
  64. package/apps/mac-voice/Sources/AgentXVoice/ResultCard.swift +0 -198
  65. package/dist/chunk-ILXW3ERG.js.map +0 -1
  66. package/dist/chunk-IW4XIZAZ.js +0 -15
  67. package/dist/chunk-IW4XIZAZ.js.map +0 -1
  68. package/dist/chunk-SDBU6747.js.map +0 -1
  69. package/dist/client-X2PTYKVX.js +0 -2
  70. package/dist/config-XB2ORKWS.js +0 -2
  71. package/dist/config-mutator-KMXR3XDG.js +0 -2
  72. package/dist/doctor-6BIMNEGU.js +0 -2
  73. package/dist/program-DHDC6XYB.js +0 -2
  74. package/dist/query-672RGMXO.js +0 -2
  75. package/dist/schedule-tool-H46AK53H.js +0 -2
  76. package/dist/scheduler-3XLWBCX7.js +0 -2
  77. package/dist/wiki-QHMHVFWU.js +0 -2
  78. /package/dist/{App-QEJGXZTY.js.map → App-LQXWKBVY.js.map} +0 -0
  79. /package/dist/{ChatApp-VKWQN5GM.js.map → ChatApp-75TL4LKW.js.map} +0 -0
  80. /package/dist/{agent-MJKLI2L5.js.map → agent-DH4FMMMY.js.map} +0 -0
  81. /package/dist/{chunk-WFPRVZBA.js.map → chunk-27P5NLTC.js.map} +0 -0
  82. /package/dist/{chunk-3EVCHRJF.js.map → chunk-2VQIPJH5.js.map} +0 -0
  83. /package/dist/{chunk-YRB75GMN.js.map → chunk-3XVTFNYM.js.map} +0 -0
  84. /package/dist/{chunk-K62U5EJJ.js.map → chunk-B3DCYBP4.js.map} +0 -0
  85. /package/dist/{chunk-EJQOKSKP.js.map → chunk-CRSXKQLZ.js.map} +0 -0
  86. /package/dist/{chunk-YCIZ2WHX.js.map → chunk-DTEQNXK3.js.map} +0 -0
  87. /package/dist/{chunk-N5GOBLUI.js.map → chunk-E2BJVHBR.js.map} +0 -0
  88. /package/dist/{chunk-6DUTIPTY.js.map → chunk-E2PWCU2N.js.map} +0 -0
  89. /package/dist/{chunk-2XZGHIVP.js.map → chunk-JO3PSLWL.js.map} +0 -0
  90. /package/dist/{chunk-YQU74AOL.js.map → chunk-JORXRF4E.js.map} +0 -0
  91. /package/dist/{chunk-DAAWUFO4.js.map → chunk-LXUG2X26.js.map} +0 -0
  92. /package/dist/{chunk-JUY77L7C.js.map → chunk-N5KD3TVN.js.map} +0 -0
  93. /package/dist/{chunk-5OF3EGCZ.js.map → chunk-NBGNPKAF.js.map} +0 -0
  94. /package/dist/{chunk-4ZTV6JV7.js.map → chunk-PAOV37KQ.js.map} +0 -0
  95. /package/dist/{chunk-WI5R64WO.js.map → chunk-WBGC7MWX.js.map} +0 -0
  96. /package/dist/{chunk-6ZV2QFXE.js.map → chunk-WIBMNQKJ.js.map} +0 -0
  97. /package/dist/{chunk-L5OCF33B.js.map → chunk-WOPFHKBF.js.map} +0 -0
  98. /package/dist/{chunk-OIDMPPV5.js.map → chunk-YQCOX4R3.js.map} +0 -0
  99. /package/dist/{client-X2PTYKVX.js.map → client-QVZIZFV4.js.map} +0 -0
  100. /package/dist/{config-XB2ORKWS.js.map → config-3XC7EYB7.js.map} +0 -0
  101. /package/dist/{config-mutator-KMXR3XDG.js.map → config-mutator-HKU63FJM.js.map} +0 -0
  102. /package/dist/{doctor-6BIMNEGU.js.map → doctor-YFA4ZSRH.js.map} +0 -0
  103. /package/dist/{program-DHDC6XYB.js.map → program-BOCLU6ES.js.map} +0 -0
  104. /package/dist/{promote-GBR6YSQW.js.map → promote-UR2GSHKC.js.map} +0 -0
  105. /package/dist/{query-672RGMXO.js.map → query-XUP3BMVS.js.map} +0 -0
  106. /package/dist/{registry-P535FNLB.js.map → registry-PGALUW5L.js.map} +0 -0
  107. /package/dist/{request-planner-KHTCHF7N.js.map → request-planner-NHE3IRJN.js.map} +0 -0
  108. /package/dist/{review-candidates-Y5KCRUNU.js.map → review-candidates-6GKRSRBQ.js.map} +0 -0
  109. /package/dist/{routing-RWCCDJ4D.js.map → routing-AQKHAU3Z.js.map} +0 -0
  110. /package/dist/{schedule-tool-H46AK53H.js.map → schedule-tool-6NMCLTIH.js.map} +0 -0
  111. /package/dist/{scheduler-3XLWBCX7.js.map → scheduler-RAYGNDWT.js.map} +0 -0
  112. /package/dist/{seat-PJPKLH6L.js.map → seat-6FMW2QI4.js.map} +0 -0
  113. /package/dist/{select-VMTJAX5X.js.map → select-5VONJMLH.js.map} +0 -0
  114. /package/dist/{set-enabled-LPXH3ZQP.js.map → set-enabled-YHD3IJJZ.js.map} +0 -0
  115. /package/dist/{tool-MWXG44OT.js.map → tool-MXJGLJZ5.js.map} +0 -0
  116. /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 its answer card go on screen, kept free of AppKit so
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
- /// Which side of the pill the answer card opens on, and where.
42
- struct Card: Equatable {
43
- let origin: CGPoint
44
- /// True: above the pill. False: below it.
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
- /// Above the pill when there is at least as much room above it as
49
- /// below, else below. Right edges line up with the pill's, and the
50
- /// card is kept on the screen.
51
- static func card(size: CGSize, pill: CGRect, visible: CGRect) -> Card {
52
- let roomAbove = visible.maxY - pill.maxY
53
- let roomBelow = pill.minY - visible.minY
54
- let above = roomAbove >= roomBelow
55
- let y = above ? pill.maxY + cardGap : pill.minY - cardGap - size.height
56
- let frame = CGRect(x: pill.maxX - size.width, y: y, width: size.width, height: size.height)
57
- return Card(origin: inside(frame, visible).origin, above: above)
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 colour")
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.onMove = { [weak self] in self?.card.follow() }
250
- card.anchor = { [weak self] in self?.panel.frame ?? .zero }
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 ResultCard.isWorthShowing(spoken: answer.text, written: answer.written,
568
+ if AnswerView.isWorthShowing(spoken: answer.text, written: answer.written,
556
569
  buttons: answer.buttons, imageURL: answer.imageURL) {
557
- card.show(spoken: answer.text, written: answer.written,
558
- buttons: answer.buttons, imageURL: answer.imageURL)
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 ResultCard.isWorthShowing(spoken: answer.text, written: answer.written,
633
+ if AnswerView.isWorthShowing(spoken: answer.text, written: answer.written,
621
634
  buttons: answer.buttons, imageURL: answer.imageURL) {
622
- card.show(spoken: answer.text, written: answer.written,
623
- buttons: answer.buttons, imageURL: answer.imageURL)
635
+ panel.showAnswer(spoken: answer.text, written: answer.written,
636
+ buttons: answer.buttons, imageURL: answer.imageURL)
624
637
  } else {
625
- card.orderOut(nil)
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-YCIZ2WHX.js";import"./chunk-IW4XIZAZ.js";import"./chunk-QIJLFPWE.js";import{useEffect as R,useReducer as V,useRef as G}from"react";import{Box as i,Text as o,useApp as W,useInput as J}from"ink";import{randomUUID as z}from"crypto";async function*q(e){let r=[];e.type&&r.push(`type=${encodeURIComponent(e.type)}`);let t=`${e.baseUrl.replace(/\/+$/,"")}/events${r.length?`?${r.join("&")}`:""}`,n={Accept:"text/event-stream"};e.token&&(n.Authorization=`Bearer ${e.token}`);let s=await fetch(t,{headers:n,signal:e.signal});if(!s.ok||!s.body)throw new Error(`SSE connect failed: HTTP ${s.status}`);let u=s.body.getReader(),p=new TextDecoder,y="";try{for(;;){let{done:f,value:l}=await u.read();if(f)return;y+=p.decode(l,{stream:!0});let d;for(;(d=y.indexOf(`
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-QEJGXZTY.js.map
8
+ //# sourceMappingURL=App-LQXWKBVY.js.map