dsh-notch 0.3.1 → 0.3.6

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 (102) hide show
  1. package/README.en.md +58 -47
  2. package/README.md +60 -55
  3. package/cordis.patch.yml +3 -0
  4. package/desktop/README.md +29 -0
  5. package/desktop/notch-lifecycle.mjs +120 -0
  6. package/desktop/restart-probe.mjs +61 -0
  7. package/docs/desktop-017rc2.md +23 -0
  8. package/docs/elastic-edge-dock.md +127 -0
  9. package/docs/motion-audit.md +1 -1
  10. package/docs/releases/v0.3.2.md +38 -0
  11. package/docs/releases/v0.3.3.md +9 -0
  12. package/docs/subagent-compatibility.md +51 -0
  13. package/docs/verification/elastic-algorithm-timing.json +337 -0
  14. package/docs/verification/elastic-attention.json +44 -0
  15. package/docs/verification/elastic-frame-timing.json +240 -0
  16. package/docs/verification/elastic-tension.json +59 -0
  17. package/docs/verification/elastic-tip-taper.json +181 -0
  18. package/dshx.yml +2 -0
  19. package/lib/client.js +112 -0
  20. package/lib/client.js.map +1 -0
  21. package/lib/index.mjs +990 -0
  22. package/lib/index.mjs.map +1 -0
  23. package/package.json +66 -10
  24. package/src/board.ts +178 -35
  25. package/src/client/index.ts +104 -0
  26. package/src/dsh-notch.ts +19 -2
  27. package/src/http.ts +17 -2
  28. package/src/session-state.ts +15 -1
  29. package/src/store.ts +3 -3
  30. package/src/types.ts +3 -0
  31. package/BASELINE.md +0 -13
  32. package/DESIGN.md +0 -34
  33. package/cordis.yml +0 -5
  34. package/macos/Package.swift +0 -17
  35. package/macos/STATUS-MOTION.md +0 -49
  36. package/macos/Sources/Client.swift +0 -139
  37. package/macos/Sources/IdleRobot.swift +0 -423
  38. package/macos/Sources/NotchMarkdown.swift +0 -266
  39. package/macos/Sources/Panel.swift +0 -153
  40. package/macos/Sources/Resources/Idle/balance.json +0 -1
  41. package/macos/Sources/Resources/Idle/blink.json +0 -1
  42. package/macos/Sources/Resources/Idle/cube-in.json +0 -1
  43. package/macos/Sources/Resources/Idle/dance.json +0 -1
  44. package/macos/Sources/Resources/Idle/hop.json +0 -1
  45. package/macos/Sources/Resources/Idle/nod.json +0 -1
  46. package/macos/Sources/Resources/Idle/satellite-in.json +0 -1
  47. package/macos/Sources/Resources/Idle/satellite-out.json +0 -1
  48. package/macos/Sources/Resources/Idle/scan.json +0 -1
  49. package/macos/Sources/Resources/Idle/sleep.json +0 -1
  50. package/macos/Sources/Resources/Idle/sneeze.json +0 -1
  51. package/macos/Sources/Resources/Idle/stretch.json +0 -1
  52. package/macos/Sources/Resources/Idle/tilt.json +0 -1
  53. package/macos/Sources/ReviewStudio.swift +0 -337
  54. package/macos/Sources/RootView.swift +0 -1093
  55. package/macos/Sources/StatusOrbit.swift +0 -642
  56. package/macos/Sources/main.swift +0 -153
  57. package/macos/Tests/ExpandedHeightProbe.swift +0 -108
  58. package/macos/Tests/GeometryProbe.swift +0 -98
  59. package/macos/Tests/IdleProbe.swift +0 -294
  60. package/macos/Tests/MotionProbe.swift +0 -106
  61. package/macos/Tests/OutcomeProbe.swift +0 -133
  62. package/macos/Tests/ScrollbarProbe.swift +0 -91
  63. package/macos/Tests/expanded-height.sh +0 -13
  64. package/macos/Tests/geometry.sh +0 -14
  65. package/macos/Tests/idle.sh +0 -14
  66. package/macos/Tests/motion.sh +0 -14
  67. package/macos/Tests/outcome.sh +0 -14
  68. package/macos/Tests/scrollbar.sh +0 -13
  69. package/tests/answer-lifecycle.test.mjs +0 -23
  70. package/tests/board.test.mjs +0 -7
  71. package/tests/browse-sync.test.mjs +0 -20
  72. package/tests/mirror.test.mjs +0 -4
  73. package/tests/peer-range.test.mjs +0 -21
  74. package/tests/session-state.test.mjs +0 -54
  75. package/tests/unread-hold.test.mjs +0 -90
  76. package/tools/desktop-shell/README.md +0 -56
  77. package/tools/desktop-shell/child-log.mjs +0 -6
  78. package/tools/desktop-shell/child-log.test.mjs +0 -14
  79. package/tools/desktop-shell/package-recovery.py +0 -78
  80. package/tools/desktop-shell/recovery-probe.cjs +0 -50
  81. package/tools/desktop-shell/renderer-recovery.mjs +0 -95
  82. package/tools/desktop-shell/renderer-recovery.test.mjs +0 -24
  83. package/tools/diagnostics/completion-watch.py +0 -101
  84. package/tools/diagnostics/completion-watch.test.py +0 -17
  85. package/tools/diagnostics/renderer-diagnostics.mjs +0 -56
  86. package/tools/diagnostics/renderer-diagnostics.test.mjs +0 -32
  87. package/tools/idle/LICENSE.open-bot-motion +0 -21
  88. package/tools/idle/export.mjs +0 -40
  89. package/tools/idle/index.html +0 -4
  90. package/tools/idle/motions.js +0 -83
  91. package/tools/idle/original.js +0 -1673
  92. package/tools/idle/renderer.js +0 -1673
  93. package/tools/recording/Client.swift +0 -76
  94. package/tools/recording/DemoCatalog.swift +0 -174
  95. package/tools/recording/README.md +0 -23
  96. package/tools/recording/build.sh +0 -41
  97. package/tools/recording/main.swift +0 -100
  98. package/tools/review/ReviewStudio.swift +0 -236
  99. package/tools/review/audit.sh +0 -19
  100. package/tools/review/build.sh +0 -7
  101. package/tools/review/main.swift +0 -158
  102. package/tools/review/render-audit.swift +0 -32
