@arach/lattices 0.2.0 → 0.6.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 (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -86
  3. package/apps/mac/Info.plist +43 -0
  4. package/apps/mac/Lattices.app/Contents/Info.plist +43 -0
  5. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  6. package/apps/mac/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  7. package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
  8. package/apps/mac/Lattices.app/Contents/Resources/tap.wav +0 -0
  9. package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +150 -0
  10. package/apps/mac/Lattices.entitlements +21 -0
  11. package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
  12. package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
  13. package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
  14. package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
  15. package/apps/mac/Resources/tap.wav +0 -0
  16. package/assets/AppIcon.icns +0 -0
  17. package/bin/assistant-intelligence.ts +912 -0
  18. package/bin/cli/capture.ts +252 -0
  19. package/bin/cli/daemon.ts +22 -0
  20. package/bin/cli/helpers.ts +105 -0
  21. package/bin/cli/layer.ts +178 -0
  22. package/bin/cli/runs.ts +43 -0
  23. package/bin/cli/search.ts +141 -0
  24. package/bin/cli/session.ts +32 -0
  25. package/bin/client.ts +17 -0
  26. package/bin/cua.ts +26 -0
  27. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  28. package/bin/handsoff-infer.ts +96 -0
  29. package/bin/handsoff-worker.ts +531 -0
  30. package/bin/infer.ts +424 -0
  31. package/bin/keychain.ts +75 -0
  32. package/bin/lattices-app.ts +655 -0
  33. package/bin/lattices-build +125 -0
  34. package/bin/lattices-build-env.ts +77 -0
  35. package/bin/lattices-dev +362 -0
  36. package/bin/lattices.ts +3260 -0
  37. package/bin/project-twin.ts +645 -0
  38. package/docs/agent-execution-plan.md +562 -0
  39. package/docs/agent-layer-guide.md +207 -0
  40. package/docs/agents.md +233 -0
  41. package/docs/ai-chat-ux-review.md +416 -0
  42. package/docs/api.md +1041 -47
  43. package/docs/app.md +96 -13
  44. package/docs/assistant-knowledge.md +130 -0
  45. package/docs/companion-deck.md +209 -0
  46. package/docs/component-extraction-roadmap.md +392 -0
  47. package/docs/concepts.md +13 -12
  48. package/docs/config.md +83 -10
  49. package/docs/gesture-customization-proposal.md +520 -0
  50. package/docs/handsoff-test-scenarios.md +84 -0
  51. package/docs/hyperspace-grid-snappiness.md +210 -0
  52. package/docs/layers.md +176 -28
  53. package/docs/mouse-gestures.md +244 -0
  54. package/docs/ocr.md +21 -9
  55. package/docs/overview.md +42 -23
  56. package/docs/presentation-execution-review.md +491 -0
  57. package/docs/prompts/hands-off-system.md +382 -0
  58. package/docs/prompts/hands-off-turn.md +30 -0
  59. package/docs/prompts/voice-advisor.md +31 -0
  60. package/docs/prompts/voice-fallback.md +23 -0
  61. package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
  62. package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
  63. package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
  64. package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
  65. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  66. package/docs/proposals/LAT-006-followup-gaps.md +103 -0
  67. package/docs/proposals/LAT-006-runs-and-capture-in-lattices.md +566 -0
  68. package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
  69. package/docs/quickstart.md +8 -12
  70. package/docs/reference/dewey.config.ts +74 -0
  71. package/docs/reference/install-agent.md +79 -0
  72. package/docs/release.md +172 -0
  73. package/docs/repo-structure.md +100 -0
  74. package/docs/terminal-kit.md +87 -0
  75. package/docs/tiling-reference.md +224 -0
  76. package/docs/twins.md +138 -0
  77. package/docs/voice-command-protocol.md +278 -0
  78. package/docs/voice-error-model.md +73 -0
  79. package/docs/voice.md +221 -0
  80. package/package.json +69 -16
  81. package/packages/npm/sdk/cua.d.mts +1 -0
  82. package/packages/npm/sdk/cua.d.ts +188 -0
  83. package/packages/npm/sdk/cua.mjs +376 -0
  84. package/app/Lattices.app/Contents/Info.plist +0 -24
  85. package/app/Package.swift +0 -13
  86. package/app/Sources/ActionRow.swift +0 -61
  87. package/app/Sources/App.swift +0 -10
  88. package/app/Sources/AppDelegate.swift +0 -234
  89. package/app/Sources/AppShellView.swift +0 -62
  90. package/app/Sources/AppTypeClassifier.swift +0 -70
  91. package/app/Sources/AppWindowShell.swift +0 -63
  92. package/app/Sources/CheatSheetHUD.swift +0 -332
  93. package/app/Sources/CommandModeState.swift +0 -1362
  94. package/app/Sources/CommandModeView.swift +0 -1405
  95. package/app/Sources/CommandModeWindow.swift +0 -192
  96. package/app/Sources/CommandPaletteView.swift +0 -307
  97. package/app/Sources/CommandPaletteWindow.swift +0 -134
  98. package/app/Sources/DaemonProtocol.swift +0 -101
  99. package/app/Sources/DaemonServer.swift +0 -414
  100. package/app/Sources/DesktopModel.swift +0 -121
  101. package/app/Sources/DesktopModelTypes.swift +0 -71
  102. package/app/Sources/DiagnosticLog.swift +0 -271
  103. package/app/Sources/EventBus.swift +0 -30
  104. package/app/Sources/HotkeyManager.swift +0 -250
  105. package/app/Sources/HotkeyStore.swift +0 -338
  106. package/app/Sources/InventoryManager.swift +0 -35
  107. package/app/Sources/InventoryPath.swift +0 -43
  108. package/app/Sources/KeyRecorderView.swift +0 -210
  109. package/app/Sources/LatticesApi.swift +0 -1125
  110. package/app/Sources/MainView.swift +0 -467
  111. package/app/Sources/MainWindow.swift +0 -83
  112. package/app/Sources/OcrModel.swift +0 -309
  113. package/app/Sources/OcrStore.swift +0 -295
  114. package/app/Sources/OmniSearchState.swift +0 -283
  115. package/app/Sources/OmniSearchView.swift +0 -288
  116. package/app/Sources/OmniSearchWindow.swift +0 -105
  117. package/app/Sources/OrphanRow.swift +0 -129
  118. package/app/Sources/PaletteCommand.swift +0 -419
  119. package/app/Sources/PermissionChecker.swift +0 -125
  120. package/app/Sources/Preferences.swift +0 -92
  121. package/app/Sources/ProcessModel.swift +0 -199
  122. package/app/Sources/ProcessQuery.swift +0 -151
  123. package/app/Sources/Project.swift +0 -28
  124. package/app/Sources/ProjectRow.swift +0 -368
  125. package/app/Sources/ProjectScanner.swift +0 -121
  126. package/app/Sources/ScreenMapState.swift +0 -2387
  127. package/app/Sources/ScreenMapView.swift +0 -2820
  128. package/app/Sources/ScreenMapWindowController.swift +0 -89
  129. package/app/Sources/SessionManager.swift +0 -72
  130. package/app/Sources/SettingsView.swift +0 -1053
  131. package/app/Sources/SettingsWindow.swift +0 -20
  132. package/app/Sources/TabGroupRow.swift +0 -178
  133. package/app/Sources/Terminal.swift +0 -259
  134. package/app/Sources/TerminalQuery.swift +0 -156
  135. package/app/Sources/TerminalSynthesizer.swift +0 -200
  136. package/app/Sources/Theme.swift +0 -163
  137. package/app/Sources/TilePickerView.swift +0 -209
  138. package/app/Sources/TmuxModel.swift +0 -53
  139. package/app/Sources/TmuxQuery.swift +0 -81
  140. package/app/Sources/WindowTiler.swift +0 -1755
  141. package/app/Sources/WorkspaceManager.swift +0 -434
  142. package/bin/lattices-app.js +0 -221
  143. package/bin/lattices.js +0 -1418
@@ -1,368 +0,0 @@
1
- import SwiftUI
2
-
3
- struct ProjectRow: View {
4
- let project: Project
5
- let onLaunch: () -> Void
6
- let onDetach: () -> Void
7
- let onKill: () -> Void
8
- let onSync: () -> Void
9
- let onRestart: (String?) -> Void
10
-
11
- @State private var isHovered = false
12
- @State private var showCoach = false
13
- @State private var showTilePicker = false
14
- @State private var contextSpaces: [SpaceInfo] = []
15
- @State private var windowInfo: WindowTiler.WindowInfo?
16
-
17
- var body: some View {
18
- VStack(spacing: 0) {
19
- HStack(spacing: 10) {
20
- // Status bar
21
- RoundedRectangle(cornerRadius: 1)
22
- .fill(project.isRunning ? Palette.running : Palette.border)
23
- .frame(width: 3, height: 32)
24
-
25
- // Info — tap to highlight window
26
- VStack(alignment: .leading, spacing: 3) {
27
- Text(project.name)
28
- .font(Typo.heading(13))
29
- .foregroundColor(Palette.text)
30
- .lineLimit(1)
31
-
32
- HStack(spacing: 6) {
33
- if !project.paneSummary.isEmpty {
34
- Text(project.paneSummary)
35
- .font(Typo.mono(10))
36
- .foregroundColor(Palette.textMuted)
37
- .lineLimit(1)
38
- } else if let cmd = project.devCommand {
39
- Text(cmd)
40
- .font(Typo.mono(10))
41
- .foregroundColor(Palette.textMuted)
42
- .lineLimit(1)
43
- }
44
-
45
- if project.isRunning, let info = windowInfo {
46
- Spacer(minLength: 4)
47
- locationBadge(info)
48
- }
49
- }
50
- }
51
- .contentShape(Rectangle())
52
- .onTapGesture {
53
- if project.isRunning {
54
- WindowTiler.highlightWindow(session: project.sessionName)
55
- }
56
- }
57
-
58
- Spacer()
59
-
60
- // Actions
61
- HStack(spacing: 4) {
62
- if project.isRunning {
63
- Button(action: {
64
- withAnimation(.easeOut(duration: 0.15)) { showTilePicker.toggle() }
65
- if !showTilePicker {
66
- // Picker just opened — highlight the window
67
- WindowTiler.highlightWindow(session: project.sessionName)
68
- } else {
69
- WindowHighlight.shared.dismiss()
70
- }
71
- }) {
72
- Image(systemName: "rectangle.split.2x1")
73
- .font(.system(size: 10))
74
- .angularButton(Palette.textDim, filled: false)
75
- }
76
- .buttonStyle(.plain)
77
-
78
- Button(action: { handleDetach() }) {
79
- Text("Detach")
80
- .angularButton(Palette.detach, filled: false)
81
- }
82
- .buttonStyle(.plain)
83
- }
84
-
85
- Button(action: onLaunch) {
86
- Text(project.isRunning ? "Attach" : "Launch")
87
- .angularButton(project.isRunning ? Palette.running : Palette.launch)
88
- }
89
- .buttonStyle(.plain)
90
- }
91
- }
92
- .padding(.horizontal, 10)
93
- .padding(.vertical, 8)
94
- .glassCard(hovered: isHovered)
95
-
96
- // Coach card
97
- if showCoach {
98
- CoachView {
99
- withAnimation(.easeOut(duration: 0.15)) { showCoach = false }
100
- }
101
- .transition(.opacity.combined(with: .move(edge: .top)))
102
- .padding(.top, 4)
103
- }
104
-
105
- // Tile picker
106
- if showTilePicker {
107
- TilePickerView(
108
- sessionName: project.sessionName,
109
- terminal: Preferences.shared.terminal,
110
- onSelect: { position in
111
- WindowHighlight.shared.dismiss()
112
- WindowTiler.tile(
113
- session: project.sessionName,
114
- terminal: Preferences.shared.terminal,
115
- to: position
116
- )
117
- withAnimation(.easeOut(duration: 0.15)) { showTilePicker = false }
118
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { refreshWindowInfo() }
119
- },
120
- onGoToSpace: { spaceId in
121
- WindowHighlight.shared.dismiss()
122
- let result = WindowTiler.moveWindowToSpace(
123
- session: project.sessionName,
124
- terminal: Preferences.shared.terminal,
125
- spaceId: spaceId
126
- )
127
- if case .success = result {
128
- WindowTiler.switchToSpace(spaceId: spaceId)
129
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
130
- WindowTiler.highlightWindow(session: project.sessionName)
131
- }
132
- } else if case .alreadyOnSpace = result {
133
- WindowTiler.switchToSpace(spaceId: spaceId)
134
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
135
- WindowTiler.highlightWindow(session: project.sessionName)
136
- }
137
- }
138
- withAnimation(.easeOut(duration: 0.15)) { showTilePicker = false }
139
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { refreshWindowInfo() }
140
- },
141
- onDismiss: {
142
- WindowHighlight.shared.dismiss()
143
- withAnimation(.easeOut(duration: 0.15)) { showTilePicker = false }
144
- }
145
- )
146
- .transition(.opacity.combined(with: .move(edge: .top)))
147
- .padding(.top, 4)
148
- }
149
- }
150
- .contentShape(Rectangle())
151
- .onHover { isHovered = $0 }
152
- .onAppear {
153
- if project.isRunning {
154
- contextSpaces = WindowTiler.getDisplaySpaces().flatMap(\.spaces)
155
- refreshWindowInfo()
156
- }
157
- }
158
- .contextMenu {
159
- if project.isRunning {
160
- Button("Attach") { onLaunch() }
161
- Button {
162
- WindowTiler.navigateToWindow(
163
- session: project.sessionName,
164
- terminal: Preferences.shared.terminal
165
- )
166
- } label: {
167
- Label("Go to Window", systemImage: "macwindow")
168
- }
169
- Button("Detach") { onDetach() }
170
- Menu("Tile Window") {
171
- ForEach(TilePosition.allCases) { tile in
172
- Button {
173
- WindowTiler.tile(
174
- session: project.sessionName,
175
- terminal: Preferences.shared.terminal,
176
- to: tile
177
- )
178
- } label: {
179
- Label(tile.label, systemImage: tile.icon)
180
- }
181
- }
182
- }
183
- if !contextSpaces.isEmpty {
184
- Menu("Go to Space") {
185
- ForEach(contextSpaces) { space in
186
- Button {
187
- let result = WindowTiler.moveWindowToSpace(
188
- session: project.sessionName,
189
- terminal: Preferences.shared.terminal,
190
- spaceId: space.id
191
- )
192
- if case .success = result {
193
- WindowTiler.switchToSpace(spaceId: space.id)
194
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
195
- WindowTiler.highlightWindow(session: project.sessionName)
196
- }
197
- }
198
- } label: {
199
- Label(
200
- "Space \(space.index)\(space.isCurrent ? " (current)" : "")",
201
- systemImage: space.isCurrent ? "desktopcomputer" : "rectangle.on.rectangle"
202
- )
203
- }
204
- }
205
- }
206
- }
207
- Divider()
208
- Button("Sync Session") { onSync() }
209
- Menu("Restart Pane") {
210
- ForEach(project.paneNames, id: \.self) { name in
211
- Button(name) { onRestart(name) }
212
- }
213
- }
214
- Divider()
215
- Button("Kill Session") { onKill() }
216
- } else {
217
- Button("Launch") { onLaunch() }
218
- }
219
- }
220
- }
221
-
222
- // MARK: - Location Badge
223
-
224
- @ViewBuilder
225
- private func locationBadge(_ info: WindowTiler.WindowInfo) -> some View {
226
- HStack(spacing: 3) {
227
- // Multi-display prefix
228
- if NSScreen.screens.count > 1 && info.displayIndex > 0 {
229
- Text("D\(info.displayIndex + 1)")
230
- .font(Typo.mono(9))
231
- .foregroundColor(Palette.textMuted)
232
- Text("\u{00B7}")
233
- .font(Typo.mono(9))
234
- .foregroundColor(Palette.textMuted)
235
- }
236
-
237
- // Space number
238
- Text(circledDigit(info.spaceIndex))
239
- .font(.system(size: 10))
240
- .foregroundColor(Palette.textDim)
241
-
242
- // Tile position icon
243
- if let tile = info.tilePosition {
244
- Image(systemName: tile.icon)
245
- .font(.system(size: 9))
246
- .foregroundColor(Palette.textMuted)
247
- }
248
- }
249
- .padding(.horizontal, 5)
250
- .padding(.vertical, 2)
251
- .background(
252
- RoundedRectangle(cornerRadius: 4)
253
- .fill(Palette.surface.opacity(0.6))
254
- )
255
- .contentShape(Rectangle())
256
- .onTapGesture {
257
- WindowTiler.navigateToWindow(
258
- session: project.sessionName,
259
- terminal: Preferences.shared.terminal
260
- )
261
- }
262
- }
263
-
264
- private func circledDigit(_ n: Int) -> String {
265
- let digits = ["\u{2776}","\u{2777}","\u{2778}","\u{2779}","\u{277A}","\u{277B}","\u{277C}","\u{277D}","\u{277E}"]
266
- return n >= 1 && n <= 9 ? digits[n - 1] : "S\(n)"
267
- }
268
-
269
- private func refreshWindowInfo() {
270
- guard project.isRunning else { windowInfo = nil; return }
271
- DispatchQueue.global(qos: .userInitiated).async {
272
- let info = WindowTiler.getWindowInfo(
273
- session: project.sessionName,
274
- terminal: Preferences.shared.terminal
275
- )
276
- DispatchQueue.main.async { windowInfo = info }
277
- }
278
- }
279
-
280
- private func handleDetach() {
281
- if Preferences.shared.mode == .learning {
282
- withAnimation(.easeOut(duration: 0.15)) { showCoach.toggle() }
283
- } else {
284
- onDetach()
285
- }
286
- }
287
- }
288
-
289
- // MARK: - Coach view
290
-
291
- struct CoachView: View {
292
- let onDismiss: () -> Void
293
-
294
- var body: some View {
295
- VStack(alignment: .leading, spacing: 10) {
296
- HStack {
297
- Text("TMUX SHORTCUTS")
298
- .font(Typo.pixel(12))
299
- .foregroundColor(Palette.running)
300
- Spacer()
301
- Button(action: onDismiss) {
302
- Image(systemName: "xmark")
303
- .font(.system(size: 8, weight: .bold))
304
- .foregroundColor(Palette.textDim)
305
- .frame(width: 18, height: 18)
306
- .background(
307
- RoundedRectangle(cornerRadius: 3)
308
- .fill(Palette.surface)
309
- )
310
- }
311
- .buttonStyle(.plain)
312
- }
313
-
314
- VStack(spacing: 6) {
315
- KeyCombo(keys: ["Ctrl+B", "D"], label: "Detach", color: Palette.detach)
316
- KeyCombo(keys: ["Ctrl+B", "X"], label: "Kill pane", color: Palette.kill)
317
- KeyCombo(keys: ["Ctrl+B", "\u{2190}\u{2192}"], label: "Switch pane", color: Palette.text)
318
- }
319
-
320
- Text("Session stays alive after detaching")
321
- .font(Typo.caption(10))
322
- .foregroundColor(Palette.textMuted)
323
- }
324
- .padding(12)
325
- .background(
326
- RoundedRectangle(cornerRadius: 6)
327
- .fill(Palette.surface)
328
- .overlay(
329
- RoundedRectangle(cornerRadius: 6)
330
- .strokeBorder(Palette.borderLit, lineWidth: 0.5)
331
- )
332
- )
333
- }
334
- }
335
-
336
- struct KeyCombo: View {
337
- let keys: [String]
338
- let label: String
339
- var color: Color = .secondary
340
-
341
- var body: some View {
342
- HStack(spacing: 6) {
343
- HStack(spacing: 3) {
344
- ForEach(keys, id: \.self) { key in
345
- Text(key)
346
- .font(Typo.geistMonoBold(10))
347
- .foregroundColor(Palette.text)
348
- .padding(.horizontal, 6)
349
- .padding(.vertical, 3)
350
- .background(
351
- RoundedRectangle(cornerRadius: 3)
352
- .fill(Palette.bg)
353
- .overlay(
354
- RoundedRectangle(cornerRadius: 3)
355
- .strokeBorder(Palette.border, lineWidth: 0.5)
356
- )
357
- )
358
- }
359
- }
360
-
361
- Text(label)
362
- .font(Typo.caption(11))
363
- .foregroundColor(color)
364
-
365
- Spacer()
366
- }
367
- }
368
- }
@@ -1,121 +0,0 @@
1
- import Foundation
2
-
3
- class ProjectScanner: ObservableObject {
4
- static let shared = ProjectScanner()
5
-
6
- @Published var projects: [Project] = []
7
-
8
- private var scanRoot: String
9
-
10
- init(root: String? = nil) {
11
- self.scanRoot = root ?? Preferences.shared.scanRoot
12
- }
13
-
14
- func updateRoot(_ root: String) {
15
- self.scanRoot = root
16
- }
17
-
18
- func scan() {
19
- // Use find to locate all .lattices.json files — no manual directory walking
20
- let task = Process()
21
- task.executableURL = URL(fileURLWithPath: "/usr/bin/find")
22
- task.arguments = [scanRoot, "-name", ".lattices.json", "-maxdepth", "3", "-not", "-path", "*/.git/*", "-not", "-path", "*/node_modules/*"]
23
- let pipe = Pipe()
24
- task.standardOutput = pipe
25
- task.standardError = FileHandle.nullDevice
26
- try? task.run()
27
- task.waitUntilExit()
28
-
29
- let data = pipe.fileHandleForReading.readDataToEndOfFile()
30
- let output = String(data: data, encoding: .utf8) ?? ""
31
- let configPaths = output.split(separator: "\n").map(String.init).filter { !$0.isEmpty }
32
-
33
- var found: [Project] = []
34
-
35
- for configPath in configPaths.sorted() {
36
- let projectPath = (configPath as NSString).deletingLastPathComponent
37
- let name = (projectPath as NSString).lastPathComponent
38
- let (devCmd, pm) = detectDevCommand(at: projectPath)
39
- let paneInfo = readPaneInfo(at: configPath)
40
-
41
- var project = Project(
42
- id: projectPath,
43
- path: projectPath,
44
- name: name,
45
- devCommand: devCmd,
46
- packageManager: pm,
47
- hasConfig: true,
48
- paneCount: paneInfo.count,
49
- paneNames: paneInfo.names,
50
- paneSummary: paneInfo.summary,
51
- isRunning: false
52
- )
53
- project.isRunning = isSessionRunning(project.sessionName)
54
- found.append(project)
55
- }
56
-
57
- DispatchQueue.main.async { self.projects = found }
58
- }
59
-
60
- func refreshStatus() {
61
- for i in projects.indices {
62
- projects[i].isRunning = isSessionRunning(projects[i].sessionName)
63
- }
64
- }
65
-
66
- // MARK: - Detection
67
-
68
- private func detectDevCommand(at path: String) -> (String?, String?) {
69
- let pkgPath = (path as NSString).appendingPathComponent("package.json")
70
- guard let data = FileManager.default.contents(atPath: pkgPath),
71
- let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
72
- let scripts = json["scripts"] as? [String: String]
73
- else { return (nil, nil) }
74
-
75
- let has = { (f: String) in
76
- FileManager.default.fileExists(atPath: (path as NSString).appendingPathComponent(f))
77
- }
78
-
79
- var pm = "npm"
80
- if has("pnpm-lock.yaml") { pm = "pnpm" }
81
- else if has("bun.lockb") || has("bun.lock") { pm = "bun" }
82
- else if has("yarn.lock") { pm = "yarn" }
83
-
84
- let run = pm == "npm" ? "npm run" : pm
85
- if scripts["dev"] != nil { return ("\(run) dev", pm) }
86
- if scripts["start"] != nil { return ("\(run) start", pm) }
87
- if scripts["serve"] != nil { return ("\(run) serve", pm) }
88
- if scripts["watch"] != nil { return ("\(run) watch", pm) }
89
- return (nil, pm)
90
- }
91
-
92
- private func readPaneInfo(at configPath: String) -> (count: Int, names: [String], summary: String) {
93
- guard let data = FileManager.default.contents(atPath: configPath),
94
- let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
95
- let panes = json["panes"] as? [[String: Any]]
96
- else { return (2, ["claude", "server"], "") }
97
-
98
- let labels = panes.compactMap { pane -> String? in
99
- if let name = pane["name"] as? String { return name }
100
- if let cmd = pane["cmd"] as? String {
101
- let parts = cmd.split(separator: " ")
102
- return parts.first.map(String.init)
103
- }
104
- return nil
105
- }
106
- return (panes.count, labels, labels.joined(separator: " · "))
107
- }
108
-
109
- private static let tmuxPath = "/opt/homebrew/bin/tmux"
110
-
111
- private func isSessionRunning(_ name: String) -> Bool {
112
- let task = Process()
113
- task.executableURL = URL(fileURLWithPath: Self.tmuxPath)
114
- task.arguments = ["has-session", "-t", name]
115
- task.standardOutput = FileHandle.nullDevice
116
- task.standardError = FileHandle.nullDevice
117
- try? task.run()
118
- task.waitUntilExit()
119
- return task.terminationStatus == 0
120
- }
121
- }