dsh-notch 0.3.1 → 0.3.4

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 +58 -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,106 +0,0 @@
1
- import AppKit
2
- import SwiftUI
3
- @main enum MotionProbe {
4
- @MainActor static func main() {
5
- let app=NSApplication.shared
6
- app.setActivationPolicy(.accessory)
7
- let output = URL(fileURLWithPath: ProcessInfo.processInfo.environment["NOTCH_MOTION_OUTPUT"] ?? NSTemporaryDirectory()).appendingPathComponent("notch-motion-proof")
8
- try! FileManager.default.createDirectory(at: output, withIntermediateDirectories: true)
9
- var failures=0
10
- func check(_ value:Bool,_ label:String) { if !value { failures += 1; print("FAIL \(label)") } }
11
- func snapshot(_ busy:Int,_ done:Int,_ failed:Int) -> NotchSnapshot {
12
- let rows=(0..<busy).map { NotchRow(id:"busy-\($0)",title:"running",child:false,busy:true,unread:false) }
13
- + (0..<done).map { NotchRow(id:"done-\($0)",title:"done",child:false,busy:false,unread:true) }
14
- + (0..<failed).map { NotchRow(id:"failed-\($0)",title:"failed",child:false,busy:false,unread:true,lastTurn:NotchLastTurn(at:0,kind:"error",failed:true)) }
15
- return NotchSnapshot(ok:true,generatedAt:0,origin:"local-test",rows:rows)
16
- }
17
- for failed in [false,true] {
18
- for angle in stride(from: 0.0, to: 2 * Double.pi, by: Double.pi / 4) {
19
- let route = OrbitBrushRoute(failed: failed, angle: angle)
20
- check(route.points.allSatisfy { abs($0.x) + 0.75 <= 15 }, "brush stays inside shell")
21
- for returns in [false,true] {
22
- var previous = OrbitMotionFrame(progress:0,failed:failed,returns:returns,angle:angle)
23
- check(previous.tint==0 && previous.resultOpacity==0,"blue start")
24
- for i in 1...160 {
25
- let frame = OrbitMotionFrame(progress:Double(i)/160,failed:failed,returns:returns,angle:angle)
26
- check(frame.distance >= previous.distance,"brush head never reverses")
27
- check(abs(frame.distance-previous.distance)<5,"continuous brush travel")
28
- check(abs(frame.tint-previous.tint)<0.15,"continuous colour")
29
- let range = route.range(for:frame)
30
- check(range.lowerBound >= 0 && range.upperBound <= route.total + 0.01,"trail range")
31
- previous=frame
32
- }
33
- check(previous.resultOpacity==1,"solid result appears")
34
- check(returns ? previous.tint==0 : previous.tint==1,"final colour")
35
- check(returns ? previous.tailLength>35 : previous.tailLength<0.01,"returning arc or settled dot")
36
- }
37
- }
38
- }
39
- let model=BoardModel()
40
- var snap=snapshot(2,0,0);model.applySnapshot(snap)
41
- check(model.statusFlight==nil,"cold snapshot has no flight")
42
- snap.rows[0].busy=false;snap.rows[0].unread=true;model.applySnapshot(snap)
43
- check(model.statusFlight?.failed==false,"successful completion moves upward")
44
- let first=model.statusFlight!.id
45
- model.applySnapshot(snap);check(model.statusFlight?.id==first,"polling does not replay")
46
- snap.rows[1].busy=false;snap.rows[1].unread=true;snap.rows[1].lastTurn=NotchLastTurn(at:0,kind:"error",failed:true);model.applySnapshot(snap)
47
- check(model.statusFlight?.id==first,"second outcome does not interrupt flight")
48
- model.finishStatusFlight(id:first)
49
- check(model.statusFlight?.failed==true,"failure queued next")
50
- model.finishStatusFlight(id:first)
51
- check(model.statusFlight?.failed==true,"stale completion cannot clear new flight")
52
- model.finishStatusFlight(id:model.statusFlight!.id)
53
- check(model.statusFlight==nil && model.busyCount==0 && model.completedUnreadCount==1 && model.failedRows.count==1,"final counts")
54
- let panel=NotchPanel(size:NSSize(width:60,height:110))
55
- let host=NotchHostingView(rootView:StatusOrbitView(model:model, reduceMotionOverride:false).frame(width:60,height:110).background(Color.black))
56
- host.sizingOptions=[];panel.contentView=host
57
- panel.setFrame(NSRect(x:-2000,y:-2000,width:60,height:110),display:true)
58
- panel.orderFrontRegardless()
59
- Task { @MainActor in
60
- for failed in [false,true] {
61
- model.rows=snapshot(1,failed ? 0:1,failed ? 1:0).rows
62
- for (i,phase) in [0.0,0.12,0.24,0.38,0.50,0.62,0.76,0.90,0.99].enumerated() {
63
- model.statusFlight=StatusFlight(failed:failed,startedAt:Date().addingTimeInterval(-phase*StatusFlight.duration),busyBefore:2,destinationBefore:0)
64
- try? await Task.sleep(for:.milliseconds(16))
65
- host.layoutSubtreeIfNeeded()
66
- if let rep=host.bitmapImageRepForCachingDisplay(in:host.bounds) {
67
- host.cacheDisplay(in:host.bounds,to:rep)
68
- try! rep.representation(using:.png,properties:[:])!.write(to:URL(fileURLWithPath:"\(output.path)/\(failed ? "failure":"success")-\(i).png"))
69
- }
70
- }
71
- }
72
- for failed in [false,true] {
73
- model.statusFlight=nil
74
- var replay=snapshot(2,0,0)
75
- model.applySnapshot(replay)
76
- replay.rows[0].busy=false;replay.rows[0].unread=true
77
- if failed { replay.rows[0].lastTurn=NotchLastTurn(at:0,kind:"error",failed:true) }
78
- model.applySnapshot(replay)
79
- for i in 0..<100 {
80
- try? await Task.sleep(for:.milliseconds(16))
81
- host.layoutSubtreeIfNeeded()
82
- if let rep=host.bitmapImageRepForCachingDisplay(in:host.bounds) {
83
- host.cacheDisplay(in:host.bounds,to:rep)
84
- let path="\(output.path)/replay-\(failed ? "failure":"success")-\(String(format:"%03d",i)).png"
85
- try! rep.representation(using:.png,properties:[:])!.write(to:URL(fileURLWithPath:path))
86
- }
87
- }
88
- }
89
- model.statusFlight=nil;model.updateOrbitLayout();model.tickOrbitLayout(at:Date().addingTimeInterval(2))
90
- host.rootView = StatusOrbitView(model:model, reduceMotionOverride:true).frame(width:60,height:110).background(Color.black)
91
- try? await Task.sleep(for:.milliseconds(60))
92
- @MainActor func pixels() -> Data {
93
- host.layoutSubtreeIfNeeded()
94
- let rep=host.bitmapImageRepForCachingDisplay(in:host.bounds)!
95
- host.cacheDisplay(in:host.bounds,to:rep)
96
- return rep.representation(using:.png,properties:[:])!
97
- }
98
- let firstPixels=pixels()
99
- try? await Task.sleep(for:.milliseconds(160))
100
- check(firstPixels==pixels(),"reduced motion stays still")
101
- try! firstPixels.write(to:URL(fileURLWithPath:"\(output.path)/reduced-motion.png"))
102
- print("FAILURES=\(failures)");exit(failures==0 ? 0:1)
103
- }
104
- app.run()
105
- }
106
- }
@@ -1,133 +0,0 @@
1
- import AppKit
2
- import SwiftUI
3
-
4
- @main enum OutcomeProbe {
5
- @MainActor static func main() {
6
- NSApplication.shared.setActivationPolicy(.accessory)
7
- var failures=0
8
- func check(_ value:Bool,_ label:String) { if !value { failures += 1; print("FAIL \(label)") } }
9
- for count in [1,2] { for kind in ["approval","success","failure"] {
10
- let model=BoardModel();model.previewMode=true
11
- var rows=(0..<count).map { NotchRow(id:"fixture-\($0)",title:"Local fixture",child:false,busy:true,unread:false) }
12
- func snap()->NotchSnapshot {NotchSnapshot(ok:true,generatedAt:0,origin:"offline",rows:rows)}
13
- model.applySnapshot(snap());model.tickOrbitLayout(at:Date().addingTimeInterval(2))
14
- check(model.statusFlight == nil,"cold state has no result replay")
15
- if kind == "approval" {rows[0].approval=NotchApproval(id:"local-approval",toolName:"offline")}
16
- else {rows[0].busy=false;rows[0].unread=true;rows[0].lastTurn=NotchLastTurn(at:1,kind:kind,failed:kind == "failure")}
17
- model.applySnapshot(snap())
18
- guard let flight=model.statusFlight else {check(false,"\(count) task \(kind): real snapshot must start travelling brush");continue}
19
- check(flight.outcome == (kind == "approval" ? .decision:(kind == "failure" ? .failure:.success)),"\(kind): correct destination colour")
20
- check(flight.returnsToRunning == (count > 1),"\(count) task \(kind): return only if work remains")
21
- check(flight.busyBefore==count,"source count retained until departure")
22
- let id=flight.id;model.applySnapshot(snap());check(model.statusFlight?.id==id,"polling does not restart brush")
23
- let start=OrbitMotionFrame(progress:0,failed:flight.failed,returns:flight.returnsToRunning,angle:flight.angle)
24
- check(start.tint==0 && start.resultOpacity==0 && start.tailLength>20,"\(kind): starts as visible blue arc")
25
- let draw=OrbitMotionFrame(progress:0.72,failed:flight.failed,returns:flight.returnsToRunning,angle:flight.angle)
26
- check(draw.tailLength>0,"\(kind): drawing phase contains ink")
27
- let end=OrbitMotionFrame(progress:1,failed:flight.failed,returns:flight.returnsToRunning,angle:flight.angle)
28
- check(end.resultOpacity==1,"\(kind): result is solid at end")
29
- check(count > 1 ? end.tint==0 : end.tailLength==0,"\(kind): returning blue or settled result")
30
- model.finishStatusFlight(id:id)
31
- check(model.statusFlight==nil,"\(kind): flight ends")
32
- check(model.busyCount==count-1,"\(kind): exact remaining work")
33
- check(kind == "approval" ? model.needsAction : (kind == "failure" ? model.failedRows.count==1 : model.completedUnreadCount==1),"\(kind): final result retained")
34
- print("CHECKED \(count) task \(kind)")
35
- }}
36
- func busyRows(_ count:Int)->[NotchRow] {(0..<count).map {NotchRow(id:"action-\($0)",title:"Local",child:false,busy:true,unread:false)}}
37
- func snapshot(_ rows:[NotchRow])->NotchSnapshot {NotchSnapshot(ok:true,generatedAt:0,origin:"offline",rows:rows)}
38
- for cancel in [false,true] {
39
- let m=BoardModel();var rows=busyRows(1)
40
- m.applySnapshot(snapshot(rows));m.tickOrbitLayout(at:Date().addingTimeInterval(2))
41
- rows[0].approval=NotchApproval(id:"approval",toolName:"offline")
42
- m.applySnapshot(snapshot(rows));let id=m.statusFlight!.id
43
- check(!m.expanded && !m.showingExpanded,"automatic panel must preserve travelling stroke")
44
- if cancel {rows[0].approval=nil;m.applySnapshot(snapshot(rows))}
45
- m.finishStatusFlight(id:id)
46
- check(m.expanded == !cancel,"only current approval may open after ink arrives")
47
- check(m.needsAction == !cancel,"cancelled question cannot reappear")
48
- }
49
- // A result queued before an approval must retain its own identity and order.
50
- let queued=BoardModel();queued.previewMode=true;var rows=busyRows(2)
51
- queued.applySnapshot(snapshot(rows));queued.tickOrbitLayout(at:Date().addingTimeInterval(2))
52
- rows[0].busy=false;rows[0].unread=true
53
- queued.applySnapshot(snapshot(rows));let first=queued.statusFlight!.id
54
- rows[1].approval=NotchApproval(id:"queued-approval",toolName:"offline")
55
- queued.applySnapshot(snapshot(rows));check(queued.statusFlight?.id==first,"decision does not replace moving success stroke")
56
- queued.finishStatusFlight(id:first);let second=queued.statusFlight!.id
57
- check(queued.statusFlight?.decision == true,"decision uses next brush slot")
58
- queued.finishStatusFlight(id:first);check(queued.statusFlight?.id==second,"old callback cannot clear current stroke")
59
- queued.finishStatusFlight(id:second)
60
- check(queued.completedUnreadCount==1 && queued.needsAction,"green result survives yellow arrival")
61
- // A decision reply resumes the same task; never increment the visible blue
62
- // count before the yellow lamp has returned to it.
63
- for count in [1,2] {
64
- let m=BoardModel();m.previewMode=true;var rows=busyRows(count)
65
- m.applySnapshot(snapshot(rows));m.tickOrbitLayout(at:Date().addingTimeInterval(2))
66
- rows[0].approval=NotchApproval(id:"roundtrip",toolName:"offline")
67
- m.applySnapshot(snapshot(rows));m.finishStatusFlight(id:m.statusFlight!.id)
68
- m.tickOrbitLayout(at:Date().addingTimeInterval(2))
69
- check(m.busyCount==count-1 && m.needsAction,"awaiting preserves remaining task count")
70
- rows[0].approval=nil;m.applySnapshot(snapshot(rows))
71
- if count==2 {check(m.retainedBusyCount==1,"yellow reply must not show blue 2 before merging")}
72
- guard let reply=m.decisionReturn else {check(false,"reply must own a continuous return timeline");continue}
73
- check(reply.travelling == (count==2),"reply draws locally or travels to remaining blue")
74
- m.applySnapshot(snapshot(rows));check(m.decisionReturn?.id==reply.id,"polling does not restart reply")
75
- m.tickOrbitLayout(at:reply.startedAt.addingTimeInterval(reply.duration*0.1))
76
- if count==2 {check(m.orbitLayout.decision==1 && m.retainedBusyCount==1,"yellow clears before shell closes and count merges")}
77
- let finalAngle=m.decisionAngle(at:reply.startedAt.addingTimeInterval(reply.duration))
78
- m.finishDecisionReturn(id:reply.id)
79
- check(m.decisionReturn==nil,"reply releases presentation state")
80
- check(abs(m.decisionAngle(at:reply.startedAt.addingTimeInterval(reply.duration))-finalAngle)<0.00001,"spin phase survives reply handoff")
81
- m.tickOrbitLayout(at:Date().addingTimeInterval(2))
82
- check(m.busyCount==count && !m.needsAction,"reply returns original task count")
83
- check(m.orbitLayout==OrbitLayout(middle:1),"reply leaves one blue lamp without yellow slot")
84
- }
85
- for count in [1,2] {
86
- let m=BoardModel();m.previewMode=true;var rows=busyRows(count)
87
- m.applySnapshot(snapshot(rows));m.tickOrbitLayout(at:Date().addingTimeInterval(2))
88
- rows[0].approval=NotchApproval(id:"fast",toolName:"offline")
89
- m.applySnapshot(snapshot(rows));let outward=m.statusFlight!.id
90
- rows[0].approval=nil;m.applySnapshot(snapshot(rows))
91
- check(m.statusFlight?.id==outward && m.decisionReturn==nil,"fast reply finishes its outgoing ink first")
92
- check(m.orbitBusyCount==count-1,"fast reply cannot prematurely change outgoing blue count")
93
- m.finishStatusFlight(id:outward)
94
- guard let reply=m.decisionReturn else {check(false,"fast reply starts return after arrival");continue}
95
- m.finishStatusFlight(id:outward);check(m.decisionReturn?.id==reply.id,"old outbound callback cannot erase reply")
96
- m.finishDecisionReturn(id:reply.id)
97
- check(m.orbitLayout==OrbitLayout(middle:1) && m.busyCount==count,"fast reply ends on original blue count")
98
- }
99
- // A folded panel must reveal the beginning of the return, not its last frame.
100
- let panel=BoardModel();var panelRows=busyRows(2)
101
- panel.applySnapshot(snapshot(panelRows));panel.tickOrbitLayout(at:Date().addingTimeInterval(2))
102
- panelRows[0].approval=NotchApproval(id:"panel",toolName:"offline")
103
- panel.applySnapshot(snapshot(panelRows));panel.finishStatusFlight(id:panel.statusFlight!.id)
104
- check(panel.expanded && panel.showingExpanded,"approval expanded after arrival")
105
- panelRows[0].approval=nil;let replyTime=Date();panel.applySnapshot(snapshot(panelRows))
106
- check(!panel.expanded && panel.showingExpanded,"reply folds panel")
107
- check((panel.decisionReturn?.startedAt.timeIntervalSince(replyTime) ?? 0)>=NotchGeometryAnimation.duration-0.02,"return waits for folded lamp surface")
108
- // Cancelling a question without resuming that row must not increment unrelated work.
109
- let cancel=BoardModel();cancel.previewMode=true;var cancelRows=busyRows(2)
110
- cancelRows[0].approval=NotchApproval(id:"cancel",toolName:"offline")
111
- cancel.applySnapshot(snapshot(cancelRows));cancel.tickOrbitLayout(at:Date().addingTimeInterval(2))
112
- cancelRows[0].approval=nil;cancelRows[0].busy=false;cancel.applySnapshot(snapshot(cancelRows))
113
- check(cancel.decisionReturn==nil && cancel.busyCount==1,"cancel is not a task resume")
114
- for angle in stride(from:0.0,to:2 * Double.pi,by:Double.pi/4) {
115
- let route=OrbitBrushRoute(failed:false,angle:angle)
116
- var previous=DecisionReturnFrame(progress:0,angle:angle)
117
- check(previous.fill==1 && previous.countMix==0,"return starts as existing yellow and existing count")
118
- for i in 1...240 {
119
- let frame=DecisionReturnFrame(progress:Double(i)/240,angle:angle)
120
- check(frame.distance>=previous.distance && frame.distance-previous.distance<3,"return brush advances continuously")
121
- check(frame.tint<=previous.tint+0.00001 && abs(frame.tint-previous.tint)<0.08,"return colour blends into blue")
122
- check(frame.collapse==0 || frame.fill==0,"yellow solid clears before lamps converge")
123
- check(frame.countMix==0 || frame.distance>=route.returned,"count changes only after ink enters blue ring")
124
- previous=frame
125
- }
126
- check(previous.fill==0 && previous.tint==0 && previous.countMix==1,"return settles on blue new count")
127
- let near=DecisionReturnFrame(progress:0.9999,angle:angle)
128
- let velocity=(previous.distance-near.distance)/(StatusFlight.duration*0.0001)
129
- check(abs(velocity-9.5*DecisionSpin.runningVelocity)<0.05,"pen speed matches final rotating ring")
130
- }
131
- print("FAILURES=\(failures)");exit(failures==0 ? 0:1)
132
- }
133
- }
@@ -1,91 +0,0 @@
1
- import AppKit
2
- import SwiftUI
3
- import Combine
4
-
5
- @main enum ScrollbarProbe {
6
- @MainActor static func main() {
7
- let app = NSApplication.shared
8
- app.setActivationPolicy(.accessory)
9
- let model = BoardModel()
10
- model.previewMode = true
11
- model.maximumExpandedHeight = 800
12
- let panel = NotchPanel(size:NSSize(width:38,height:44))
13
- let host = NotchHostingView(rootView:RootView(model:model,panelSize:CGSize(width:480,height:800),restSize:CGSize(width:38,height:44)))
14
- host.sizingOptions = []
15
- panel.embedHost(host)
16
- panel.setFrame(NSRect(x:-2000,y:-2000,width:38,height:44),display:true)
17
- panel.orderFrontRegardless()
18
- var subscriptions = Set<AnyCancellable>()
19
- Publishers.CombineLatest(model.$currentIslandWidth,model.$currentIslandHeight)
20
- .receive(on:DispatchQueue.main).sink {w,h in panel.resizeAnchored(to:NSSize(width:w,height:h))}
21
- .store(in:&subscriptions)
22
- @MainActor func scrollViews(_ view:NSView)->[NSScrollView] {
23
- ((view as? NSScrollView).map {[$0]} ?? []) + view.subviews.flatMap(scrollViews)
24
- }
25
- Task { @MainActor in
26
- let rows=(0..<5).map {NotchRow(id:"fixture-\($0)",title:"Running task \($0+1)",child:false,busy:true,unread:false)}
27
- model.applySnapshot(NotchSnapshot(ok:true,generatedAt:0,origin:"offline",rows:rows))
28
- try? await Task.sleep(for:.milliseconds(800))
29
- var failures=0, samples=0, flashFrames=0
30
- @MainActor func check(_ condition:Bool,_ message:String) {
31
- if !condition {failures += 1;print("FAIL \(message)")}
32
- }
33
- @MainActor func inspectShortExpansion(_ scenario:String) async {
34
- model.expanded=true
35
- for frame in 0..<45 {
36
- try? await Task.sleep(for:.milliseconds(16))
37
- host.layoutSubtreeIfNeeded()
38
- let scrolls=scrollViews(host)
39
- for (i,scroll) in scrolls.enumerated() {
40
- guard let bar=scroll.verticalScroller else {continue}
41
- let visible=scroll.hasVerticalScroller && !bar.isHiddenOrHasHiddenAncestor && bar.alphaValue>0 && bar.bounds.height>0 && bar.knobProportion<0.999
42
- if visible {failures += 1;flashFrames += 1}
43
- if frame<4 || visible || frame==44 {
44
- print("SCENARIO=\(scenario) FRAME=\(frame) SCROLL=\(i) PANEL=\(Int(panel.frame.height)) DOCUMENT=\(Int(scroll.documentView?.frame.height ?? 0)) VIEWPORT=\(Int(scroll.contentView.bounds.height)) HAS_BAR=\(scroll.hasVerticalScroller) HIDDEN=\(bar.isHiddenOrHasHiddenAncestor) ALPHA=\(bar.alphaValue) KNOB=\(bar.knobProportion) VISIBLE=\(visible)")
45
- }
46
- }
47
- samples += scrolls.count
48
- }
49
- }
50
- await inspectShortExpansion("five-tasks")
51
- model.expanded=false
52
- try? await Task.sleep(for:.milliseconds(600))
53
- await inspectShortExpansion("reopen")
54
- model.expanded=false
55
- try? await Task.sleep(for:.milliseconds(70))
56
- await inspectShortExpansion("reverse-collapse")
57
- check(samples>0,"no native scroll views reached")
58
-
59
- // A real overflow must keep both its indicator and a movable viewport.
60
- model.maximumExpandedHeight=400
61
- let question=NotchQuestion(id:"long",question:"Long question",options:[NotchOption(label:"Option",description:String(repeating:"Long offline content. ",count:150))])
62
- let row=NotchRow(id:"question",title:"Local fixture",child:false,busy:true,unread:false,ask:NotchAsk(id:"ask",questions:[question]))
63
- model.applySnapshot(NotchSnapshot(ok:true,generatedAt:0,origin:"offline",rows:[row]))
64
- try? await Task.sleep(for:.milliseconds(850))
65
- host.layoutSubtreeIfNeeded()
66
- check(abs(panel.frame.height-400)<2,"long content reaches the screen cap")
67
- if let scroll=scrollViews(host).first,let document=scroll.documentView {
68
- let overflow=document.frame.height-scroll.contentView.bounds.height
69
- check(overflow>20,"long fixture actually overflows")
70
- check(scroll.hasVerticalScroller,"real overflow retains its indicator")
71
- let before=scroll.contentView.bounds.origin.y
72
- scroll.contentView.scroll(to:NSPoint(x:0,y:overflow))
73
- scroll.reflectScrolledClipView(scroll.contentView)
74
- check(scroll.contentView.bounds.origin.y>before+20,"long content remains scrollable")
75
- print("LONG_OVERFLOW=\(Int(overflow)) SCROLLED=\(Int(scroll.contentView.bounds.origin.y)) INDICATOR=\(scroll.hasVerticalScroller)")
76
- } else {check(false,"long fixture has a native scroll view")}
77
- model.maximumExpandedHeight=800
78
- model.applySnapshot(NotchSnapshot(ok:true,generatedAt:0,origin:"offline",rows:rows))
79
- try? await Task.sleep(for:.milliseconds(850))
80
- check(panel.frame.height<400,"short content shrinks after long content")
81
- model.expanded=false
82
- try? await Task.sleep(for:.milliseconds(600))
83
- await inspectShortExpansion("after-long-content")
84
- print("SCROLLBAR_FLASH_FRAMES=\(flashFrames)")
85
- print("FAILURES=\(failures)")
86
- exit(failures==0 ? 0:1)
87
- }
88
- app.run()
89
- withExtendedLifetime(subscriptions) {}
90
- }
91
- }
@@ -1,13 +0,0 @@
1
- #!/bin/sh
2
- set -eu
3
- base=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
4
- work="$base/.build/expanded-height-probe"
5
- mkdir -p "$work/Sources"
6
- cp -R "$base/Sources/Resources" "$work/Sources/"
7
- cp "$base/Package.swift" "$work/Package.swift"
8
- for file in IdleRobot.swift NotchMarkdown.swift RootView.swift Panel.swift Client.swift StatusOrbit.swift; do
9
- cp "$base/Sources/$file" "$work/Sources/$file"
10
- done
11
- cp "$base/Tests/ExpandedHeightProbe.swift" "$work/Sources/main.swift"
12
- swift build --package-path "$work" -c release
13
- "$work/.build/release/dsh-notch"
@@ -1,14 +0,0 @@
1
- #!/bin/sh
2
- set -eu
3
- base=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
4
- work=$(mktemp -d "${TMPDIR:-/tmp}/notch-geometry.XXXXXX")
5
- trap 'rm -rf "$work"' EXIT
6
- mkdir "$work/Sources"
7
- cp -R "$base/Sources/Resources" "$work/Sources/Resources"
8
- cp "$base/Sources/IdleRobot.swift" "$work/Sources/"
9
- cp "$base/Sources/NotchMarkdown.swift" "$work/Sources/"
10
- cp "$base/Package.swift" "$work/Package.swift"
11
- cp "$base/Sources/RootView.swift" "$base/Sources/Panel.swift" "$base/Sources/Client.swift" "$base/Sources/StatusOrbit.swift" "$work/Sources/"
12
- cp "$base/Tests/GeometryProbe.swift" "$work/Sources/main.swift"
13
- swift build --package-path "$work" -c release
14
- "$work/.build/release/dsh-notch"
@@ -1,14 +0,0 @@
1
- #!/bin/sh
2
- set -eu
3
- base=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
4
- work=$(mktemp -d "${TMPDIR:-/tmp}/notch-idle.XXXXXX")
5
- trap 'rm -rf "$work"' EXIT
6
- mkdir "$work/Sources"
7
- cp -R "$base/Sources/Resources" "$work/Sources/Resources"
8
- cp "$base/Sources/IdleRobot.swift" "$work/Sources/"
9
- cp "$base/Sources/NotchMarkdown.swift" "$work/Sources/"
10
- cp "$base/Package.swift" "$work/Package.swift"
11
- cp "$base/Sources/RootView.swift" "$base/Sources/Panel.swift" "$base/Sources/Client.swift" "$base/Sources/StatusOrbit.swift" "$work/Sources/"
12
- cp "$base/Tests/IdleProbe.swift" "$work/Sources/main.swift"
13
- swift build --package-path "$work" -c release
14
- "$work/.build/release/dsh-notch"
@@ -1,14 +0,0 @@
1
- #!/bin/sh
2
- set -eu
3
- base=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
4
- work=$(mktemp -d "${TMPDIR:-/tmp}/notch-motion.XXXXXX")
5
- trap 'rm -rf "$work"' EXIT
6
- mkdir "$work/Sources"
7
- cp -R "$base/Sources/Resources" "$work/Sources/Resources"
8
- cp "$base/Sources/IdleRobot.swift" "$work/Sources/"
9
- cp "$base/Sources/NotchMarkdown.swift" "$work/Sources/"
10
- cp "$base/Package.swift" "$work/Package.swift"
11
- cp "$base/Sources/RootView.swift" "$base/Sources/Panel.swift" "$base/Sources/Client.swift" "$base/Sources/StatusOrbit.swift" "$work/Sources/"
12
- cp "$base/Tests/MotionProbe.swift" "$work/Sources/main.swift"
13
- swift build --package-path "$work" -c release
14
- "$work/.build/release/dsh-notch"
@@ -1,14 +0,0 @@
1
- #!/bin/sh
2
- set -eu
3
- base=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
4
- work=$(mktemp -d "${TMPDIR:-/tmp}/notch-outcome.XXXXXX")
5
- trap 'rm -rf "$work"' EXIT
6
- mkdir "$work/Sources"
7
- cp -R "$base/Sources/Resources" "$work/Sources/Resources"
8
- cp "$base/Sources/IdleRobot.swift" "$work/Sources/"
9
- cp "$base/Sources/NotchMarkdown.swift" "$work/Sources/"
10
- cp "$base/Package.swift" "$work/Package.swift"
11
- cp "$base/Sources/RootView.swift" "$base/Sources/Panel.swift" "$base/Sources/Client.swift" "$base/Sources/StatusOrbit.swift" "$work/Sources/"
12
- cp "$base/Tests/OutcomeProbe.swift" "$work/Sources/main.swift"
13
- swift build --package-path "$work" -c release
14
- "$work/.build/release/dsh-notch"
@@ -1,13 +0,0 @@
1
- #!/bin/sh
2
- set -eu
3
- base=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
4
- work="$base/.build/scrollbar-probe"
5
- mkdir -p "$work/Sources"
6
- cp -R "$base/Sources/Resources" "$work/Sources/"
7
- cp "$base/Package.swift" "$work/Package.swift"
8
- for file in IdleRobot.swift NotchMarkdown.swift RootView.swift Panel.swift Client.swift StatusOrbit.swift; do
9
- cp "$base/Sources/$file" "$work/Sources/$file"
10
- done
11
- cp "$base/Tests/ScrollbarProbe.swift" "$work/Sources/main.swift"
12
- swift build --package-path "$work" -c release
13
- "$work/.build/release/dsh-notch"
@@ -1,23 +0,0 @@
1
- import {test} from 'node:test';import assert from 'node:assert/strict';
2
- import os from 'node:os';
3
- import {mkdtempSync, rmSync} from 'node:fs';
4
- import {syncBuiltinESMExports} from 'node:module';
5
- import {after} from 'node:test';
6
- const testDir=mkdtempSync(os.tmpdir()+'/notch-answer-test-');
7
- const realHomedir=os.homedir;
8
- os.homedir=()=>testDir; syncBuiltinESMExports();
9
- const {Board}=await import('../src/board.ts');
10
- os.homedir=realHomedir; syncBuiltinESMExports();
11
- after(()=>rmSync(testDir,{recursive:true,force:true}));
12
- const session={id:'notch-lifecycle-test',header:{},snapshotEvents:()=>[]};
13
- const ctx={sessions:{list:()=>[session]},agents:{get:()=>({status:'running'})},get:()=>undefined,logger:{warn(){}}};
14
- test('Notch answer cancels the web wait and preserves caller signal',async()=>{
15
- const board=new Board(ctx), owner=new AbortController();const request={agent:{id:session.id},questions:[{id:'q',question:'test'}],signal:owner.signal};let webVisible=true;
16
- const pending=board.holdAsk(request,()=>new Promise((resolve,reject)=>{request.signal.addEventListener('abort',()=>{webVisible=false;reject(Error('web wait ended'))},{once:true})}));
17
- const ask=board.snapshot('http://localhost').rows[0].ask;assert.ok(ask);assert.equal(board.answerAsk(ask.id,[{id:'q',selected:['test']}]),true);
18
- assert.deepEqual(await pending,{answers:[{id:'q',selected:['test']}]});assert.equal(webVisible,false);assert.equal(owner.signal.aborted,false);assert.equal(request.signal,owner.signal);
19
- });
20
- test('Web answer removes the Notch request',async()=>{
21
- const board=new Board(ctx);let finish;const answer={answers:[{id:'q',selected:[]}]};
22
- const pending=board.holdAsk({agent:{id:session.id},questions:[{id:'q',question:'test'}]},()=>new Promise(r=>finish=r));await Promise.resolve();finish(answer);assert.equal(await pending,answer);assert.equal(board.snapshot('http://localhost').rows[0].ask,undefined);
23
- });
@@ -1,7 +0,0 @@
1
- import {test} from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import {Board} from '../src/board.ts'
4
- const session={id:'session-regression',header:{},snapshotEvents:()=>[]}
5
- const ctx={sessions:{list:()=>[session]},agents:{get:()=>({status:'running'})},get:()=>undefined,logger:{warn:()=>{}}}
6
- test('snapshot and background notification handle live sessions without missing seen state',()=>{const board=new Board(ctx);let notifications=0;board.onChange(()=>{assert.equal(board.snapshot('http://localhost').rows.length,1);notifications++});board.notify();assert.equal(notifications,1)})
7
- test('one broken subscriber cannot crash Host or stop other subscribers',()=>{const board=new Board(ctx);let n=0;board.onChange(()=>{throw Error('broken stream')});board.onChange(()=>n++);assert.doesNotThrow(()=>board.notify());assert.equal(n,1)})
@@ -1,20 +0,0 @@
1
- import {test} from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import {installBrowseSync} from '../src/browse-sync.ts'
4
- class Controller { history={page:async()=>({records:[1]}),follow:async function*(){yield {records:[1]}}}; page(request,signal){return this.history.page(request,signal)} follow(request,signal){return this.history.follow(request,signal)} }
5
- const req={address:{sessionId:'test'}}
6
- test('legacy synchronous Notch wrapper reproduces exact error; repair removes all nested wrappers',async()=>{
7
- const c=new Controller();const board={markSeen(){}};const getTargetSessionId=a=>a.sessionId
8
- for(let i=0;i<2;i++){const origFollow=c.follow.bind(c);c.follow=function* (req, signal) {const sid=getTargetSessionId(req.address);if(sid)board.markSeen(sid);return yield* origFollow(req,signal)}}
9
- await assert.rejects(async()=>{for await(const x of c.follow(req)){}},/is not iterable/)
10
- const stop=installBrowseSync(c,()=>{});const rows=[];for await(const x of c.follow(req))rows.push(x)
11
- assert.deepEqual(rows,[{records:[1]}]);stop();assert.equal(Object.hasOwn(c,'follow'),false)
12
- })
13
- test('preserves async stream identity, return, cancellation; repeated generations dispose cleanly',async()=>{
14
- const c=new Controller();let returned=0;const signal=AbortSignal.abort();const source={async *[Symbol.asyncIterator](){try{yield 1;yield 2}finally{returned++}}}
15
- c.history.follow=(r,s)=>{assert.equal(s,signal);return source}
16
- let a=0,b=0;const stopA=installBrowseSync(c,()=>a++);const stopB=installBrowseSync(c,()=>b++)
17
- assert.equal(c.follow(req,signal),source);stopA();const stream=c.follow(req,signal);for await(const v of stream){break}
18
- assert.equal(a,1);assert.equal(b,2);assert.equal(returned,1);stopB();stopB();assert.equal(Object.hasOwn(c,'follow'),false)
19
- const stop=installBrowseSync(c,()=>{throw Error('read marker failed')});assert.deepEqual(await c.page(req),{records:[1]});stop()
20
- })
@@ -1,4 +0,0 @@
1
- import {test} from 'node:test';import assert from 'node:assert/strict';import {Board} from '../src/board.ts';
2
- const ctx={sessions:{list:()=>[]},agents:{get:()=>undefined},get:()=>undefined,logger:{warn:()=>{}}};
3
- test('sidebar completion survives cold session; opening in DSH clears green; background page cannot overwrite',()=>{const b=new Board(ctx);const row={id:'session-cold',title:'Cold',running:false,completed:true};assert.equal(b.syncSidebar({clientId:'app',focused:true,rows:[row]}),true);assert.equal(b.snapshot('').rows[0].unread,true);assert.equal(b.requestFocus(row.id),true);b.syncSidebar({clientId:'app',focused:true,rows:[]});assert.equal(b.snapshot('').rows.length,0);b.syncSidebar({clientId:'browser',focused:false,rows:[row]});assert.equal(b.snapshot('').rows.length,0)});
4
- test('malformed mirror never replaces previous valid state',()=>{const b=new Board(ctx);assert.equal(b.syncSidebar({clientId:'app',rows:[{id:123}]}),false);assert.deepEqual(b.snapshot('').rows,[])});
@@ -1,21 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { readFileSync } from 'node:fs'
4
-
5
- const RANGE = '>=0.1.7-rc.1 <0.1.8'
6
- const PEERS = [
7
- '@deepseek-ai/dsh',
8
- '@deepseek-ai/dsh-host-webserver',
9
- '@deepseek-ai/dsh-session',
10
- '@deepseek-ai/dsh-user-approval',
11
- '@deepseek-ai/dsh-user-questions',
12
- ]
13
-
14
- test('Harness peer range is the 0.1.7-rc.1 desk pin', () => {
15
- const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
16
- assert.equal(pkg.version, '0.3.1')
17
- for (const name of PEERS) {
18
- assert.equal(pkg.peerDependencies[name], RANGE)
19
- assert.equal(pkg.peerDependenciesMeta[name].optional, true)
20
- }
21
- })
@@ -1,54 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { foldSession } from '../src/session-state.ts'
4
-
5
- function session(events) {
6
- return { snapshotEvents: () => events }
7
- }
8
-
9
- test('aborted last turn is not a failure lamp', () => {
10
- const folded = foldSession(session([
11
- { type: 'turn/start', time: 1 },
12
- { type: 'turn/end', time: 2, data: { reason: { kind: 'aborted' } } },
13
- ]))
14
- assert.equal(folded.busy, false)
15
- assert.equal(folded.lastTurn.kind, 'aborted')
16
- assert.equal(folded.lastTurn.failed, false)
17
- })
18
-
19
- test('interrupted last turn is not a failure lamp', () => {
20
- const folded = foldSession(session([
21
- { type: 'turn/start', time: 1 },
22
- { type: 'turn/end', time: 2, data: { reason: { kind: 'interrupted' } } },
23
- ]))
24
- assert.equal(folded.lastTurn.failed, false)
25
- })
26
-
27
- test('a new turn clears the previous abort so a running session cannot stay red', () => {
28
- const folded = foldSession(session([
29
- { type: 'turn/start', time: 1 },
30
- { type: 'turn/end', time: 2, data: { reason: { kind: 'aborted' } } },
31
- { type: 'turn/start', time: 3 },
32
- ]))
33
- assert.equal(folded.busy, true)
34
- assert.equal(folded.lastTurn, undefined)
35
- })
36
-
37
- test('a fork-seed closer is not a failure lamp', () => {
38
- const folded = foldSession(session([
39
- { type: 'turn/start', time: 1 },
40
- { type: 'turn/end', time: 2, data: { reason: { kind: 'forked' } } },
41
- ]))
42
- assert.equal(folded.busy, false)
43
- assert.equal(folded.lastTurn.kind, 'forked')
44
- assert.equal(folded.lastTurn.failed, false)
45
- })
46
-
47
- test('error still counts as failed after the turn ends', () => {
48
- const folded = foldSession(session([
49
- { type: 'turn/start', time: 1 },
50
- { type: 'turn/end', time: 2, data: { reason: { kind: 'error' } } },
51
- ]))
52
- assert.equal(folded.busy, false)
53
- assert.equal(folded.lastTurn.failed, true)
54
- })