agentix-cli 0.101.1 → 0.102.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.
@@ -69,6 +69,11 @@ final class CharacterHost {
69
69
  private var carried: (from: NSPoint, rest: CGPoint)?
70
70
  /// What the answering agent sent it to show (#482), and its mark.
71
71
  private let guide = CharacterGuide()
72
+ /// Sent somewhere, and what its bubble says there (#562).
73
+ var sent: Bool { guide.showing != nil }
74
+ var caption: String? { guide.caption }
75
+ /// Told when it is sent somewhere, to its next stop or back.
76
+ var onGuide: (() -> Void)?
72
77
  /// A still picture: Reduce Motion, or "Animated orb" off.
73
78
  private var still: Bool { !animated || reduceMotion }
74
79
  /// The look is the character: whether it is on screen. Nil with the orb.
@@ -147,12 +152,13 @@ final class CharacterHost {
147
152
  }
148
153
 
149
154
  /// Go beside `rect` and mark it, or with nil go back to where it rests.
150
- func guide(to rect: NSRect?, mark kind: GuideMath.Mark = .none) {
155
+ func guide(to rect: NSRect?, mark kind: GuideMath.Mark = .none, caption: String? = nil) {
151
156
  if shown, !hidden, carried == nil, let rect {
152
- guide.show(rect, kind, color: view.stops[2], animated: !still)
157
+ guide.show(rect, kind, caption: caption, color: view.stops[2], animated: !still)
153
158
  } else {
154
159
  guide.end()
155
160
  }
161
+ onGuide?()
156
162
  redraw()
157
163
  }
158
164
 
@@ -192,7 +198,7 @@ final class CharacterHost {
192
198
  switch phase {
193
199
  case .began:
194
200
  let mouse = pointerSource()
195
- if body.contains(mouse) { guide.end(glide: false); carried = (mouse, CGPoint(x: body.midX, y: rest.y)) }
201
+ if body.contains(mouse) { guide.end(glide: false); onGuide?(); carried = (mouse, CGPoint(x: body.midX, y: rest.y)) }
196
202
  case .moved:
197
203
  if carried != nil { tick() }
198
204
  case .ended:
@@ -10,6 +10,9 @@ enum GuideClient {
10
10
  /// Nil: back to where it rests.
11
11
  let rect: Rect?
12
12
  let mark: String
13
+ /// What its bubble says at the stop (#562). Nil: no bubble there,
14
+ /// and a daemon too old to send one.
15
+ let text: String?
13
16
  }
14
17
 
15
18
  /// GET /voice/guide: the command after `seq`, as soon as there is
@@ -34,8 +37,8 @@ final class GuideWatcher {
34
37
  private var task: Task<Void, Never>?
35
38
  /// The character is on screen.
36
39
  var wanted: (() -> Bool)?
37
- /// Show this, in AppKit coordinates, or go home.
38
- var onCommand: ((NSRect?, GuideMath.Mark) -> Void)?
40
+ /// Show this, in AppKit coordinates, with this caption, or go home.
41
+ var onCommand: ((NSRect?, GuideMath.Mark, String?) -> Void)?
39
42
 
40
43
  /// (Re)start the wait. Called again when the character comes on screen
41
44
  /// or leaves it, so a wait already open is ended: cancelling it closes
@@ -53,7 +56,7 @@ final class GuideWatcher {
53
56
  private func turn() async {
54
57
  guard wanted?() == true, let command = await GuideClient.next(after: seq) else {
55
58
  // Hidden, or the daemon is gone: nothing is left on screen.
56
- if away { away = false; onCommand?(nil, .none) }
59
+ if away { away = false; onCommand?(nil, .none, nil) }
57
60
  try? await Task.sleep(nanoseconds: 2_000_000_000)
58
61
  return
59
62
  }
@@ -65,7 +68,7 @@ final class GuideWatcher {
65
68
  }
66
69
  away = rect != nil
67
70
  Log.info("guide: \(rect.map { "to \(Int($0.minX)),\(Int($0.minY)) \(Int($0.width))x\(Int($0.height)) \(command.mark)" } ?? "home")")
68
- onCommand?(rect, GuideMath.Mark(rawValue: command.mark) ?? .box)
71
+ onCommand?(rect, GuideMath.Mark(rawValue: command.mark) ?? .box, rect == nil ? nil : command.text)
69
72
  }
70
73
  }
71
74
 
@@ -74,13 +77,16 @@ final class GuideWatcher {
74
77
  @MainActor
75
78
  final class CharacterGuide {
76
79
  private(set) var showing: NSRect?
80
+ /// What its bubble says there (#562).
81
+ private(set) var caption: String?
77
82
  private let mark = GuideMark()
78
83
  /// On its way up or down, to what it shows or back; and the last frame.
79
84
  private var gliding = false
80
85
  private var ticked = 0.0
81
86
 
82
- func show(_ rect: NSRect, _ kind: GuideMath.Mark, color: NSColor, animated: Bool) {
87
+ func show(_ rect: NSRect, _ kind: GuideMath.Mark, caption text: String?, color: NSColor, animated: Bool) {
83
88
  showing = rect
89
+ caption = text
84
90
  gliding = true
85
91
  mark.show(kind, around: rect, color: color, animated: animated)
86
92
  }
@@ -89,6 +95,7 @@ final class CharacterGuide {
89
95
  func end(glide: Bool = true) {
90
96
  gliding = glide && showing != nil
91
97
  showing = nil
98
+ caption = nil
92
99
  mark.hide()
93
100
  }
94
101
 
@@ -40,6 +40,17 @@ enum GuideMath {
40
40
  }
41
41
  }
42
42
 
43
+ /// What the bubble says while the assistant has nothing to say (#562).
44
+ enum Idle: Equatable { case hint, caption(String), nothing }
45
+
46
+ /// At a stop the bubble says the command's caption, and with none
47
+ /// there is no bubble; the idle hint is for where it rests, not for
48
+ /// a stop, the way there or a play. `away`: sent somewhere, or playing.
49
+ static func idle(caption: String?, away: Bool) -> Idle {
50
+ if let caption, !caption.isEmpty { return .caption(caption) }
51
+ return away ? .nothing : .hint
52
+ }
53
+
43
54
  /// One frame of its way up or down to where it is sent: most of the
44
55
  /// way in a third of a second, and there once it is within a point.
45
56
  static func glide(_ y: CGFloat, toward goal: CGFloat, dt: Double) -> CGFloat {
@@ -130,6 +130,10 @@ final class Panel: NSPanel {
130
130
  var small: CGFloat = 0
131
131
  let dots = BubbleDots()
132
132
 
133
+ /// What the bubble says while the assistant is idle (#562): the hint,
134
+ /// the caption of what the character shows, or nothing while it is away.
135
+ private(set) var idle = GuideMath.Idle.hint
136
+
133
137
  /// Told of every state rendered, so the menu-bar icon can follow.
134
138
  var onRender: ((State) -> Void)?
135
139
 
@@ -569,7 +573,10 @@ final class Panel: NSPanel {
569
573
  }
570
574
  orb.show(state.orbPhase, tint: tint, colors: colors)
571
575
 
572
- if state.isMeta {
576
+ if state.isMeta, case .caption(let words) = idle {
577
+ label.textColor = .labelColor
578
+ setText(words)
579
+ } else if state.isMeta {
573
580
  stopMarquee()
574
581
  label.attributedStringValue = Brand.metaString(state.text, color: state.color)
575
582
  label.frame.origin.x = 0
@@ -583,7 +590,9 @@ final class Panel: NSPanel {
583
590
  // Grown into an answer, it stays until it collapses, idle or not;
584
591
  // so does a pill with agents still busy in its mini orbs.
585
592
  // Reduced, the orb is the assistant's place on screen: it stays, idle or not.
586
- if dismissed || (state.isMeta && !alwaysVisible && !expanded && busyCount == 0 && !reduced) { hide() } else { show() }
593
+ // A caption shows like any words; away with none, the hint does not.
594
+ let quiet = state.isMeta && !expanded && busyCount == 0
595
+ if dismissed || (quiet && idle == .nothing) || (quiet && idle == .hint && !alwaysVisible && !reduced) { hide() } else { show() }
587
596
  // After showing or hiding: a still character draws its bubble's
588
597
  // tail only while the bubble is on screen.
589
598
  onLook?(state, tint, colors)
@@ -591,6 +600,14 @@ final class Panel: NSPanel {
591
600
  onRender?(state)
592
601
  }
593
602
 
603
+ /// The character was sent somewhere or back, or a play began or ended.
604
+ @MainActor
605
+ func setIdle(_ words: GuideMath.Idle) {
606
+ guard words != idle else { return }
607
+ idle = words
608
+ render(current)
609
+ }
610
+
594
611
  /// On screen with something to use: an answer, an error or the call
595
612
  /// buttons. The character does not play with the pointer then (#537).
596
613
  @MainActor
@@ -86,6 +86,8 @@ final class App: NSObject, NSApplicationDelegate {
86
86
  // An agent ringing the owner (#321).
87
87
  private let callWatcher = CallWatcher()
88
88
  private let guideWatcher = GuideWatcher()
89
+ /// A play runs, or its picture is being taken (#562).
90
+ private var playing = false
89
91
  private let ringer = Ringer()
90
92
  /// The call ringing on the pill now.
91
93
  private var ringingCall: IncomingCall?
@@ -236,6 +238,12 @@ final class App: NSObject, NSApplicationDelegate {
236
238
  && !busy && !recorder.isRecording && !play.running
237
239
  }
238
240
 
241
+ /// What the idle bubble says (#562): no hint at a guide stop, on the
242
+ /// way there or during a play; the stop's caption if it has one.
243
+ private func idleWords() {
244
+ panel.setIdle(GuideMath.idle(caption: character.caption, away: character.sent || playing))
245
+ }
246
+
239
247
  /// "Play on this page" (#505): one picture of the character's screen, the
240
248
  /// lines of text read on it, and the character plays on the picture,
241
249
  /// differently each time. The page itself is never touched.
@@ -252,12 +260,17 @@ final class App: NSObject, NSApplicationDelegate {
252
260
  }
253
261
  let stops = character.stops, size = screen.frame.size
254
262
  let start = PlayMath.Point(x: foot.x - screen.frame.minX, y: screen.frame.maxY - foot.y)
255
- // Off its edge, so it is not in the picture twice.
263
+ // Off its edge, so it is not in the picture twice; its idle
264
+ // bubble too, which would stay in the picture all the play long.
256
265
  character.setShown(false)
266
+ playing = true
267
+ idleWords()
257
268
  Task { @MainActor in
258
269
  try? await Task.sleep(nanoseconds: 150_000_000)
259
270
  guard let picture = PlayRead.picture(of: screen), !busy, !recorder.isRecording else {
271
+ playing = false
260
272
  character.setShown(settings?.general.look == "character")
273
+ idleWords()
261
274
  return
262
275
  }
263
276
  play.start(picture: picture, on: screen, stops: stops, at: start)
@@ -403,7 +416,9 @@ final class App: NSObject, NSApplicationDelegate {
403
416
  statusMenu.onPlay = { [weak self] in self?.startPlay() }
404
417
  play.onEnd = { [weak self] in
405
418
  guard let self else { return }
419
+ self.playing = false
406
420
  self.character.setShown(self.settings?.general.look == "character")
421
+ self.idleWords()
407
422
  }
408
423
  panel.onShown = { [weak self] in self?.character.redraw() }
409
424
  panel.onDismiss = { [weak self] in self?.dismissPill() }
@@ -511,7 +526,8 @@ final class App: NSObject, NSApplicationDelegate {
511
526
  callWatcher.start()
512
527
  // The answering agent sends the character to something (#482).
513
528
  guideWatcher.wanted = { [weak self] in self?.character.onScreen == true }
514
- guideWatcher.onCommand = { [weak self] rect, mark in self?.character.guide(to: rect, mark: mark) }
529
+ guideWatcher.onCommand = { [weak self] rect, mark, caption in self?.character.guide(to: rect, mark: mark, caption: caption) }
530
+ character.onGuide = { [weak self] in self?.idleWords() }
515
531
  guideWatcher.start()
516
532
  character.onScreenChanged = { [weak self] in self?.guideWatcher.start() }
517
533