poke-gate 0.1.9 → 0.2.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.
Files changed (32) hide show
  1. package/.github/workflows/release.yml +53 -3
  2. package/Gate.app +0 -0
  3. package/README.md +48 -14
  4. package/bin/poke-gate.js +17 -0
  5. package/clients/Poke macOS Gate/Poke macOS Gate/AboutView.swift +7 -1
  6. package/clients/Poke macOS Gate/Poke macOS Gate/AccessibilityPermissionView.swift +58 -0
  7. package/clients/Poke macOS Gate/Poke macOS Gate/GateService.swift +389 -23
  8. package/clients/Poke macOS Gate/Poke macOS Gate/Info.plist +2 -0
  9. package/clients/Poke macOS Gate/Poke macOS Gate/LogsView.swift +1 -1
  10. package/clients/Poke macOS Gate/Poke macOS Gate/MacVisualStyle.swift +89 -0
  11. package/clients/Poke macOS Gate/Poke macOS Gate/PermissionRowView.swift +55 -0
  12. package/clients/Poke macOS Gate/Poke macOS Gate/Poke_macOS_GateApp.swift +234 -91
  13. package/clients/Poke macOS Gate/Poke macOS Gate/SettingsView.swift +125 -81
  14. package/clients/Poke macOS Gate/Poke macOS Gate/SetupView.swift +157 -0
  15. package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj +31 -11
  16. package/docs/cli.md +19 -0
  17. package/docs/getting-started.md +9 -6
  18. package/docs/index.md +23 -18
  19. package/docs/macos-app.md +39 -4
  20. package/docs/security.md +62 -18
  21. package/examples/agents/battery.30m.js +1 -1
  22. package/examples/agents/screentime.24h.js +5 -6
  23. package/macOS +0 -0
  24. package/package.json +3 -1
  25. package/src/agents.js +5 -8
  26. package/src/app.js +29 -5
  27. package/src/mcp-server.js +502 -27
  28. package/src/permission-service.js +128 -0
  29. package/test/mcp-server-access-policy.test.js +40 -0
  30. package/test/mcp-server-loop-guard.test.js +57 -0
  31. package/test/mcp-server-sandbox-command.test.js +18 -0
  32. package/test/permission-service.test.js +97 -0