@@ -1,423 +0,0 @@
1
- import AppKit
2
- import SwiftUI
3
-
4
- struct IdleEye: Decodable {
5
- var x: Double; var y: Double; var w: Double; var h: Double
6
- var r: Double; var angle: Double; var opacity: Double
7
- }
8
- struct IdleFrame: Decodable {
9
- var points: [[Double]]; var eyes: [IdleEye]; var body: Int; var eye: Int
10
- }
11
- struct IdleClip: Decodable {
12
- var fps: Double; var duration: Double; var frames: [IdleFrame]
13
- }
14
-
15
- enum IdleInterpolation {
16
- static func mix(_ a: IdleFrame, _ b: IdleFrame, _ t: Double) -> IdleFrame {
17
- let w = max(0, min(1, t))
18
- func n(_ x: Double, _ y: Double) -> Double { x + (y-x)*w }
19
- func color(_ x: Int, _ y: Int) -> Int {
20
- [16,8,0].reduce(0) { $0 | (Int(n(Double((x >> $1)&255), Double((y >> $1)&255)).rounded()) << $1) }
21
- }
22
- let points = zip(a.points,b.points).map { [n($0[0],$1[0]),n($0[1],$1[1])] }
23
- let eyes = zip(a.eyes,b.eyes).map { e,f in
24
- IdleEye(x:n(e.x,f.x),y:n(e.y,f.y),w:n(e.w,f.w),h:n(e.h,f.h),r:n(e.r,f.r),angle:n(e.angle,f.angle),opacity:n(e.opacity,f.opacity))
25
- }
26
- return IdleFrame(points:points,eyes:eyes,body:color(a.body,b.body),eye:color(a.eye,b.eye))
27
- }
28
- // Shape-preserving cubic Hermite interpolation: continuous velocity between
29
- // authored samples, without overshoot or changing the clip's choreography.
30
- static func sample(_ clip: IdleClip, at position: Double) -> IdleFrame {
31
- let p = min(Double(clip.frames.count-1), max(0, position)), i = Int(p), t = p-Double(i)
32
- let a=clip.frames[max(0,i-1)], b=clip.frames[i]
33
- let c=clip.frames[min(i+1,clip.frames.count-1)], d=clip.frames[min(i+2,clip.frames.count-1)]
34
- func curve(_ a:Double,_ b:Double,_ c:Double,_ d:Double) -> Double {
35
- func slope(_ x:Double,_ y:Double)->Double { x*y > 0 ? 2*x*y/(x+y) : 0 }
36
- let m=slope(b-a,c-b), n=slope(c-b,d-c), t2=t*t, t3=t2*t
37
- return (2*t3-3*t2+1)*b+(t3-2*t2+t)*m+(-2*t3+3*t2)*c+(t3-t2)*n
38
- }
39
- var result=mix(b,c,t)
40
- result.points=b.points.indices.map { j in (0...1).map { k in curve(a.points[j][k],b.points[j][k],c.points[j][k],d.points[j][k]) } }
41
- result.eyes=b.eyes.indices.map { j in
42
- let a=a.eyes[j],b=b.eyes[j],c=c.eyes[j],d=d.eyes[j]
43
- return IdleEye(x:curve(a.x,b.x,c.x,d.x),y:curve(a.y,b.y,c.y,d.y),w:curve(a.w,b.w,c.w,d.w),h:curve(a.h,b.h,c.h,d.h),r:curve(a.r,b.r,c.r,d.r),angle:curve(a.angle,b.angle,c.angle,d.angle),opacity:curve(a.opacity,b.opacity,c.opacity,d.opacity))
44
- }
45
- return result
46
- }
47
- static func carry(_ source:IdleFrame, previous:IdleFrame?, seconds:Double) -> IdleFrame {
48
- guard let previous else { return source }
49
- var result=source
50
- let distance=seconds*240
51
- result.points=zip(source.points,previous.points).map { a,b in [a[0]+(a[0]-b[0])*distance,a[1]+(a[1]-b[1])*distance] }
52
- result.eyes=zip(source.eyes,previous.eyes).map { a,b in
53
- func n(_ x:Double,_ y:Double)->Double { x+(x-y)*distance }
54
- return IdleEye(x:n(a.x,b.x),y:n(a.y,b.y),w:max(0,n(a.w,b.w)),h:max(0,n(a.h,b.h)),r:max(0,n(a.r,b.r)),angle:n(a.angle,b.angle),opacity:min(1,max(0,n(a.opacity,b.opacity))))
55
- }
56
- return result
57
- }
58
- static func smooth(_ value: Double) -> Double {
59
- let t=max(0,min(1,value));return t*t*t*(t*(t*6-15)+10)
60
- }
61
- }
62
-
63
- @MainActor
64
- final class IdleLibrary {
65
- static let shared = IdleLibrary()
66
- private var clips: [String: IdleClip] = [:]
67
- func clip(_ id: String) -> IdleClip? {
68
- if let clip = clips[id] { return clip }
69
- guard let url = Bundle.module.url(forResource: id, withExtension: "json", subdirectory: "Idle"),
70
- let data = try? Data(contentsOf: url), let clip = try? JSONDecoder().decode(IdleClip.self, from: data),
71
- !clip.frames.isEmpty else { return nil }
72
- clips[id] = clip
73
- return clip
74
- }
75
- }
76
-
77
- /// Only advances while an idle robot is visible. Screen sleep never queues overdue gags.
78
- @MainActor
79
- final class IdleDirector: ObservableObject {
80
- static weak var previewInstance: IdleDirector?
81
- static let basics = ["blink", "scan", "tilt", "nod", "stretch", "hop", "balance", "sneeze", "sleep"]
82
- @Published var action: String? = "blink"
83
- @Published var began = Date()
84
- @Published var asleep = false
85
- private(set) var currentDuration: Double = 7
86
- static func restDuration() -> Double { Double.random(in: 5...10) }
87
- private var timer: Timer?
88
- private var nextBasic = Date()
89
- private var nextRare = Date()
90
- private var previous = "blink"
91
- private var blendFrom: IdleFrame?
92
- private var blendPrevious: IdleFrame?
93
- private var blendBegan = Date()
94
- private let blinkEpoch=Date()
95
- var animating: Bool { timer != nil || action != nil || blendFrom != nil }
96
- static func blinkClosure(at elapsed:Double) -> Double {
97
- let durations=[3.7,4.9,3.2,4.6,4.1,3.5]
98
- let cycle=durations.reduce(0,+)
99
- var phase=max(0,elapsed).truncatingRemainder(dividingBy:cycle)
100
- for duration in durations {
101
- if phase < duration {
102
- let t=phase-(duration-0.28)
103
- guard t >= 0 else { return 0 }
104
- return t < 0.09 ? IdleInterpolation.smooth(t/0.09) : 1-IdleInterpolation.smooth((t-0.09)/0.19)
105
- }
106
- phase-=duration
107
- }
108
- return 0
109
- }
110
- func displayFrame(at now:Date)->IdleFrame? {
111
- guard var f=frame(at:now) else { return nil }
112
- guard !reduceMotion,action != "sleep",action != "blink",action != "sneeze" else { return f }
113
- let close=Self.blinkClosure(at:now.timeIntervalSince(blinkEpoch))
114
- f.eyes=f.eyes.map { eye in var e=eye;e.h=max(0.5,e.h*(1-0.95*close));e.r=min(e.r,e.h/2);return e }
115
- return f
116
- }
117
- func frame(at now: Date) -> IdleFrame? {
118
- guard let neutral=IdleLibrary.shared.clip("blink")?.frames.first else { return nil }
119
- var target=neutral
120
- if let id=action,let clip=IdleLibrary.shared.clip(id) {
121
- let elapsed=max(0,now.timeIntervalSince(began))
122
- let position=min(elapsed*clip.fps,Double(clip.frames.count-1))
123
- target=IdleInterpolation.sample(clip,at:position)
124
- if id == "dance" {
125
- let weight=IdleInterpolation.smooth(min(elapsed/0.35,(currentDuration-elapsed)/0.45))
126
- target=IdleInterpolation.mix(neutral,target,weight)
127
- }
128
- }
129
- if let source=blendFrom {
130
- let elapsed=max(0,now.timeIntervalSince(blendBegan))
131
- let moving=IdleInterpolation.carry(source,previous:blendPrevious,seconds:0.06*(1-exp(-elapsed/0.06)))
132
- return IdleInterpolation.mix(moving,target,IdleInterpolation.smooth(elapsed/0.35))
133
- }
134
- return target
135
- }
136
- private func beginBlend(from source: IdleFrame?, at now: Date) {
137
- let previous=frame(at:now.addingTimeInterval(-1.0/240))
138
- blendPrevious=previous;blendFrom=source;blendBegan=now
139
- }
140
-
141
- var automaticActions = true
142
- private var sequence = 0
143
- private var observers: [NSObjectProtocol] = []
144
- var reduceMotion: Bool { NSWorkspace.shared.accessibilityDisplayShouldReduceMotion }
145
- func start(playImmediately: Bool = true) {
146
- Self.previewInstance = self
147
- guard timer == nil else { return }
148
- blendFrom = nil
149
- currentDuration = IdleLibrary.shared.clip("blink")?.duration ?? 7
150
- began = Date(); action = reduceMotion || !playImmediately ? nil : "blink"
151
- schedule(from: began)
152
- let nc = NSWorkspace.shared.notificationCenter
153
- observers = [nc.addObserver(forName: NSWorkspace.screensDidSleepNotification, object: nil, queue: .main) { [weak self] _ in
154
- Task { @MainActor in self?.asleep = true; self?.action = nil }
155
- }, nc.addObserver(forName: NSWorkspace.screensDidWakeNotification, object: nil, queue: .main) { [weak self] _ in
156
- Task { @MainActor in guard let self else { return }; self.asleep = false; self.schedule(from: Date()) }
157
- }]
158
- timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
159
- Task { @MainActor in self?.tick(Date()) }
160
- }
161
- }
162
- private func schedule(from date: Date) {
163
- nextBasic = date.addingTimeInterval(Self.restDuration())
164
- nextRare = date.addingTimeInterval(Double.random(in: 1200...2400))
165
- }
166
- func tick(_ now: Date) {
167
- guard !asleep, !reduceMotion else { blendFrom = nil; action = nil; return }
168
- if blendFrom != nil && now.timeIntervalSince(blendBegan) >= 0.35 { blendFrom = nil; objectWillChange.send() }
169
- if action != nil {
170
- let duration = currentDuration
171
- if now.timeIntervalSince(began) >= duration { let source=frame(at:now); beginBlend(from:source,at:now); self.action = nil; nextBasic = now.addingTimeInterval(Self.restDuration()) }
172
- return
173
- }
174
- guard automaticActions else { return }
175
- if now >= nextRare {
176
- play("dance"); nextRare = now.addingTimeInterval(Double.random(in: 1200...2400))
177
- } else if now >= nextBasic {
178
- play(Self.basics.filter { $0 != previous }.randomElement() ?? "blink")
179
- }
180
- }
181
- func play(_ id: String, at now: Date = Date()) {
182
- guard !asleep, !reduceMotion else { return }
183
- guard IdleLibrary.shared.clip(id) != nil else { return }
184
- let source=frame(at:now)
185
- beginBlend(from:source,at:now)
186
- currentDuration = id == "dance" ? Double.random(in: 3...5) : (IdleLibrary.shared.clip(id)?.duration ?? 7)
187
- previous = id; began = now; action = id
188
- if id == "dance" { nextRare = began.addingTimeInterval(Double.random(in: 1200...2400)) }
189
- }
190
- func resume(_ id: String, elapsed: Double, from source: IdleFrame? = nil) {
191
- guard !reduceMotion, !asleep else { return }
192
- beginBlend(from:source,at:Date()); blendPrevious=source
193
- currentDuration = id == "dance" ? Double.random(in: 3...5) : (IdleLibrary.shared.clip(id)?.duration ?? 7)
194
- previous = id; action = id; began = Date().addingTimeInterval(-elapsed)
195
- }
196
- func tryNext() { play(Self.basics[sequence % Self.basics.count]); sequence += 1 }
197
- func stop() {
198
- if Self.previewInstance === self { Self.previewInstance = nil }
199
- timer?.invalidate(); timer = nil; action = nil; blendFrom = nil
200
- for observer in observers { NSWorkspace.shared.notificationCenter.removeObserver(observer) }
201
- observers.removeAll()
202
- }
203
- }
204
-
205
- struct IdleRobotCanvas: View {
206
- let clip: IdleClip?
207
- let elapsed: Double
208
- var visibility: Double = 1
209
- var entering: Bool = false
210
- var entryColor: Int? = nil
211
- var exitColor: Int = 0x4d6bfe
212
- private func color(_ rgb: Int) -> Color {
213
- Color(red: Double((rgb >> 16) & 255) / 255, green: Double((rgb >> 8) & 255) / 255, blue: Double(rgb & 255) / 255)
214
- }
215
- var body: some View {
216
- let neutralFrame = IdleLibrary.shared.clip("blink")?.frames.first
217
- return Canvas { context, size in
218
- guard let clip, !clip.frames.isEmpty else { return }
219
- // Sampled vector paths are interpolated at display cadence, never enlarged bitmaps.
220
- let position = min(max(0, elapsed * clip.fps), Double(clip.frames.count - 1))
221
- let index = Int(position), amount = position - Double(index)
222
- let a = clip.frames[index], b = clip.frames[min(index + 1, clip.frames.count - 1)]
223
- let edge = clip.duration > 10 ? min(1, max(0, min(elapsed / 0.35, (clip.duration - elapsed) / 0.45))) : 1
224
- let danceBlend = edge * edge * (3 - 2 * edge)
225
- func mix(_ x: Double, _ y: Double) -> Double { x + (y - x) * amount }
226
- func mixedColor(_ x: Int, _ y: Int) -> Color {
227
- Color(red: mix(Double((x >> 16) & 255), Double((y >> 16) & 255)) / 255,
228
- green: mix(Double((x >> 8) & 255), Double((y >> 8) & 255)) / 255,
229
- blue: mix(Double(x & 255), Double(y & 255)) / 255)
230
- }
231
- context.translateBy(x: size.width / 2, y: size.height / 2)
232
- context.scaleBy(x: 40.0 / 280.0, y: 40.0 / 280.0)
233
- var centerX = 0.0, centerY = 0.0
234
- for i in a.points.indices {
235
- let n = neutralFrame?.points[i] ?? a.points[i]
236
- centerX += mix(a.points[i][0], b.points[i][0]) * danceBlend + n[0] * (1-danceBlend)
237
- centerY += mix(a.points[i][1], b.points[i][1]) * danceBlend + n[1] * (1-danceBlend)
238
- }
239
- centerX /= Double(a.points.count); centerY /= Double(a.points.count)
240
- var bodyPath = Path()
241
- for i in a.points.indices {
242
- let q = a.points[i], r = b.points[i]
243
- let n = neutralFrame?.points[i] ?? q
244
- let x = mix(q[0], r[0]) * danceBlend + n[0] * (1 - danceBlend)
245
- let y = mix(q[1], r[1]) * danceBlend + n[1] * (1 - danceBlend)
246
- let angle = atan2(y - centerY, x - centerX)
247
- let radius = (entering ? 9.5:5.7) * 280.0 / 40.0
248
- let point = CGPoint(x: x * visibility + cos(angle) * radius * (1 - visibility),
249
- y: y * visibility + sin(angle) * radius * (1 - visibility))
250
- if i == 0 { bodyPath.move(to: point) } else { bodyPath.addLine(to: point) }
251
- }
252
- bodyPath.closeSubpath()
253
- // Dance begins and ends through the neutral body, retaining original bounce speed.
254
- let pigment = mixedColor(a.body, b.body)
255
- var filled = context
256
- filled.opacity = entering ? 1 : max(0, (visibility - 0.30) / 0.70)
257
- let entryBlend=visibility
258
- let origin=entryColor ?? 0x4d6bfe
259
- func entryChannel(_ shift:Int,_ end:Double)->Double { (Double((origin >> shift)&255)*(1-entryBlend)+end*entryBlend)/255 }
260
- let entryInk=Color(red:entryChannel(16,229),green:entryChannel(8,229),blue:entryChannel(0,231))
261
- filled.fill(bodyPath, with: .color(entering ? entryInk : color(0xe5e5e7)))
262
- var painted = filled; painted.opacity *= danceBlend * (entering ? entryBlend:1); painted.fill(bodyPath, with: .color(pigment))
263
- if !entering && visibility < 1 {
264
- var outline = context; outline.opacity = min(1, (1 - visibility) * 3) * min(1, visibility * 4)
265
- func inkChannel(_ shift: Int, _ neutral: Double, _ blue: Double) -> Double {
266
- let current = mix(Double((a.body >> shift) & 255), Double((b.body >> shift) & 255)) * danceBlend + neutral * (1-danceBlend)
267
- return (current * visibility + Double((exitColor >> shift)&255) * (1-visibility))/255
268
- }
269
- let ink = Color(red: inkChannel(16,229,77), green: inkChannel(8,229,107), blue: inkChannel(0,231,254))
270
- outline.stroke(bodyPath, with: .color(ink), lineWidth: 1.7 * 280/40)
271
- }
272
- context.clip(to: bodyPath)
273
- for i in a.eyes.indices where i < b.eyes.count {
274
- let rawE = a.eyes[i], rawF = b.eyes[i]
275
- let neutral = neutralFrame?.eyes[i] ?? rawE
276
- func blendEye(_ v: IdleEye) -> IdleEye {
277
- func blend(_ x: Double, _ n: Double) -> Double { x*danceBlend+n*(1-danceBlend) }
278
- return IdleEye(x:blend(v.x,neutral.x),y:blend(v.y,neutral.y),w:blend(v.w,neutral.w),h:blend(v.h,neutral.h),r:blend(v.r,neutral.r),angle:blend(v.angle,neutral.angle),opacity:blend(v.opacity,neutral.opacity))
279
- }
280
- let e = blendEye(rawE), f = blendEye(rawF)
281
- var eyeContext = context
282
- eyeContext.translateBy(x: mix(e.x, f.x) * visibility, y: mix(e.y, f.y) * visibility)
283
- eyeContext.rotate(by: .degrees(mix(e.angle, f.angle)))
284
- eyeContext.opacity = mix(e.opacity, f.opacity) * max(0, min(1, (visibility - 0.60) / 0.40))
285
- let w = mix(e.w, f.w), h = mix(e.h, f.h), r = mix(e.r, f.r)
286
- eyeContext.fill(Path(roundedRect: CGRect(x: -w/2, y: -h/2, width: w, height: h), cornerRadius: r), with: .color(mixedColor(a.eye, b.eye)))
287
- }
288
- }
289
- .allowsHitTesting(false)
290
- }
291
- }
292
-
293
- struct RobotDeparture {
294
- let progress:Double
295
- static let duration=1.22
296
- private var robotProgress:Double { progress*Self.duration/0.9 }
297
- var scale:Double { 1-0.94*IdleInterpolation.smooth((robotProgress-0.48)/0.20) }
298
- var opacity:Double { 1-IdleInterpolation.smooth((robotProgress-0.68)/0.06) }
299
- var statusOpacity:Double { IdleInterpolation.smooth((robotProgress-0.68)/0.06) }
300
- var statusScale:Double { min(1,max(0,(progress*Self.duration-0.666)/(Self.duration-0.666))) }
301
- }
302
-
303
- @MainActor
304
- final class IdlePresence: ObservableObject {
305
- @Published var visibility: Double = 1
306
- var returnColor: Int = 0x4d6bfe
307
- @Published var entering = true
308
- @Published var transitioning = false
309
- @Published var departureProgress = 1.0
310
- private var transitionAt=Date()
311
- private var transitionIdle=true
312
- private var blendReversal=false
313
- func presentedFrame(at now:Date)->IdleFrame? {
314
- guard transitioning,let clip=IdleLibrary.shared.clip(transitionIdle ? "cube-in":"satellite-out") else { return coastFrame(at:now) }
315
- let elapsed=max(0,now.timeIntervalSince(transitionAt))
316
- let target=IdleInterpolation.sample(clip,at:elapsed*clip.fps)
317
- guard (!transitionIdle || blendReversal),let source=coastFrame(at:now) else { return target }
318
- return IdleInterpolation.mix(source,target,IdleInterpolation.smooth(elapsed/0.22))
319
- }
320
- var frozenFrame: IdleFrame?
321
- var frozenID = "blink"
322
- var frozenElapsed = 0.0
323
- private var frozenPrevious: IdleFrame?
324
- private var frozenAt=Date()
325
- func coastFrame(at now:Date)->IdleFrame? {
326
- guard let frozenFrame else { return nil }
327
- let t=max(0,now.timeIntervalSince(frozenAt))
328
- return IdleInterpolation.carry(frozenFrame,previous:frozenPrevious,seconds:0.06*(1-exp(-t/0.06)))
329
- }
330
- private var timer: Timer?
331
- private var generation = 0
332
- func set(_ idle: Bool, director: IdleDirector, animated: Bool = true) {
333
- let reversing=transitioning
334
- if reversing { frozenFrame=presentedFrame(at:Date());frozenPrevious=nil;frozenAt=Date() }
335
- blendReversal=reversing
336
- timer?.invalidate(); timer = nil
337
- generation += 1; let current = generation
338
- if !idle {
339
- if visibility >= 1 && !blendReversal {
340
- frozenAt=Date()
341
- frozenPrevious=director.displayFrame(at:frozenAt.addingTimeInterval(-1.0/240))
342
- frozenFrame = director.displayFrame(at:frozenAt)
343
- frozenID = director.action ?? "blink"
344
- frozenElapsed = director.action == nil ? 0 : max(0, Date().timeIntervalSince(director.began))
345
- }
346
- director.stop()
347
- } else {
348
- frozenFrame=coastFrame(at:Date());frozenPrevious=nil
349
- director.start(playImmediately: false)
350
- // Continue the same geometry when a partially completed transition reverses.
351
- if visibility == 0 { frozenID = "blink"; frozenElapsed = 0; frozenFrame = IdleLibrary.shared.clip("blink")?.frames.first }
352
- }
353
- // Preserve circle geometry when reversing mid-flight.
354
- if !blendReversal { entering = idle }
355
- let start = visibility, end = idle ? 1.0 : 0.0
356
- transitionAt=Date();transitionIdle=idle;transitioning=animated
357
- if !idle { departureProgress=animated ? 0:1 }
358
-
359
- guard animated, !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion else { transitioning=false;visibility = end; return }
360
- let began = ProcessInfo.processInfo.systemUptime
361
- timer = Timer.scheduledTimer(withTimeInterval: 1/60.0, repeats: true) { [weak self] _ in
362
- Task { @MainActor in
363
- guard let self, self.generation == current else { return }
364
- let t = min(1, (ProcessInfo.processInfo.systemUptime - began) / (idle ? 1.05 : RobotDeparture.duration))
365
- if !idle { self.departureProgress=t }
366
- let smooth = idle ? IdleInterpolation.smooth(t/0.32) : IdleInterpolation.smooth((t-0.68)/0.32)
367
- self.visibility = start + (end-start)*smooth
368
- if t >= 1 {
369
- self.timer?.invalidate(); self.timer = nil
370
- let final=self.presentedFrame(at:Date())
371
- self.transitioning=false
372
- if idle { director.resume("blink", elapsed:0, from:final) }
373
- }
374
- }
375
- }
376
- }
377
- func stop() { generation += 1; timer?.invalidate(); timer = nil }
378
- }
379
-
380
- struct IdleStatusSlot: View {
381
- @ObservedObject var model: BoardModel
382
- @StateObject private var director = IdleDirector()
383
- @StateObject private var presence = IdlePresence()
384
- @Environment(\.accessibilityReduceMotion) private var reduceMotion
385
- private var idle: Bool { model.showsIdleRobot }
386
- private var hasStatus: Bool { model.orbitLayout.total > 0.0001 || model.anyFailed || model.completedUnreadCount > 0 || model.busyCount > 0 || model.statusFlight != nil }
387
- var body: some View {
388
- let departing = presence.transitioning && !presence.entering
389
- let departure = RobotDeparture(progress:presence.departureProgress)
390
- ZStack {
391
- if hasStatus {
392
- StatusOrbitView(model: model,workReveal:departing ? departure.statusScale:1)
393
- .opacity(departing ? departure.statusOpacity:1-presence.visibility)
394
- }
395
- if presence.visibility > 0 || idle {
396
- TimelineView(.animation(minimumInterval: 1/60.0, paused: (!director.animating && !presence.transitioning && presence.visibility >= 1) || director.asleep || reduceMotion)) { timeline in
397
- let stable = idle && !presence.transitioning
398
- let frame = stable ? director.displayFrame(at: timeline.date) : presence.presentedFrame(at:timeline.date)
399
- let sampled = frame.map { IdleClip(fps:1,duration:7,frames:[$0]) }
400
- IdleRobotCanvas(clip: sampled, elapsed: 0, visibility: departing ? 1:presence.visibility, entering: presence.entering, entryColor:presence.returnColor,exitColor:model.needsAction ? 0xf2ff14:0x4d6bfe)
401
- }
402
- .frame(width: 30, height: 42)
403
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
404
- .scaleEffect(departing ? departure.scale:1)
405
- .opacity(departing ? departure.opacity:1)
406
- .accessibilityLabel("待机机器人")
407
- .contextMenu {
408
- if idle {
409
- Button("试试下一个待机动作") { director.tryNext() }
410
- Button("播放变色跳舞彩蛋") { director.play("dance") }
411
- }
412
- }
413
- }
414
- }
415
- .frame(height: !hasStatus && !idle ? 0 : nil)
416
- .onAppear { presence.set(idle, director: director, animated: false) }
417
- .onChange(of: idle) { _, value in
418
- if value { presence.returnColor=model.orbitLayout.decision > 0.1 ? 0xf2ff14:model.retainedFailureCount > 0 ? 0xff4000:model.retainedSuccessCount > 0 ? 0x34c759:0x4d6bfe }
419
- presence.set(value, director: director)
420
- }
421
- .onDisappear { presence.stop(); director.stop() }
422
- }
423
- }
@@ -1,266 +0,0 @@
1
- import AppKit
2
- import SwiftUI
3
-
4
- /// Block Markdown using DSH body type: 14pt system, 12pt SF Mono.
5
- enum NotchMarkdown {
6
- static let bodySize: CGFloat = 14
7
- static let headingSize: CGFloat = 17
8
- static let codeSize: CGFloat = 12
9
- }
10
-
11
- struct NotchMarkdownView: View {
12
- let source: String
13
-
14
- var body: some View {
15
- VStack(alignment: .leading, spacing: 10) {
16
- ForEach(Array(blocks.enumerated()), id: \.offset) { _, block in
17
- blockView(block)
18
- }
19
- }
20
- .frame(maxWidth: .infinity, alignment: .leading)
21
- }
22
-
23
- private var blocks: [Block] { Block.parse(source) }
24
-
25
- @ViewBuilder
26
- private func blockView(_ block: Block) -> some View {
27
- switch block {
28
- case .heading(let level, let text):
29
- inline(text)
30
- .font(.system(size: level <= 1 ? 18 : level == 2 ? 16 : 15, weight: .semibold))
31
- .foregroundStyle(.white)
32
- .padding(.top, level <= 2 ? 8 : 4)
33
- case .paragraph(let text):
34
- inline(text)
35
- .font(.system(size: NotchMarkdown.bodySize))
36
- .foregroundStyle(Color.white.opacity(0.9))
37
- .lineSpacing(6)
38
- case .list(let items, let ordered):
39
- VStack(alignment: .leading, spacing: 4) {
40
- ForEach(Array(items.enumerated()), id: \.offset) { i, item in
41
- HStack(alignment: .top, spacing: 8) {
42
- Text(ordered ? "\(i + 1)." : "•")
43
- .font(.system(size: NotchMarkdown.bodySize, weight: .medium))
44
- .foregroundStyle(Color.white.opacity(0.55))
45
- .frame(width: 18, alignment: .trailing)
46
- inline(item)
47
- .font(.system(size: NotchMarkdown.bodySize))
48
- .foregroundStyle(Color.white.opacity(0.9))
49
- .lineSpacing(5)
50
- }
51
- }
52
- }
53
- case .table(let headers, let rows):
54
- table(headers: headers, rows: rows)
55
- case .code(let text):
56
- Text(text)
57
- .font(.system(size: NotchMarkdown.codeSize, design: .monospaced))
58
- .foregroundStyle(Color.white.opacity(0.92))
59
- .frame(maxWidth: .infinity, alignment: .leading)
60
- .padding(10)
61
- .background(
62
- RoundedRectangle(cornerRadius: 8, style: .continuous)
63
- .fill(Color.white.opacity(0.07))
64
- )
65
- case .quote(let text):
66
- inline(text)
67
- .font(.system(size: NotchMarkdown.bodySize))
68
- .foregroundStyle(Color.white.opacity(0.7))
69
- .padding(.leading, 10)
70
- .overlay(alignment: .leading) {
71
- Rectangle().fill(Color.white.opacity(0.28)).frame(width: 2)
72
- }
73
- case .rule:
74
- Divider().overlay(Color.white.opacity(0.18))
75
- }
76
- }
77
-
78
- private func inline(_ markdown: String) -> Text {
79
- var options = AttributedString.MarkdownParsingOptions()
80
- options.interpretedSyntax = .inlineOnlyPreservingWhitespace
81
- options.failurePolicy = .returnPartiallyParsedIfPossible
82
- if let parsed = try? AttributedString(markdown: markdown, options: options) {
83
- var styled = parsed
84
- for run in styled.runs {
85
- if run.inlinePresentationIntent?.contains(.code) == true {
86
- styled[run.range].font = .system(size: NotchMarkdown.codeSize, design: .monospaced)
87
- styled[run.range].backgroundColor = NSColor.white.withAlphaComponent(0.1)
88
- }
89
- }
90
- return Text(styled)
91
- }
92
- return Text(markdown)
93
- }
94
-
95
- private func table(headers: [String], rows: [[String]]) -> some View {
96
- let columns = max(headers.count, rows.map(\.count).max() ?? 0)
97
- return VStack(alignment: .leading, spacing: 0) {
98
- if !headers.isEmpty {
99
- tableRow(cells: headers, header: true, columns: columns)
100
- Rectangle().fill(Color.white.opacity(0.2)).frame(height: 1)
101
- }
102
- ForEach(Array(rows.enumerated()), id: \.offset) { i, row in
103
- tableRow(cells: row, header: false, columns: columns)
104
- if i < rows.count - 1 {
105
- Rectangle().fill(Color.white.opacity(0.08)).frame(height: 1)
106
- }
107
- }
108
- }
109
- .padding(8)
110
- .background(
111
- RoundedRectangle(cornerRadius: 8, style: .continuous)
112
- .stroke(Color.white.opacity(0.16), lineWidth: 1)
113
- )
114
- }
115
-
116
- private func tableRow(cells: [String], header: Bool, columns: Int) -> some View {
117
- HStack(alignment: .top, spacing: 0) {
118
- ForEach(0..<columns, id: \.self) { i in
119
- let cell = i < cells.count ? cells[i] : ""
120
- Text(cell)
121
- .font(.system(size: 12, weight: header ? .semibold : .regular))
122
- .foregroundStyle(Color.white.opacity(header ? 0.95 : 0.82))
123
- .frame(maxWidth: .infinity, alignment: .leading)
124
- .padding(.vertical, 5)
125
- .padding(.horizontal, 6)
126
- if i < columns - 1 {
127
- Rectangle().fill(Color.white.opacity(0.1)).frame(width: 1)
128
- }
129
- }
130
- }
131
- }
132
- }
133
-
134
- private enum Block {
135
- case heading(Int, String)
136
- case paragraph(String)
137
- case list([String], Bool)
138
- case table([String], [[String]])
139
- case code(String)
140
- case quote(String)
141
- case rule
142
-
143
- static func parse(_ source: String) -> [Block] {
144
- let lines = source.replacingOccurrences(of: "\r\n", with: "\n").split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
145
- var blocks: [Block] = []
146
- var i = 0
147
- while i < lines.count {
148
- let raw = lines[i]
149
- let line = raw.trimmingCharacters(in: .whitespaces)
150
- if line.isEmpty { i += 1; continue }
151
- if line.hasPrefix("```") {
152
- i += 1
153
- var body: [String] = []
154
- while i < lines.count, !lines[i].trimmingCharacters(in: .whitespaces).hasPrefix("```") {
155
- body.append(lines[i])
156
- i += 1
157
- }
158
- if i < lines.count { i += 1 }
159
- blocks.append(.code(body.joined(separator: "\n")))
160
- continue
161
- }
162
- if line == "---" || line == "***" || line == "___" {
163
- blocks.append(.rule)
164
- i += 1
165
- continue
166
- }
167
- if let heading = heading(line) {
168
- blocks.append(heading)
169
- i += 1
170
- continue
171
- }
172
- if line.hasPrefix("|") && i + 1 < lines.count, isTableDivider(lines[i + 1]) {
173
- let headers = cells(line)
174
- i += 2
175
- var rows: [[String]] = []
176
- while i < lines.count, lines[i].trimmingCharacters(in: .whitespaces).hasPrefix("|") {
177
- rows.append(cells(lines[i]))
178
- i += 1
179
- }
180
- blocks.append(.table(headers, rows))
181
- continue
182
- }
183
- if line.hasPrefix("> ") || line == ">" {
184
- var quoted: [String] = []
185
- while i < lines.count {
186
- let t = lines[i].trimmingCharacters(in: .whitespaces)
187
- if t.hasPrefix("> ") { quoted.append(String(t.dropFirst(2))) }
188
- else if t == ">" { quoted.append("") }
189
- else { break }
190
- i += 1
191
- }
192
- blocks.append(.quote(quoted.joined(separator: "\n")))
193
- continue
194
- }
195
- if isBullet(line) || isOrdered(line) {
196
- let ordered = isOrdered(line)
197
- var items: [String] = []
198
- while i < lines.count {
199
- let t = lines[i].trimmingCharacters(in: .whitespaces)
200
- if ordered, let item = orderedItem(t) { items.append(item) }
201
- else if !ordered, let item = bulletItem(t) { items.append(item) }
202
- else { break }
203
- i += 1
204
- }
205
- blocks.append(.list(items, ordered))
206
- continue
207
- }
208
- var para: [String] = [line]
209
- i += 1
210
- while i < lines.count {
211
- let t = lines[i].trimmingCharacters(in: .whitespaces)
212
- if t.isEmpty || t.hasPrefix("#") || t.hasPrefix("|") || t.hasPrefix("```") || t.hasPrefix("> ") || isBullet(t) || isOrdered(t) || t == "---" { break }
213
- para.append(t)
214
- i += 1
215
- }
216
- blocks.append(.paragraph(para.joined(separator: " ")))
217
- }
218
- return blocks
219
- }
220
-
221
- private static func heading(_ line: String) -> Block? {
222
- guard line.hasPrefix("#") else { return nil }
223
- var level = 0
224
- for ch in line {
225
- if ch == "#" { level += 1 } else { break }
226
- }
227
- guard level > 0, level <= 6, line.count > level, line[line.index(line.startIndex, offsetBy: level)] == " " else { return nil }
228
- return .heading(level, String(line.dropFirst(level + 1)))
229
- }
230
-
231
- private static func isTableDivider(_ line: String) -> Bool {
232
- let t = line.trimmingCharacters(in: .whitespaces)
233
- guard t.hasPrefix("|") else { return false }
234
- return t.replacingOccurrences(of: "|", with: "").replacingOccurrences(of: ":", with: "").replacingOccurrences(of: "-", with: "").replacingOccurrences(of: " ", with: "").isEmpty
235
- }
236
-
237
- private static func cells(_ line: String) -> [String] {
238
- var parts = line.split(separator: "|", omittingEmptySubsequences: false).map { $0.trimmingCharacters(in: .whitespaces) }
239
- if parts.first == "" { parts.removeFirst() }
240
- if parts.last == "" { parts.removeLast() }
241
- return parts
242
- }
243
-
244
- private static func isBullet(_ line: String) -> Bool {
245
- line.hasPrefix("- ") || line.hasPrefix("* ")
246
- }
247
-
248
- private static func bulletItem(_ line: String) -> String? {
249
- if line.hasPrefix("- ") { return String(line.dropFirst(2)) }
250
- if line.hasPrefix("* ") { return String(line.dropFirst(2)) }
251
- return nil
252
- }
253
-
254
- private static func isOrdered(_ line: String) -> Bool {
255
- orderedItem(line) != nil
256
- }
257
-
258
- private static func orderedItem(_ line: String) -> String? {
259
- guard let dot = line.firstIndex(of: ".") else { return nil }
260
- let n = line[..<dot]
261
- guard !n.isEmpty, n.allSatisfy(\.isNumber) else { return nil }
262
- let rest = line[line.index(after: dot)...]
263
- guard rest.first == " " else { return nil }
264
- return String(rest.dropFirst())
265
- }
266
- }