dsh-notch 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BASELINE.md +13 -0
- package/DESIGN.md +34 -0
- package/LICENSE +21 -0
- package/README.en.md +136 -0
- package/README.md +145 -0
- package/cordis.yml +5 -0
- package/docs/motion-audit.md +65 -0
- package/docs/motion-continuity.md +77 -0
- package/docs/releases/v0.3.0.md +20 -0
- package/dshx.yml +14 -0
- package/macos/Package.swift +17 -0
- package/macos/STATUS-MOTION.md +49 -0
- package/macos/Sources/Client.swift +139 -0
- package/macos/Sources/IdleRobot.swift +423 -0
- package/macos/Sources/NotchMarkdown.swift +266 -0
- package/macos/Sources/Panel.swift +153 -0
- package/macos/Sources/Resources/Idle/balance.json +1 -0
- package/macos/Sources/Resources/Idle/blink.json +1 -0
- package/macos/Sources/Resources/Idle/cube-in.json +1 -0
- package/macos/Sources/Resources/Idle/dance.json +1 -0
- package/macos/Sources/Resources/Idle/hop.json +1 -0
- package/macos/Sources/Resources/Idle/nod.json +1 -0
- package/macos/Sources/Resources/Idle/satellite-in.json +1 -0
- package/macos/Sources/Resources/Idle/satellite-out.json +1 -0
- package/macos/Sources/Resources/Idle/scan.json +1 -0
- package/macos/Sources/Resources/Idle/sleep.json +1 -0
- package/macos/Sources/Resources/Idle/sneeze.json +1 -0
- package/macos/Sources/Resources/Idle/stretch.json +1 -0
- package/macos/Sources/Resources/Idle/tilt.json +1 -0
- package/macos/Sources/ReviewStudio.swift +337 -0
- package/macos/Sources/RootView.swift +1093 -0
- package/macos/Sources/StatusOrbit.swift +642 -0
- package/macos/Sources/main.swift +153 -0
- package/macos/Tests/ExpandedHeightProbe.swift +108 -0
- package/macos/Tests/GeometryProbe.swift +98 -0
- package/macos/Tests/IdleProbe.swift +294 -0
- package/macos/Tests/MotionProbe.swift +106 -0
- package/macos/Tests/OutcomeProbe.swift +133 -0
- package/macos/Tests/ScrollbarProbe.swift +91 -0
- package/macos/Tests/expanded-height.sh +13 -0
- package/macos/Tests/geometry.sh +14 -0
- package/macos/Tests/idle.sh +14 -0
- package/macos/Tests/motion.sh +14 -0
- package/macos/Tests/outcome.sh +14 -0
- package/macos/Tests/scrollbar.sh +13 -0
- package/package.json +46 -0
- package/src/board.ts +299 -0
- package/src/browse-sync.ts +59 -0
- package/src/dsh-notch.ts +60 -0
- package/src/http.ts +215 -0
- package/src/session-state.ts +31 -0
- package/src/store.ts +49 -0
- package/src/types.ts +65 -0
- package/tests/answer-lifecycle.test.mjs +23 -0
- package/tests/board.test.mjs +7 -0
- package/tests/browse-sync.test.mjs +20 -0
- package/tests/mirror.test.mjs +4 -0
- package/tests/peer-range.test.mjs +21 -0
- package/tests/session-state.test.mjs +54 -0
- package/tests/unread-hold.test.mjs +90 -0
- package/tools/desktop-shell/README.md +56 -0
- package/tools/desktop-shell/child-log.mjs +6 -0
- package/tools/desktop-shell/child-log.test.mjs +14 -0
- package/tools/desktop-shell/package-recovery.py +78 -0
- package/tools/desktop-shell/recovery-probe.cjs +50 -0
- package/tools/desktop-shell/renderer-recovery.mjs +95 -0
- package/tools/desktop-shell/renderer-recovery.test.mjs +24 -0
- package/tools/diagnostics/completion-watch.py +101 -0
- package/tools/diagnostics/completion-watch.test.py +17 -0
- package/tools/diagnostics/renderer-diagnostics.mjs +56 -0
- package/tools/diagnostics/renderer-diagnostics.test.mjs +32 -0
- package/tools/idle/LICENSE.open-bot-motion +21 -0
- package/tools/idle/export.mjs +40 -0
- package/tools/idle/index.html +4 -0
- package/tools/idle/motions.js +83 -0
- package/tools/idle/original.js +1673 -0
- package/tools/idle/renderer.js +1673 -0
- package/tools/recording/Client.swift +76 -0
- package/tools/recording/DemoCatalog.swift +174 -0
- package/tools/recording/README.md +23 -0
- package/tools/recording/build.sh +41 -0
- package/tools/recording/main.swift +100 -0
- package/tools/review/ReviewStudio.swift +236 -0
- package/tools/review/audit.sh +19 -0
- package/tools/review/build.sh +7 -0
- package/tools/review/main.swift +158 -0
- package/tools/review/render-audit.swift +32 -0
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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"
|
|
@@ -0,0 +1,14 @@
|
|
|
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"
|
|
@@ -0,0 +1,14 @@
|
|
|
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"
|
|
@@ -0,0 +1,14 @@
|
|
|
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"
|
|
@@ -0,0 +1,14 @@
|
|
|
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"
|
|
@@ -0,0 +1,13 @@
|
|
|
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"
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-notch",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/dsh-notch.ts",
|
|
6
|
+
"description": "Native macOS notch for live DSH sessions, approvals, and model questions.",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node --import tsx --test tests/*.test.mjs",
|
|
9
|
+
"test:motion": "sh macos/Tests/motion.sh",
|
|
10
|
+
"test:geometry": "sh macos/Tests/geometry.sh",
|
|
11
|
+
"build:macos": "swift build --package-path macos -c release",
|
|
12
|
+
"test:outcome": "sh macos/Tests/outcome.sh",
|
|
13
|
+
"test:idle": "sh macos/Tests/idle.sh",
|
|
14
|
+
"build:demo": "sh tools/recording/build.sh",
|
|
15
|
+
"test:scrollbar": "sh macos/Tests/scrollbar.sh",
|
|
16
|
+
"test:expanded-height": "sh macos/Tests/expanded-height.sh"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"tsx": "4.22.4"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@deepseek-ai/dsh": ">=0.1.7-rc.1 <0.1.8",
|
|
23
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1 <0.1.8",
|
|
24
|
+
"@deepseek-ai/dsh-session": ">=0.1.7-rc.1 <0.1.8",
|
|
25
|
+
"@deepseek-ai/dsh-user-approval": ">=0.1.7-rc.1 <0.1.8",
|
|
26
|
+
"@deepseek-ai/dsh-user-questions": ">=0.1.7-rc.1 <0.1.8"
|
|
27
|
+
},
|
|
28
|
+
"peerDependenciesMeta": {
|
|
29
|
+
"@deepseek-ai/dsh": {
|
|
30
|
+
"optional": true
|
|
31
|
+
},
|
|
32
|
+
"@deepseek-ai/dsh-host-webserver": {
|
|
33
|
+
"optional": true
|
|
34
|
+
},
|
|
35
|
+
"@deepseek-ai/dsh-session": {
|
|
36
|
+
"optional": true
|
|
37
|
+
},
|
|
38
|
+
"@deepseek-ai/dsh-user-approval": {
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"@deepseek-ai/dsh-user-questions": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT"
|
|
46
|
+
}
|