Skip to content

Commit 625c55a

Browse files
authored
Add preview_playground tool and fix build system detection (#38)
1 parent 583817d commit 625c55a

12 files changed

Lines changed: 440 additions & 130 deletions

File tree

.claude/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"mcp__previewsmcp__preview_stop",
2929
"mcp__previewsmcp__preview_elements",
3030
"mcp__previewsmcp__preview_touch",
31+
"mcp__previewsmcp__preview_playground",
3132
"mcp__previewsmcp__simulator_list"
3233
]
3334
}

.claude/skills/integration-test/SKILL.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name: integration-test
33
description: Run integration tests against example projects in the examples/ directory. Use when the user wants to validate PreviewsMCP's build system support, rendering, interaction, or hot-reload end-to-end.
44
argument-hint: [example-name]
5-
allowed-tools: Bash, Read, Glob, Grep, preview_start, preview_snapshot, preview_elements, preview_touch, preview_stop, preview_list, simulator_list
5+
allowed-tools: Bash, Read, Glob, Grep, preview_start, preview_snapshot, preview_elements, preview_touch, preview_stop, preview_list, preview_playground, simulator_list
66
---
77

88
Run integration tests for PreviewsMCP example projects.
@@ -19,7 +19,14 @@ Run integration tests for PreviewsMCP example projects.
1919

2020
3. **For each example**, read its `README.md` and follow the "Integration Test Prompt" section. The README contains the exact steps to execute, including which MCP tools to call and what to verify.
2121

22-
4. **Report results.** For each example, report pass/fail per test step. Summarize at the end.
22+
4. **Test playground.** After example tests, run the playground integration test:
23+
- Call `preview_playground` with no arguments (default code) — verify it returns a session ID and file path
24+
- Take a snapshot — verify it renders the default "Hello, playground!" view
25+
- Call `preview_playground` with custom `code` containing a simple SwiftUI view — verify it compiles and renders
26+
- Take a snapshot of the custom code session — verify the custom view appears
27+
- Stop both playground sessions
28+
29+
5. **Report results.** For each example, report pass/fail per test step. Summarize at the end.
2330

2431
## Project path guidance
2532

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,25 @@ previewsmcp list MyView.swift
3636
# Run a live preview window (macOS)
3737
previewsmcp run MyView.swift
3838

39+
# Run on iOS simulator
40+
previewsmcp run MyView.swift --platform ios-simulator
41+
3942
# Capture a screenshot
4043
previewsmcp snapshot MyView.swift -o preview.png
4144

42-
# Start the MCP server
43-
previewsmcp serve
45+
# Open a playground — creates a temp file with a starter view and live-reloads on edit
46+
previewsmcp playground
47+
previewsmcp playground --platform ios-simulator
48+
49+
# Or use an existing file
50+
previewsmcp playground MyView.swift
51+
52+
# Pipe to your editor
53+
vim $(previewsmcp playground)
4454
```
4555

56+
The `playground` command opens a live preview with hot-reload — no project setup needed. Pass an existing file or omit to create a temp one.
57+
4658
## MCP Server
4759

4860
Add to your `.mcp.json` (or Claude Code MCP config):
@@ -58,6 +70,8 @@ Add to your `.mcp.json` (or Claude Code MCP config):
5870
}
5971
```
6072

73+
Tools: `preview_list`, `preview_start`, `preview_snapshot`, `preview_elements`, `preview_touch`, `preview_stop`, `preview_playground`, `simulator_list`
74+
6175
## License
6276

6377
MIT

Sources/PreviewsCLI/BuildHelpers.swift

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import AppKit
12
import ArgumentParser
23
import Foundation
34
import PreviewsCore
45
import PreviewsIOS
6+
import PreviewsMacOS
57

