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,76 +0,0 @@
1
- import Foundation
2
-
3
- struct RuntimeFile: Decodable {
4
- var origin: String
5
- var token: String
6
- }
7
-
8
- struct NotchOption: Decodable, Identifiable {
9
- var label: String
10
- var description: String?
11
- var id: String { label }
12
- }
13
-
14
- struct NotchQuestion: Decodable, Identifiable {
15
- var id: String
16
- var question: String
17
- var detail: String?
18
- var header: String?
19
- var options: [NotchOption]?
20
- var multiSelect: Bool?
21
- }
22
-
23
- struct NotchApproval: Decodable {
24
- var id: String
25
- var toolName: String
26
- var reason: String?
27
- }
28
-
29
- struct NotchAsk: Decodable {
30
- var id: String
31
- var questions: [NotchQuestion]
32
- }
33
-
34
- struct NotchLastTurn: Decodable {
35
- var at: Double
36
- var kind: String
37
- var failed: Bool
38
- }
39
-
40
- struct NotchRow: Decodable, Identifiable {
41
- var id: String
42
- var title: String
43
- var child: Bool
44
- var busy: Bool
45
- var unread: Bool
46
- var lastTurn: NotchLastTurn?
47
- var approval: NotchApproval?
48
- var ask: NotchAsk?
49
-
50
- var needsAction: Bool { approval != nil || ask != nil }
51
- /// A red lamp is a finished unsuccessful turn, never a session that is still running.
52
- var isFailedResult: Bool { lastTurn?.failed == true && !busy && !needsAction }
53
- }
54
-
55
- struct NotchSnapshot: Decodable {
56
- var ok: Bool
57
- var generatedAt: Double
58
- var origin: String
59
- var rows: [NotchRow]
60
- }
61
-
62
- enum NotchClientError: Error {
63
- case noRuntime
64
- case http(Int)
65
- }
66
-
67
- // Preview-only transport: no credential reads, network, or live session actions.
68
- final class NotchClient: @unchecked Sendable {
69
- func reloadRuntime() throws { throw NotchClientError.noRuntime }
70
- func status() async throws -> NotchSnapshot { throw NotchClientError.noRuntime }
71
- func approve(id: String, outcome: String) async throws {}
72
- @MainActor func answer(id: String, answers: [[String: Any]]) async throws {}
73
- func seen(sessionId: String) async throws {}
74
- func seenAll() async throws {}
75
- func focus(sessionId: String) async throws {}
76
- }
@@ -1,174 +0,0 @@
1
- import AppKit
2
- import SwiftUI
3
-
4
- final class PreviewIdleCue: @unchecked Sendable {
5
- static let notification=Notification.Name("NotchRecordingIdleCue")
6
- let boardID:ObjectIdentifier
7
- let action:String
8
- init(board:BoardModel,action:String) {boardID=ObjectIdentifier(board);self.action=action}
9
- }
10
-
11
- enum DemoState {case running, decision, success, failure}
12
- struct DemoBeat {
13
- let at:Double
14
- var rows:[NotchRow]? = nil
15
- var action:String? = nil
16
- }
17
- struct DemoScene: Identifiable {
18
- let id:String
19
- let title:String
20
- var englishTitle:String {DemoCatalog.english[id] ?? id}
21
- let initial:[NotchRow]
22
- let beats:[DemoBeat]
23
- var duration:Double=7.8
24
- }
25
-
26
- enum DemoCatalog {
27
- static let english:[String:String] = [
28
- "birth-blue":"Idle → Running",
29
- "birth-yellow":"Idle → Decision",
30
- "one-yellow":"Running → Decision",
31
- "one-return":"Decision → Running",
32
- "one-green":"Running → Done",
33
- "one-red":"Running → Failed",
34
- "two-yellow":"Two Tasks → Decision",
35
- "two-return":"Decision → Two Tasks",
36
- "two-green":"Two Tasks → One Done",
37
- "two-red":"Two Tasks → One Failed",
38
- "one-roundtrip":"One Task · Decision & Resume",
39
- "two-roundtrip":"Two Tasks · Decision & Resume",
40
- "green-idle":"Done → Idle",
41
- "red-idle":"Failed → Idle",
42
- "green-blue":"Read Result → Keep Running",
43
- "red-blue":"Read Failure → Keep Running",
44
- "clear-green":"Read Result → Keep Failure",
45
- "clear-red":"Read Failure → Keep Result",
46
- "add-work":"One Task → Many Tasks",
47
- "all-green":"More Tasks Done",
48
- "all-red":"More Tasks Failed",
49
- "mixed-green":"Last Task → Done",
50
- "mixed-red":"Last Task → Failed",
51
- "clear-all":"All Read → Idle",
52
- "idle-blink":"Blink",
53
- "idle-scan":"Look Around",
54
- "idle-tilt":"Head Tilt",
55
- "idle-nod":"Nod",
56
- "idle-stretch":"Stretch",
57
- "idle-hop":"Hop",
58
- "idle-balance":"Balance",
59
- "idle-sneeze":"Sneeze",
60
- "idle-sleep":"Doze",
61
- "idle-dance":"Chameleon",
62
- "idle-flow":"Motion → Motion",
63
- "dance-flow":"Chameleon Transitions",
64
- ]
65
- static func rows(_ states:[DemoState])->[NotchRow] {
66
- states.enumerated().map {index,state in
67
- var row=NotchRow(id:"demo-task-\(index)",title:"本地任务",child:false,busy:state == .running || state == .decision,unread:state == .success || state == .failure)
68
- if state == .decision {row.approval=NotchApproval(id:"demo-approval-\(index)",toolName:"本地演示")}
69
- if state == .success || state == .failure {row.lastTurn=NotchLastTurn(at:1,kind:"demo",failed:state == .failure)}
70
- return row
71
- }
72
- }
73
- static func scene(_ id:String,_ title:String,_ from:[DemoState],_ to:[DemoState],at:Double=2.3)->DemoScene {
74
- DemoScene(id:id,title:title,initial:rows(from),beats:[DemoBeat(at:at,rows:rows(to))])
75
- }
76
- static func clear(_ id:String,_ title:String,_ from:[DemoState],index:Int)->DemoScene {
77
- let source=rows(from)
78
- return DemoScene(id:id,title:title,initial:source,beats:[DemoBeat(at:2.3,rows:source.filter {$0.id != "demo-task-\(index)"})])
79
- }
80
- static func idle(_ action:String,_ title:String)->DemoScene {
81
- DemoScene(id:"idle-\(action)",title:title,initial:[],beats:[DemoBeat(at:0.65,action:action)],duration:action == "sleep" ? 10.5:9)
82
- }
83
- static let groups:[[DemoScene]] = [
84
- [scene("birth-blue","待机 → 运行",[],[.running]),
85
- scene("birth-yellow","待机 → 决策",[],[.decision]),
86
- scene("one-yellow","运行 → 决策",[.running],[.decision]),
87
- scene("one-return","决策 → 运行",[.decision],[.running]),
88
- scene("one-green","运行 → 完成",[.running],[.success]),
89
- scene("one-red","运行 → 失败",[.running],[.failure])],
90
- [scene("two-yellow","两个任务 → 决策",[.running,.running],[.decision,.running]),
91
- scene("two-return","决策 → 两个任务",[.decision,.running],[.running,.running]),
92
- scene("two-green","两个任务 → 完成",[.running,.running],[.success,.running]),
93
- scene("two-red","两个任务 → 失败",[.running,.running],[.failure,.running]),
94
- DemoScene(id:"one-roundtrip",title:"一个任务 · 决策往返",initial:rows([.running]),beats:[DemoBeat(at:1.6,rows:rows([.decision])),DemoBeat(at:4.5,rows:rows([.running]))]),
95
- DemoScene(id:"two-roundtrip",title:"两个任务 · 决策往返",initial:rows([.running,.running]),beats:[DemoBeat(at:1.6,rows:rows([.decision,.running])),DemoBeat(at:4.5,rows:rows([.running,.running]))])],
96
- [clear("green-idle","完成 → 待机",[.success],index:0),
97
- clear("red-idle","失败 → 待机",[.failure],index:0),
98
- clear("green-blue","已读完成 → 继续运行",[.success,.running],index:0),
99
- clear("red-blue","已读失败 → 继续运行",[.failure,.running],index:0),
100
- clear("clear-green","已读完成 → 保留失败",[.success,.failure],index:0),
101
- clear("clear-red","已读失败 → 保留完成",[.success,.failure],index:1)],
102
- [scene("add-work","一个任务 → 多个任务",[.running],[.running,.running,.running]),
103
- scene("all-green","继续完成 → 完成累积",[.success,.running],[.success,.success]),
104
- scene("all-red","继续失败 → 失败累积",[.failure,.running],[.failure,.failure]),
105
- scene("mixed-green","最后任务 → 完成",[.success,.failure,.running],[.success,.failure,.success]),
106
- scene("mixed-red","最后任务 → 失败",[.success,.failure,.running],[.success,.failure,.failure]),
107
- DemoScene(id:"clear-all",title:"全部已读 → 待机",initial:rows([.success,.failure]),beats:[DemoBeat(at:1.6,rows:rows([.success,.failure]).filter {$0.id != "demo-task-0"}),DemoBeat(at:4.1,rows:[])])],
108
- [idle("blink","眨眼"),idle("scan","左右看看"),idle("tilt","歪头"),idle("nod","点头"),idle("stretch","伸懒腰"),idle("hop","蹦一蹦")],
109
- [idle("balance","保持平衡"),idle("sneeze","打喷嚏"),idle("sleep","打个盹"),idle("dance","变色龙"),
110
- DemoScene(id:"idle-flow",title:"动作自然衔接",initial:[],beats:[DemoBeat(at:0.65,action:"nod"),DemoBeat(at:4.4,action:"stretch")],duration:12),
111
- DemoScene(id:"dance-flow",title:"彩蛋自然衔接",initial:[],beats:[DemoBeat(at:0.65,action:"dance"),DemoBeat(at:5.3,action:"scan")],duration:13)]
112
- ]
113
- static var all:[DemoScene] {groups.flatMap {$0}}
114
- }
115
-
116
- @MainActor final class DemoTile:ObservableObject,Identifiable {
117
- let id=UUID()
118
- let scene:DemoScene
119
- let board=BoardModel()
120
- init(_ scene:DemoScene) {
121
- self.scene=scene;board.previewMode=true
122
- apply(scene.initial);board.tickOrbitLayout(at:Date().addingTimeInterval(3))
123
- }
124
- func apply(_ rows:[NotchRow]) {
125
- board.applySnapshot(NotchSnapshot(ok:true,generatedAt:0,origin:"offline-recording",rows:rows))
126
- }
127
- func apply(_ beat:DemoBeat) {
128
- if let rows=beat.rows {apply(rows)}
129
- if let action=beat.action {NotificationCenter.default.post(name:PreviewIdleCue.notification,object:PreviewIdleCue(board:board,action:action))}
130
- }
131
- }
132
-
133
- @MainActor final class RecordingModel:ObservableObject {
134
- @Published var tiles:[DemoTile]=[]
135
- @Published var group=0
136
- @Published var selected=0
137
- @Published var solo=false
138
- @Published var autoplay=true
139
- @Published var controls=false
140
- private var job:Task<Void,Never>?
141
- private var epoch=0
142
- var count:Int {solo ? DemoCatalog.all.count:DemoCatalog.groups.count}
143
- var page:Int {solo ? selected:group}
144
- var scenes:[DemoScene] {solo ? [DemoCatalog.all[selected]]:DemoCatalog.groups[group]}
145
- func move(_ direction:Int) {
146
- if solo {selected=(selected+direction+count)%count} else {group=(group+direction+count)%count}
147
- replay()
148
- }
149
- func toggleLayout() {
150
- if !solo {selected=group*6} else {group=selected/6}
151
- solo.toggle();replay()
152
- }
153
- func replay() {
154
- job?.cancel();epoch += 1;let token=epoch
155
- job=Task { @MainActor in
156
- while !Task.isCancelled,token==epoch {
157
- let batch=scenes.map(DemoTile.init);tiles=batch
158
- let duration=scenes.map(\.duration).max() ?? 8
159
- let events=batch.flatMap {tile in tile.scene.beats.map {(tile,$0)}}.sorted {$0.1.at < $1.1.at}
160
- var last=0.0
161
- for (tile,beat) in events {
162
- do {try await Task.sleep(for:.seconds(max(0,beat.at-last)))} catch {return}
163
- guard token==epoch else {return};tile.apply(beat);last=beat.at
164
- }
165
- do {try await Task.sleep(for:.seconds(max(0,duration-last)))} catch {return}
166
- guard token==epoch else {return}
167
- if autoplay {
168
- if solo {selected=(selected+1)%count} else {group=(group+1)%count}
169
- }
170
- }
171
- }
172
- }
173
- func stop() {job?.cancel();job=nil;epoch += 1}
174
- }
@@ -1,23 +0,0 @@
1
- # Bilingual recording demo / 双语录屏演示
2
-
3
- Build from the current production animation sources:
4
-
5
- ```sh
6
- npm run build:demo
7
- open "dist/DSH Notch Demo.app"
8
- ```
9
-
10
- 36 scenes in six groups cover idle, working, decisions and resume, success, failure, unread clearing, nine robot motions, and the chameleon easter egg. Chinese and English titles share one fixed title area. The demo uses local fixtures and a transport stub; it does not connect to DSH or use model tokens.
11
-
12
- 共 6 组、36 个场景。中文在上、英文在下,仅保留状态名称。动画源与正式 helper 相同,演示仅用本地假任务驱动,不连接 DSH。
13
-
14
- | Key | Action / 操作 |
15
- | --- | --- |
16
- | ← / → | Previous / next group · 上一组 / 下一组 |
17
- | R | Replay · 重播 |
18
- | S | Grid / single scene · 并排 / 单场景 |
19
- | H | Show / hide controls · 显示 / 隐藏控制栏 |
20
- | Control + Command + F | Full screen · 全屏 |
21
- | Command + Q | Quit · 退出 |
22
-
23
- Disable “全部连播” to keep looping the selected group. Move the pointer away and hide the controls before recording. The regular helper and DSH can remain running.
@@ -1,41 +0,0 @@
1
- #!/bin/sh
2
- set -eu
3
- repo=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
4
- export DEVELOPER_DIR="${DEVELOPER_DIR:-/Library/Developer/CommandLineTools}"
5
- work="$repo/.build/recording"
6
- mkdir -p "$work/Sources" "$repo/dist"
7
- python3 - "$repo" "$work" <<'PY'
8
- from pathlib import Path
9
- import shutil,sys
10
- repo,work=map(Path,sys.argv[1:])
11
- for name in ['RootView.swift','StatusOrbit.swift','IdleRobot.swift','Panel.swift','NotchMarkdown.swift']:
12
- shutil.copy2(repo/'macos/Sources'/name,work/'Sources'/name)
13
- for name in ['DemoCatalog.swift','main.swift','Client.swift']:
14
- shutil.copy2(repo/'tools/recording'/name,work/'Sources'/name)
15
- shutil.copytree(repo/'macos/Sources/Resources',work/'Sources/Resources',dirs_exist_ok=True)
16
- p=work/'Sources/IdleRobot.swift';s=p.read_text()
17
- old='.onAppear { presence.set(idle, director: director, animated: false) }'
18
- new='''.onAppear { director.automaticActions=false;presence.set(idle, director: director, animated: false) }
19
- .onReceive(NotificationCenter.default.publisher(for:PreviewIdleCue.notification)) { note in
20
- guard let cue=note.object as? PreviewIdleCue,cue.boardID==ObjectIdentifier(model) else {return}
21
- director.automaticActions=false;director.play(cue.action)
22
- }'''
23
- assert s.count(old)==1,'Idle playback hook changed; inspect the production source before updating this adapter.'
24
- p.write_text(s.replace(old,new))
25
- (work/'Package.swift').write_text('''// swift-tools-version: 6.0
26
- import PackageDescription
27
- let package = Package(name:"NotchDemo",platforms:[.macOS(.v14)],products:[.executable(name:"NotchDemo",targets:["NotchDemo"])],targets:[.executableTarget(name:"NotchDemo",path:"Sources",resources:[.copy("Resources/Idle")])])
28
- ''')
29
- PY
30
- swift build --package-path "$work" -c release
31
- python3 - "$repo" "$work" <<'PY'
32
- from pathlib import Path
33
- import shutil,plistlib,sys
34
- repo,work=map(Path,sys.argv[1:]);app=repo/'dist/DSH Notch Demo.app/Contents'
35
- (app/'MacOS').mkdir(parents=True,exist_ok=True);(app/'Resources').mkdir(exist_ok=True)
36
- shutil.copy2(work/'.build/release/NotchDemo',app/'MacOS/NotchDemo')
37
- for bundle in (work/'.build/release').glob('*.bundle'):
38
- shutil.copytree(bundle,app/'Resources'/bundle.name,dirs_exist_ok=True)
39
- (app/'Info.plist').write_bytes(plistlib.dumps({'CFBundleIdentifier':'local.dsh.notch.demo','CFBundleName':'DSH Notch Demo','CFBundleExecutable':'NotchDemo','CFBundlePackageType':'APPL','CFBundleShortVersionString':'0.3.0','CFBundleVersion':'3','NSHighResolutionCapable':True,'LSMinimumSystemVersion':'14.0'}))
40
- print(app.parent)
41
- PY
@@ -1,100 +0,0 @@
1
- import AppKit
2
- import SwiftUI
3
-
4
- struct RecordingTileView:View {
5
- let tile:DemoTile
6
- let solo:Bool
7
- @ObservedObject var board:BoardModel
8
- init(tile:DemoTile,solo:Bool) {self.tile=tile;self.solo=solo;board=tile.board}
9
- var body:some View {
10
- GeometryReader {geo in
11
- let scale:CGFloat=solo ? min(5.4,(geo.size.height-140)/110):min(2.65,(geo.size.height-104)/104)
12
- VStack(spacing:0) {
13
- VStack(spacing:5) {
14
- Text(tile.scene.title).font(.system(size:solo ? 40:24,weight:.semibold))
15
- .foregroundStyle(Color(white:0.14))
16
- Text(tile.scene.englishTitle).font(.system(size:solo ? 27:17,weight:.medium))
17
- .foregroundStyle(Color(white:0.34))
18
- }.lineLimit(1).minimumScaleFactor(0.7)
19
- .frame(height:solo ? 130:82)
20
- ZStack {
21
- RootView(model:board,panelSize:CGSize(width:340,height:260),restSize:CGSize(width:38,height:44))
22
- .frame(width:38,height:max(44,board.orbitLayout.height+24))
23
- .scaleEffect(scale).allowsHitTesting(false)
24
- }.frame(maxWidth:.infinity,maxHeight:.infinity)
25
- }.frame(width:geo.size.width,height:geo.size.height)
26
- }
27
- }
28
- }
29
-
30
- struct RecordingView:View {
31
- @ObservedObject var model:RecordingModel
32
- var body:some View {
33
- GeometryReader {geo in
34
- ZStack(alignment:.bottom) {
35
- Color(white:0.965)
36
- if model.solo {
37
- if let tile=model.tiles.first {
38
- RecordingTileView(tile:tile,solo:true).id(tile.id).padding(.horizontal,60).padding(.top,22).padding(.bottom,80)
39
- }
40
- } else {
41
- VStack(spacing:16) {
42
- ForEach(0..<2,id:\.self) {row in
43
- HStack(spacing:26) {
44
- ForEach(Array(model.tiles.dropFirst(row*3).prefix(3))) {tile in
45
- RecordingTileView(tile:tile,solo:false).id(tile.id)
46
- }
47
- }
48
- }
49
- }.padding(.horizontal,38).padding(.top,16).padding(.bottom,68)
50
- }
51
- HStack(spacing:18) {
52
- Button {model.move(-1)} label:{Image(systemName:"chevron.left")}.keyboardShortcut(.leftArrow,modifiers:[]).help("上一组")
53
- Text("\(model.page+1) / \(model.count)").monospacedDigit().foregroundStyle(.secondary)
54
- Button {model.move(1)} label:{Image(systemName:"chevron.right")}.keyboardShortcut(.rightArrow,modifiers:[]).help("下一组")
55
- Divider().frame(height:18)
56
- Button {model.replay()} label:{Image(systemName:"arrow.counterclockwise")}.keyboardShortcut("r",modifiers:[]).help("重播")
57
- Toggle("全部连播",isOn:$model.autoplay).toggleStyle(.checkbox)
58
- Button {model.controls.toggle()} label:{Image(systemName:"eye.slash")}.keyboardShortcut("h",modifiers:[]).help("显示 / 隐藏控制栏")
59
- Button {model.toggleLayout()} label:{Image(systemName:model.solo ? "square.grid.3x2":"rectangle")}.keyboardShortcut("s",modifiers:[]).help("并排 / 单场景")
60
- Button {NSApp.keyWindow?.toggleFullScreen(nil)} label:{Image(systemName:"arrow.up.left.and.arrow.down.right")}.keyboardShortcut("f",modifiers:[.command,.control]).help("全屏")
61
- }.font(.system(size:14)).buttonStyle(.plain)
62
- .padding(.horizontal,22).padding(.vertical,14).background(.regularMaterial,in:Capsule())
63
- .padding(.bottom,15).opacity(model.controls ? 1:0).allowsHitTesting(model.controls)
64
- .animation(.easeOut(duration:0.15),value:model.controls)
65
- }.onContinuousHover {phase in
66
- switch phase {case .active(let p):model.controls=p.y > geo.size.height-70
67
- case .ended:model.controls=false}
68
- }
69
- }.frame(minWidth:900,minHeight:620)
70
- }
71
- }
72
-
73
- @MainActor final class RecordingDelegate:NSObject,NSApplicationDelegate,NSWindowDelegate {
74
- let model=RecordingModel()
75
- var window:NSWindow?
76
- func applicationDidFinishLaunching(_ notification:Notification) {
77
- let window=NSWindow(contentRect:NSRect(x:0,y:0,width:1200,height:780),styleMask:[.titled,.closable,.miniaturizable,.resizable,.fullSizeContentView],backing:.buffered,defer:false)
78
- window.title="DSH Notch · Demo"
79
- window.titleVisibility = .hidden;window.titlebarAppearsTransparent=true
80
- window.contentView=NSHostingView(rootView:RecordingView(model:model))
81
- window.backgroundColor=NSColor(white:0.965,alpha:1)
82
- window.isReleasedWhenClosed=false;window.delegate=self
83
- window.collectionBehavior=[.fullScreenPrimary]
84
- window.center();window.makeKeyAndOrderFront(nil);self.window=window
85
- NSApp.activate(ignoringOtherApps:true);model.replay()
86
- }
87
- func windowWillClose(_ notification:Notification) {model.stop();NSApp.terminate(nil)}
88
- func applicationShouldTerminateAfterLastWindowClosed(_ sender:NSApplication)->Bool {true}
89
- }
90
-
91
- @main enum RecordingMain {
92
- @MainActor static func main() {
93
- let app=NSApplication.shared;app.setActivationPolicy(.regular)
94
- let menu=NSMenu(),item=NSMenuItem(),sub=NSMenu()
95
- sub.addItem(withTitle:"退出演示",action:#selector(NSApplication.terminate(_:)),keyEquivalent:"q")
96
- item.submenu=sub;menu.addItem(item);app.mainMenu=menu
97
- let delegate=RecordingDelegate();app.delegate=delegate
98
- withExtendedLifetime(delegate) {app.run()}
99
- }
100
- }
@@ -1,236 +0,0 @@
1
- import AppKit
2
- import SwiftUI
3
-
4
- /// Earlier recording-stage reference. The supported recorder is in tools/recording.
5
- @MainActor
6
- final class DemoStudio: ObservableObject {
7
- @Published var board = BoardModel()
8
- @Published var caption = "按「开始」再录"
9
- @Published var playing = false
10
- private var job: Task<Void, Never>?
11
- private var generation = 0
12
-
13
- init() { resetBoard() }
14
-
15
- func reset() {
16
- job?.cancel()
17
- job = nil
18
- playing = false
19
- generation += 1
20
- caption = "按「开始」再录"
21
- resetBoard()
22
- IdleDirector.previewInstance?.automaticActions = false
23
- }
24
-
25
- func start() {
26
- job?.cancel()
27
- generation += 1
28
- let token = generation
29
- resetBoard()
30
- playing = true
31
- job = Task { @MainActor in
32
- await runScript(token)
33
- }
34
- }
35
-
36
- private func resetBoard() {
37
- board = BoardModel()
38
- board.previewMode = true
39
- board.maximumExpandedHeight = 460
40
- board.expanded = false
41
- }
42
-
43
- private func snap(_ rows: [NotchRow]) {
44
- board.applySnapshot(NotchSnapshot(ok: true, generatedAt: Date().timeIntervalSince1970, origin: "demo", rows: rows))
45
- }
46
-
47
- private func busy(_ n: Int) -> [NotchRow] {
48
- (0..<n).map { i in
49
- NotchRow(id: "demo-\(i)", title: "任务 \(i + 1)", child: false, busy: true, unread: false, lastTurn: nil, approval: nil, ask: nil)
50
- }
51
- }
52
-
53
- private func mixed() -> [NotchRow] {
54
- [
55
- NotchRow(id: "demo-0", title: "任务 1", child: false, busy: false, unread: true, lastTurn: nil, approval: nil, ask: nil),
56
- NotchRow(
57
- id: "demo-1",
58
- title: "任务 2",
59
- child: false,
60
- busy: false,
61
- unread: true,
62
- lastTurn: NotchLastTurn(at: Date().timeIntervalSince1970, kind: "error", failed: true),
63
- approval: nil,
64
- ask: nil
65
- ),
66
- NotchRow(
67
- id: "demo-2",
68
- title: "任务 3",
69
- child: false,
70
- busy: false,
71
- unread: false,
72
- lastTurn: nil,
73
- approval: nil,
74
- ask: NotchAsk(id: "demo-ask", questions: [
75
- NotchQuestion(
76
- id: "q1",
77
- question: "这条任务下一步怎么走?",
78
- detail: "录屏用的假问题,点任意一项即可。",
79
- header: nil,
80
- options: [
81
- NotchOption(label: "先改动画", description: "收起和展开再顺一点"),
82
- NotchOption(label: "先改文案", description: "字幕再短一点"),
83
- NotchOption(label: "都行", description: "你选就好"),
84
- ],
85
- multiSelect: false
86
- )
87
- ])
88
- ),
89
- NotchRow(id: "demo-3", title: "任务 4", child: false, busy: true, unread: false, lastTurn: nil, approval: nil, ask: nil),
90
- ]
91
- }
92
-
93
- private func runScript(_ token: Int) async {
94
- func alive() -> Bool { token == generation && !Task.isCancelled }
95
-
96
- try? await Task.sleep(for: .milliseconds(400))
97
- guard alive() else { return }
98
- IdleDirector.previewInstance?.automaticActions = false
99
- let tour = IdleDirector.basics + ["dance"]
100
- for id in tour {
101
- guard alive() else { return }
102
- caption = "待机 · \(id)"
103
- IdleDirector.previewInstance?.play(id)
104
- let wait = IdleDirector.previewInstance?.currentDuration ?? 7
105
- try? await Task.sleep(for: .seconds(wait))
106
- }
107
-
108
- guard alive() else { return }
109
- caption = "4 个任务开始运行"
110
- snap(busy(4))
111
- try? await Task.sleep(for: .seconds(2.2))
112
-
113
- guard alive() else { return }
114
- caption = "请把鼠标移到黄点上,点一个选项"
115
- snap(mixed())
116
-
117
- while alive() && board.needsAction {
118
- try? await Task.sleep(for: .milliseconds(120))
119
- }
120
- guard alive() else { return }
121
- caption = "请点红点"
122
-
123
- while alive() && !board.failedRows.isEmpty {
124
- try? await Task.sleep(for: .milliseconds(120))
125
- }
126
- guard alive() else { return }
127
- caption = "蓝灯即将结束"
128
- try? await Task.sleep(for: .seconds(2.6))
129
- guard alive() else { return }
130
- snap(board.rows.filter { !$0.busy })
131
- caption = "请点绿点"
132
-
133
- while alive() && board.completedUnreadCount > 0 {
134
- try? await Task.sleep(for: .milliseconds(120))
135
- }
136
- guard alive() else { return }
137
- caption = "结束 · 可按「重来」"
138
- playing = false
139
- }
140
- }
141
-
142
- struct DemoStudioView: View {
143
- @ObservedObject var studio: DemoStudio
144
- @State private var hoverNotch = false
145
-
146
- private var previewWidth: CGFloat {
147
- studio.board.expanded ? 320 : (studio.board.isPillHovered ? 42 : 38)
148
- }
149
- private var previewHeight: CGFloat {
150
- if studio.board.expanded {
151
- return min(max(studio.board.measuredContentHeight, studio.board.needsAction ? 280 : 160), 420)
152
- }
153
- return max(44, studio.board.orbitLayout.height + 24)
154
- }
155
-
156
- var body: some View {
157
- HStack(alignment: .top, spacing: 24) {
158
- VStack(alignment: .leading, spacing: 12) {
159
- Text("录屏模拟台").font(.system(size: 18, weight: .semibold))
160
- Text(studio.caption)
161
- .font(.system(size: 13))
162
- .foregroundStyle(.secondary)
163
- .frame(minHeight: 36, alignment: .topLeading)
164
- ZStack(alignment: .topTrailing) {
165
- RoundedRectangle(cornerRadius: 16, style: .continuous)
166
- .fill(Color(red: 0.07, green: 0.07, blue: 0.08))
167
- RootView(
168
- model: studio.board,
169
- panelSize: CGSize(width: 320, height: 460),
170
- restSize: CGSize(width: 38, height: 44)
171
- )
172
- .frame(width: previewWidth, height: previewHeight)
173
- .clipped()
174
- .onHover { hovering in
175
- hoverNotch = hovering
176
- if hovering && studio.board.needsAction {
177
- studio.board.expanded = true
178
- } else if !hovering {
179
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.38) {
180
- if !hoverNotch { studio.board.expanded = false }
181
- }
182
- }
183
- }
184
- }
185
- .frame(width: 360, height: 440, alignment: .topTrailing)
186
- .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
187
- }
188
- VStack(alignment: .leading, spacing: 10) {
189
- Button(studio.playing ? "进行中…" : "开始") { studio.start() }
190
- .disabled(studio.playing)
191
- .keyboardShortcut(.defaultAction)
192
- Button("重来") { studio.reset() }
193
- Text("不接 Host,不影响正在跑的 notch。\n黄 / 红 / 绿需要你点。")
194
- .font(.system(size: 11))
195
- .foregroundStyle(.secondary)
196
- .padding(.top, 8)
197
- Spacer()
198
- }
199
- .frame(width: 180)
200
- }
201
- .padding(24)
202
- .frame(minWidth: 640, minHeight: 520)
203
- }
204
- }
205
-
206
- @MainActor
207
- final class DemoStudioDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
208
- let studio = DemoStudio()
209
- var window: NSWindow?
210
- func applicationDidFinishLaunching(_ notification: Notification) {
211
- setbuf(stdout, nil)
212
- let window = NSPanel(
213
- contentRect: NSRect(x: 0, y: 0, width: 720, height: 560),
214
- styleMask: [.titled, .closable, .resizable, .nonactivatingPanel, .hudWindow],
215
- backing: .buffered,
216
- defer: false
217
- )
218
- window.title = "dsh-notch 录屏模拟台"
219
- window.delegate = self
220
- window.isReleasedWhenClosed = false
221
- window.isFloatingPanel = true
222
- window.hidesOnDeactivate = false
223
- window.level = .floating
224
- window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
225
- window.contentView = NSHostingView(rootView: DemoStudioView(studio: studio))
226
- let screen = NSScreen.screens.first { abs($0.frame.minX) < 1 && abs($0.frame.minY) < 1 } ?? NSScreen.main
227
- if let vis = screen?.visibleFrame {
228
- window.setFrame(NSRect(x: vis.midX - 360, y: vis.maxY - 620, width: 720, height: 560), display: true)
229
- }
230
- window.orderFrontRegardless()
231
- NSApplication.shared.activate(ignoringOtherApps: true)
232
- self.window = window
233
- print("DEMO_STUDIO ready")
234
- }
235
- func windowWillClose(_ notification: Notification) { NSApplication.shared.terminate(nil) }
236
- }
@@ -1,19 +0,0 @@
1
- #!/bin/sh
2
- set -eu
3
- repo=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
4
- work=$(mktemp -d "${TMPDIR:-/tmp}/notch-audit.XXXXXX")
5
- output=${1:-"$work/frames"}
6
- mkdir -p "$output"
7
- cp "$repo/macos/Package.swift" "$work/Package.swift"
8
- cp -R "$repo/macos/Sources" "$work/Sources"
9
- python3 - "$repo" "$work" <<'PYTHON'
10
- from pathlib import Path
11
- import sys
12
- repo,work=map(Path,sys.argv[1:])
13
- s=(repo/'tools/review/main.swift').read_text().split('@main enum LoopMain')[0]
14
- s+=(repo/'tools/review/render-audit.swift').read_text()
15
- (work/'Sources/main.swift').write_text(s)
16
- p=work/'Sources/IdleRobot.swift';s=p.read_text().replace('final class IdleDirector: ObservableObject {','final class IdleDirector: ObservableObject {\n static weak var previewInstance: IdleDirector?').replace(' func start(playImmediately: Bool = true) {',' func start(playImmediately: Bool = true) {\n Self.previewInstance=self');p.write_text(s)
17
- PYTHON
18
- swift build --package-path "$work" -c release -j 2
19
- NOTCH_AUDIT_OUTPUT="$output" "$work/.build/release/dsh-notch"