@@ -24,7 +24,10 @@ struct Poke_macOS_GateApp: App {
24
24
  .onAppear {
25
25
  service.autoStartIfNeeded()
26
26
  appDelegate.service = service
27
- checkLoginItemPrompt()
27
+ service.startPermissionPolling()
28
+ }
29
+ .onDisappear {
30
+ service.stopPermissionPolling()
28
31
  }
29
32
  } label: {
30
33
  Image(systemName: menuBarIcon)
@@ -36,6 +39,11 @@ struct Poke_macOS_GateApp: App {
36
39
  }
37
40
  .defaultSize(width: 560, height: 400)
38
41
 
42
+ Window("Setup", id: "setup") {
43
+ SetupView(service: service)
44
+ }
45
+ .windowResizability(.contentSize)
46
+
39
47
  Window("Settings", id: "settings") {
40
48
  SettingsView(service: service)
41
49
  }
@@ -96,128 +104,232 @@ struct Poke_macOS_GateApp: App {
96
104
  struct PopoverContent: View {
97
105
  @ObservedObject var service: GateService
98
106
  @Environment(\.openWindow) private var openWindow
107
+ @State private var pendingFullMode = false
99
108
 
100
109
  var body: some View {
101
- VStack(spacing: 0) {
102
- VStack(spacing: 6) {
103
- HStack(spacing: 8) {
104
- Circle()
105
- .fill(statusColor)
106
- .frame(width: 10, height: 10)
110
+ if !service.hasCompletedSetup {
111
+ SetupView(service: service)
112
+ } else {
113
+ VStack(spacing: 10) {
114
+ statusSection
115
+ recentActivitySection
116
+ accessModeSection
117
+ actionsSection
118
+ footerSection
119
+ }
120
+ .frame(width: 320)
121
+ .padding(10)
122
+ .onChange(of: service.hasSystemPermissionsGranted) { _, granted in
123
+ if granted && pendingFullMode {
124
+ pendingFullMode = false
125
+ service.setPermissionMode(.full)
126
+ }
127
+ }
128
+ }
129
+ }
107
130
 
108
- Text(statusText)
109
- .font(.system(.body, weight: .medium))
131
+ private var statusSection: some View {
132
+ VStack(alignment: .leading, spacing: 6) {
133
+ HStack(spacing: 8) {
134
+ Circle()
135
+ .fill(statusColor)
136
+ .frame(width: 10, height: 10)
110
137
 
111
- Spacer()
112
- }
138
+ Text(statusText)
139
+ .font(.system(.body, weight: .medium))
113
140
 
114
- if service.status == .connected {
115
- Text("This machine is accessible via Poke. Ask your Poke to run commands or read files.")
116
- .font(.caption)
117
- .foregroundStyle(.secondary)
118
- .frame(maxWidth: .infinity, alignment: .leading)
119
- .fixedSize(horizontal: false, vertical: true)
120
- } else if service.status == .starting {
121
- Text("Establishing connection…")
122
- .font(.caption)
123
- .foregroundStyle(.secondary)
124
- .frame(maxWidth: .infinity, alignment: .leading)
125
- } else if service.status == .error {
126
- Text("Check Logs for details.")
127
- .font(.caption)
128
- .foregroundStyle(.red.opacity(0.8))
129
- .frame(maxWidth: .infinity, alignment: .leading)
130
- }
141
+ Spacer()
131
142
  }
132
- .padding(12)
133
143
 
134
- Divider()
144
+ statusMessage
145
+ }
146
+ .frame(maxWidth: .infinity, alignment: .leading)
147
+ .padding(12)
148
+ .macPanelStyle(.neutral, cornerRadius: 12)
149
+ }
135
150
 
136
- VStack(alignment: .leading, spacing: 4) {
137
- Text("Recent activity")
138
- .font(.caption2)
139
- .foregroundStyle(.tertiary)
140
- .textCase(.uppercase)
141
-
142
- if service.logs.isEmpty {
143
- Text("No activity yet")
144
- .font(.caption)
145
- .foregroundStyle(.secondary)
146
- } else {
147
- ForEach(Array(service.logs.suffix(4).enumerated()), id: \.offset) { _, line in
148
- Text(line)
151
+ @ViewBuilder
152
+ private var statusMessage: some View {
153
+ switch service.status {
154
+ case .connected:
155
+ Text("This machine is accessible via Poke. Ask your Poke to run commands or read files.")
156
+ .font(.caption)
157
+ .foregroundStyle(.secondary)
158
+ .frame(maxWidth: .infinity, alignment: .leading)
159
+ .fixedSize(horizontal: false, vertical: true)
160
+ case .starting:
161
+ Text("Establishing connection…")
162
+ .font(.caption)
163
+ .foregroundStyle(.secondary)
164
+ .frame(maxWidth: .infinity, alignment: .leading)
165
+ case .error:
166
+ Text("Check Logs for details.")
167
+ .font(.caption)
168
+ .foregroundStyle(.red.opacity(0.8))
169
+ .frame(maxWidth: .infinity, alignment: .leading)
170
+ case .disconnected, .stopped:
171
+ EmptyView()
172
+ }
173
+ }
174
+
175
+ private var recentActivitySection: some View {
176
+ VStack(alignment: .leading, spacing: 6) {
177
+ sectionTitle("Recent activity")
178
+
179
+ if !service.terminalPreviews.isEmpty {
180
+ ForEach(Array(service.terminalPreviews.suffix(4).enumerated()), id: \.element.id) { _, entry in
181
+ HStack(spacing: 6) {
182
+ Circle()
183
+ .fill(entry.exitCode == 0 ? Color.green : (entry.exitCode == nil ? Color.gray : Color.red))
184
+ .frame(width: 5, height: 5)
185
+ Text("$ \(entry.command)")
149
186
  .font(.system(size: 9, design: .monospaced))
150
187
  .foregroundStyle(.tertiary)
151
188
  .lineLimit(1)
152
189
  .truncationMode(.tail)
153
190
  }
154
191
  }
192
+ } else if !service.logs.isEmpty {
193
+ ForEach(Array(service.logs.suffix(4).enumerated()), id: \.offset) { _, log in
194
+ Text(log)
195
+ .font(.system(size: 9, design: .monospaced))
196
+ .foregroundStyle(.tertiary)
197
+ .lineLimit(1)
198
+ .truncationMode(.tail)
199
+ }
200
+ } else {
201
+ Text("No activity yet")
202
+ .font(.caption)
203
+ .foregroundStyle(.secondary)
155
204
  }
156
- .frame(maxWidth: .infinity, alignment: .leading)
157
- .padding(12)
205
+ }
206
+ .frame(maxWidth: .infinity, alignment: .leading)
207
+ .padding(12)
208
+ .macPanelStyle(.neutral, cornerRadius: 12)
209
+ }
158
210
 
159
- Divider()
211
+ private var accessModeSection: some View {
212
+ VStack(alignment: .leading, spacing: 8) {
213
+ HStack {
214
+ sectionTitle("Access mode")
215
+ Spacer()
216
+ Text(service.permissionMode.title)
217
+ .font(.caption2)
218
+ .foregroundStyle(.secondary)
219
+ }
160
220
 
161
- HStack(spacing: 12) {
162
- ActionButton(icon: "text.alignleft", label: "Logs") {
163
- NSApp.activate(ignoringOtherApps: true)
164
- openWindow(id: "logs")
221
+ HStack(spacing: 6) {
222
+ ForEach(GateService.PermissionMode.allCases) { mode in
223
+ let isActive = service.permissionMode == mode || (mode == .full && pendingFullMode)
224
+ Button {
225
+ handleModeSelection(mode)
226
+ } label: {
227
+ Text(modeChipTitle(mode))
228
+ .font(.system(size: 10, weight: .semibold))
229
+ .foregroundStyle(isActive ? .white : .secondary)
230
+ .padding(.vertical, 5)
231
+ .padding(.horizontal, 8)
232
+ .background(isActive ? MacVisualStyle.chipActiveFill : MacVisualStyle.chipInactiveFill)
233
+ .clipShape(Capsule())
234
+ }
235
+ .buttonStyle(.plain)
165
236
  }
237
+ }
166
238
 
167
- ActionButton(icon: "bolt.fill", label: "Agents") {
168
- NSApp.activate(ignoringOtherApps: true)
169
- openWindow(id: "agents")
170
- }
239
+ Text(activeModeDescription)
240
+ .font(.caption2)
241
+ .foregroundStyle(.secondary)
242
+ .fixedSize(horizontal: false, vertical: true)
171
243
 
172
- ActionButton(icon: "gearshape", label: "Settings") {
173
- NSApp.activate(ignoringOtherApps: true)
174
- openWindow(id: "settings")
175
- }
244
+ if service.permissionMode == .full || pendingFullMode {
245
+ AccessibilityPermissionView(service: service)
246
+ }
247
+ }
248
+ .frame(maxWidth: .infinity, alignment: .leading)
249
+ .padding(12)
250
+ .macPanelStyle(.neutral, cornerRadius: 12)
251
+ }
176
252
 
177
- if service.status == .connected || service.status == .starting || service.status == .disconnected {
178
- ActionButton(icon: "arrow.counterclockwise", label: "Restart") {
179
- service.restart()
180
- }
181
- } else {
182
- ActionButton(icon: "play.fill", label: "Start") {
183
- service.start()
184
- }
185
- }
253
+ private var activeModeDescription: String {
254
+ let mode = pendingFullMode ? GateService.PermissionMode.full : service.permissionMode
255
+ switch mode {
256
+ case .full:
257
+ return "All tools enabled. Risky actions require chat approval."
258
+ case .limited:
259
+ return "Read-only tools and safe commands only (ls, cat, grep, curl…). File writes and screenshots are disabled."
260
+ case .sandbox:
261
+ return "Broader commands (brew, node, python, ffmpeg…) but file writes restricted to ~/Downloads and /tmp via macOS sandbox."
262
+ }
263
+ }
186
264
 
187
- Spacer()
265
+ private var actionsSection: some View {
266
+ HStack(spacing: 12) {
267
+ ActionButton(icon: "text.alignleft", label: "Logs") {
268
+ NSApp.activate(ignoringOtherApps: true)
269
+ openWindow(id: "logs")
270
+ }
188
271
 
189
- ActionButton(icon: "xmark.circle", label: "Quit", tint: .secondary) {
190
- service.stop()
191
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
192
- NSApp.terminate(nil)
193
- }
194
- }
272
+ ActionButton(icon: "bolt.fill", label: "Agents") {
273
+ NSApp.activate(ignoringOtherApps: true)
274
+ openWindow(id: "agents")
195
275
  }
196
- .padding(12)
197
276
 
198
- Divider()
277
+ ActionButton(icon: "gearshape", label: "Settings") {
278
+ NSApp.activate(ignoringOtherApps: true)
279
+ openWindow(id: "settings")
280
+ }
199
281
 
200
- HStack {
201
- Button {
202
- NSApp.activate(ignoringOtherApps: true)
203
- openWindow(id: "about")
204
- } label: {
205
- Text("Poke Gate v0.0.8")
206
- .font(.caption2)
207
- .foregroundStyle(.tertiary)
282
+ if service.status == .connected || service.status == .starting || service.status == .disconnected {
283
+ ActionButton(icon: "arrow.counterclockwise", label: "Restart") {
284
+ service.restart()
285
+ }
286
+ } else {
287
+ ActionButton(icon: "play.fill", label: "Start") {
288
+ service.start()
208
289
  }
209
- .buttonStyle(.plain)
290
+ }
210
291
 
211
- Spacer()
292
+ Spacer()
212
293
 
213
- Text("Not affiliated with Poke")
294
+ ActionButton(icon: "xmark.circle", label: "Quit", tint: .secondary) {
295
+ service.stop()
296
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
297
+ NSApp.terminate(nil)
298
+ }
299
+ }
300
+ }
301
+ .padding(12)
302
+ .frame(maxWidth: .infinity, alignment: .leading)
303
+ .macPanelStyle(.neutral, cornerRadius: 12)
304
+ }
305
+
306
+ private var footerSection: some View {
307
+ HStack {
308
+ Button {
309
+ NSApp.activate(ignoringOtherApps: true)
310
+ openWindow(id: "about")
311
+ } label: {
312
+ Text(appVersionText)
214
313
  .font(.caption2)
215
- .foregroundStyle(.quaternary)
314
+ .foregroundStyle(MacVisualStyle.sectionTitleColor)
216
315
  }
217
- .padding(.horizontal, 12)
218
- .padding(.vertical, 8)
316
+ .buttonStyle(.plain)
317
+
318
+ Spacer()
319
+
320
+ Text("Not affiliated with Poke")
321
+ .font(.caption2)
322
+ .foregroundStyle(MacVisualStyle.sectionTitleColor.opacity(0.7))
219
323
  }
220
- .frame(width: 320)
324
+ .padding(.horizontal, 6)
325
+ }
326
+
327
+ private func sectionTitle(_ text: String) -> some View {
328
+ Text(text)
329
+ .font(.caption2)
330
+ .foregroundStyle(MacVisualStyle.sectionTitleColor)
331
+ .textCase(.uppercase)
332
+ .tracking(0.5)
221
333
  }
222
334
 
223
335
  private var statusText: String {
@@ -242,6 +354,37 @@ struct PopoverContent: View {
242
354
  case .stopped: .gray.opacity(0.5)
243
355
  }
244
356
  }
357
+
358
+ private func modeChipTitle(_ mode: GateService.PermissionMode) -> String {
359
+ switch mode {
360
+ case .full: return "Full"
361
+ case .limited: return "Limited"
362
+ case .sandbox: return "Sandbox"
363
+ }
364
+ }
365
+
366
+ private func handleModeSelection(_ mode: GateService.PermissionMode) {
367
+ guard mode != service.permissionMode else { return }
368
+
369
+ if mode == .full && !service.hasSystemPermissionsGranted {
370
+ pendingFullMode = true
371
+ service.openSystemPermission(.accessibility)
372
+ } else {
373
+ pendingFullMode = false
374
+ service.setPermissionMode(mode)
375
+ }
376
+ }
377
+
378
+ private var appVersionText: String {
379
+ let short = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0"
380
+ let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
381
+
382
+ if let build, !build.isEmpty, build != short {
383
+ return "Poke Gate v\(short) (\(build))"
384
+ }
385
+
386
+ return "Poke Gate v\(short)"
387
+ }
245
388
  }
246
389
 
247
390
  struct ActionButton: View {
@@ -8,90 +8,14 @@ struct SettingsView: View {
8
8
 
9
9
  var body: some View {
10
10
  VStack(alignment: .leading, spacing: 16) {
11
- VStack(alignment: .leading, spacing: 8) {
12
- Text("AUTHENTICATION")
13
- .font(.caption2)
14
- .foregroundStyle(.tertiary)
15
- .textCase(.uppercase)
16
- .tracking(0.5)
17
-
18
- HStack(spacing: 8) {
19
- Image(systemName: service.hasPokeLoginCredentials
20
- ? "checkmark.shield.fill" : "shield.slash")
21
- .foregroundStyle(service.hasPokeLoginCredentials ? .green : .orange)
22
- .font(.title3)
23
-
24
- VStack(alignment: .leading, spacing: 2) {
25
- Text(service.hasPokeLoginCredentials
26
- ? "Signed in via Poke"
27
- : "Not signed in")
28
- .font(.subheadline)
29
- .fontWeight(.medium)
30
-
31
- if service.hasPokeLoginCredentials {
32
- Text("Your Poke session is active.")
33
- .font(.caption)
34
- .foregroundStyle(.secondary)
35
- } else {
36
- Text("Run this command in Terminal to sign in:")
37
- .font(.caption)
38
- .foregroundStyle(.secondary)
39
- }
40
- }
41
- }
42
- .padding(10)
43
- .frame(maxWidth: .infinity, alignment: .leading)
44
- .background(.quaternary.opacity(0.5))
45
- .cornerRadius(8)
46
-
47
- if !service.hasPokeLoginCredentials {
48
- Button {
49
- service.runPokeLogin()
50
- } label: {
51
- Label("Sign in with Poke", systemImage: "person.crop.circle.badge.plus")
52
- }
53
- .controlSize(.large)
54
-
55
- Text("Opens a browser window to sign in.")
56
- .font(.caption)
57
- .foregroundStyle(.secondary)
58
- }
59
- }
60
-
61
- VStack(alignment: .leading, spacing: 8) {
62
- Text("CONNECTION")
63
- .font(.caption2)
64
- .foregroundStyle(.tertiary)
65
- .textCase(.uppercase)
66
- .tracking(0.5)
67
-
68
- HStack(spacing: 8) {
69
- Circle()
70
- .fill(connectionColor)
71
- .frame(width: 8, height: 8)
72
-
73
- Text(service.status.rawValue)
74
- .font(.subheadline)
75
-
76
- Spacer()
77
-
78
- Button {
79
- service.restart()
80
- } label: {
81
- Label("Reconnect", systemImage: "arrow.counterclockwise")
82
- .font(.caption)
83
- }
84
- }
85
- .padding(10)
86
- .frame(maxWidth: .infinity, alignment: .leading)
87
- .background(.quaternary.opacity(0.5))
88
- .cornerRadius(8)
89
- }
11
+ authenticationSection
12
+ accessModeSection
13
+ connectionSection
90
14
 
91
15
  VStack(alignment: .leading, spacing: 8) {
92
16
  Text("GENERAL")
93
17
  .font(.caption2)
94
- .foregroundStyle(.tertiary)
18
+ .foregroundStyle(MacVisualStyle.sectionTitleColor)
95
19
  .textCase(.uppercase)
96
20
  .tracking(0.5)
97
21
 
@@ -119,7 +43,127 @@ struct SettingsView: View {
119
43
  }
120
44
  }
121
45
  .padding(20)
122
- .frame(width: 380)
46
+ .frame(width: 430)
47
+ .onAppear { service.startPermissionPolling() }
48
+ .onDisappear { service.stopPermissionPolling() }
49
+ }
50
+
51
+ private var authenticationSection: some View {
52
+ VStack(alignment: .leading, spacing: 8) {
53
+ sectionTitle("AUTHENTICATION")
54
+
55
+ HStack(spacing: 8) {
56
+ Image(systemName: service.hasPokeLoginCredentials ? "checkmark.shield.fill" : "shield.slash")
57
+ .foregroundStyle(service.hasPokeLoginCredentials ? .green : .orange)
58
+ .font(.title3)
59
+
60
+ VStack(alignment: .leading, spacing: 2) {
61
+ Text(service.hasPokeLoginCredentials ? "Signed in via Poke" : "Not signed in")
62
+ .font(.subheadline)
63
+ .fontWeight(.medium)
64
+
65
+ Text(service.hasPokeLoginCredentials ? "Your Poke session is active." : "Run this command in Terminal to sign in:")
66
+ .font(.caption)
67
+ .foregroundStyle(.secondary)
68
+ }
69
+ }
70
+ .padding(10)
71
+ .frame(maxWidth: .infinity, alignment: .leading)
72
+ .macPanelStyle(.neutral, cornerRadius: 8)
73
+
74
+ if !service.hasPokeLoginCredentials {
75
+ Button {
76
+ service.runPokeLogin()
77
+ } label: {
78
+ Label("Sign in with Poke", systemImage: "person.crop.circle.badge.plus")
79
+ }
80
+ .controlSize(.large)
81
+
82
+ Text("Opens a browser window to sign in.")
83
+ .font(.caption)
84
+ .foregroundStyle(.secondary)
85
+ }
86
+ }
87
+ }
88
+
89
+ private var accessModeSection: some View {
90
+ VStack(alignment: .leading, spacing: 10) {
91
+ sectionTitle("ACCESS MODE")
92
+
93
+ VStack(spacing: 8) {
94
+ ForEach(GateService.PermissionMode.allCases) { mode in
95
+ permissionModeRow(mode)
96
+ }
97
+ }
98
+
99
+ if service.permissionMode == .full {
100
+ AccessibilityPermissionView(service: service)
101
+ }
102
+ }
103
+ }
104
+
105
+ private func permissionModeRow(_ mode: GateService.PermissionMode) -> some View {
106
+ Button {
107
+ service.setPermissionMode(mode)
108
+ } label: {
109
+ HStack(alignment: .top, spacing: 10) {
110
+ Image(systemName: service.permissionMode == mode ? "checkmark.circle.fill" : "circle")
111
+ .font(.headline)
112
+ .foregroundStyle(service.permissionMode == mode ? .green : .secondary)
113
+
114
+ VStack(alignment: .leading, spacing: 2) {
115
+ Text(mode.title)
116
+ .font(.subheadline)
117
+ .fontWeight(.medium)
118
+ .foregroundStyle(.primary)
119
+
120
+ Text(mode.subtitle)
121
+ .font(.caption)
122
+ .foregroundStyle(.secondary)
123
+ }
124
+
125
+ Spacer()
126
+ }
127
+ .padding(10)
128
+ .frame(maxWidth: .infinity, alignment: .leading)
129
+ .macPanelStyle(service.permissionMode == mode ? .selected : .neutral)
130
+ }
131
+ .buttonStyle(.plain)
132
+ }
133
+
134
+ private var connectionSection: some View {
135
+ VStack(alignment: .leading, spacing: 8) {
136
+ sectionTitle("CONNECTION")
137
+
138
+ HStack(spacing: 8) {
139
+ Circle()
140
+ .fill(connectionColor)
141
+ .frame(width: 8, height: 8)
142
+
143
+ Text(service.status.rawValue)
144
+ .font(.subheadline)
145
+
146
+ Spacer()
147
+
148
+ Button {
149
+ service.restart()
150
+ } label: {
151
+ Label("Reconnect", systemImage: "arrow.counterclockwise")
152
+ .font(.caption)
153
+ }
154
+ }
155
+ .padding(10)
156
+ .frame(maxWidth: .infinity, alignment: .leading)
157
+ .macPanelStyle(.neutral, cornerRadius: 8)
158
+ }
159
+ }
160
+
161
+ private func sectionTitle(_ text: String) -> some View {
162
+ Text(text)
163
+ .font(.caption2)
164
+ .foregroundStyle(MacVisualStyle.sectionTitleColor)
165
+ .textCase(.uppercase)
166
+ .tracking(0.5)
123
167
  }
124
168
 
125
169
  private var connectionColor: Color {