68
/// Detect the build system for a source file and build it, logging progress to stderr.
79
func detectAndBuild(
@@ -28,6 +30,130 @@ func detectAndBuild(
2830
return context
2931
}
3032

33+
let defaultPlaygroundCode = """
34+
import SwiftUI
35+
36+
struct PlaygroundView: View {
37+
var body: some View {
38+
VStack {
39+
Text("Hello, playground!")
40+
.font(.title)
41+
}
42+
.padding()
43+
}
44+
}
45+
46+
#Preview {
47+
PlaygroundView()
48+
}
49+
"""
50+
51+
/// Create a temporary playground Swift file, returning its URL.
52+
func createPlaygroundFile(code: String? = nil) throws -> URL {
53+
let playgroundDir = FileManager.default.temporaryDirectory
54+
.appendingPathComponent("previewsmcp-playground", isDirectory: true)
55+
try FileManager.default.createDirectory(at: playgroundDir, withIntermediateDirectories: true)
56+
57+
let shortID = UUID().uuidString.prefix(8)
58+
let fileName = "Playground_\(shortID).swift"
59+
let fileURL = playgroundDir.appendingPathComponent(fileName)
60+
try (code ?? defaultPlaygroundCode).write(to: fileURL, atomically: true, encoding: .utf8)
61+
62+
return fileURL
63+
}
64+
65+
/// Compile and display a macOS SwiftUI preview window with file watching.
66+
func launchMacOSPreview(
67+
fileURL: URL,
68+
previewIndex: Int,
69+
title: String,
70+
width: Int,
71+
height: Int,
72+
buildContext: BuildContext?
73+
) async throws {
74+
let compiler = try await Compiler()
75+
76+
let session = PreviewSession(
77+
sourceFile: fileURL,
78+
previewIndex: previewIndex,
79+
compiler: compiler,
80+
buildContext: buildContext
81+
)
82+
83+
fputs("Compiling \(fileURL.lastPathComponent)...\n", stderr)
84+
let compileResult = try await session.compile()
85+
86+
await MainActor.run {
87+
do {
88+
try App.host.loadPreview(
89+
sessionID: session.id,
90+
dylibPath: compileResult.dylibPath,
91+
title: title,
92+
size: NSSize(width: width, height: height)
93+
)
94+
App.host.watchFile(
95+
sessionID: session.id,
96+
session: session,
97+
filePath: fileURL.path,
98+
compiler: compiler,
99+
previewIndex: previewIndex,
100+
additionalPaths: buildContext?.sourceFiles?.map(\.path) ?? [],
101+
buildContext: buildContext
102+
)
103+
fputs("Preview is live! Watching for changes...\n", stderr)
104+
} catch {
105+
fputs("Failed to load preview: \(error)\n", stderr)
106+
NSApp.terminate(nil)
107+
}
108+
}
109+
}
110+
111+
/// Launch an iOS simulator preview with file watching.
112+
func launchIOSPreview(
113+
fileURL: URL,
114+
previewIndex: Int,
115+
deviceUDID: String?,
116+
buildContext: BuildContext?
117+
) async throws {
118+
let compiler = try await Compiler(platform: .iOSSimulator)
119+
let hostBuilder = try await IOSHostBuilder()
120+
let simulatorManager = SimulatorManager()
121+
122+
let udid = try await resolveDeviceUDID(provided: deviceUDID, using: simulatorManager)
123+
124+
let session = IOSPreviewSession(
125+
sourceFile: fileURL,
126+
previewIndex: previewIndex,
127+
deviceUDID: udid,
128+
compiler: compiler,
129+
hostBuilder: hostBuilder,
130+
simulatorManager: simulatorManager,
131+
headless: true,
132+
buildContext: buildContext
133+
)
134+
135+
fputs("Launching on simulator \(udid)...\n", stderr)
136+
_ = try await session.start()
137+
fputs("Preview is live! Watching for changes...\n", stderr)
138+
139+
let allPaths = [fileURL.path] + (buildContext?.sourceFiles?.map(\.path) ?? [])
140+
let watcher = try? FileWatcher(paths: allPaths) {
141+
Task {
142+
do {
143+
let wasLiteralOnly = try await session.handleSourceChange()
144+
if wasLiteralOnly {
145+
fputs("Literal-only change applied (state preserved)\n", stderr)
146+
} else {
147+
fputs("Structural change — recompiled\n", stderr)
148+
}
149+
} catch {
150+
fputs("Reload failed: \(error)\n", stderr)
151+
}
152+
}
153+
}
154+
_ = watcher
155+
}
156+
31157
/// Resolve a simulator device UDID: provided > booted > first available.
32158
func resolveDeviceUDID(
33159
provided: String?,

0 commit comments

Comments
 (0)