# PhotoLM Site Skill

## Purpose

PhotoLM exposes a browser-console API for humans, browser assistants, and coding agents. The canonical surface is `window.PhotoLM`; `window.photolm` is an alias. Use it when you need to operate the live editor without clicking through the UI.

Always start with:

```js
await PhotoLM.ready()
```

`ready()` starts the editor if it is still on the landing page, waits for the internal editor bridge, and resolves when script commands can be sent safely.

## Operating Model

- Commands are serialized through one queue. Always `await` a command before sending the next one.
- `PhotoLM.run(script)` executes PhotoLM / Photoshop-style JavaScript in the editor.
- `PhotoLM.eval(expression)` evaluates an expression and returns a JSON-compatible value.
- `app.echoToOE(value)` returns text or JSON-like values to the caller.
- `app.activeDocument.saveToOE(format)` returns binary export data to the caller.
- `done` is the internal completion signal. The wrapper consumes it and resolves the Promise.
- Binary export helpers return an object with `bytes`, `blob`, `mimeType`, `byteLength`, `dataUrl()`, and `download()`.

Some features require network access, accounts, permissions, or AI credits. Examples include cloud storage, stock-image search beyond the bundled local gallery, remove background, and magic replace / inpainting.

## Quick Start

Create a document, fill it red, and export PNG:

```js
await PhotoLM.ready()
await PhotoLM.document.new({ width: 512, height: 512, name: "agent-test" })
await PhotoLM.selection.selectAll()
await PhotoLM.selection.fill({ r: 255, g: 0, b: 0 })
const png = await PhotoLM.export("png")
png.byteLength
```

Run native script directly:

```js
await PhotoLM.run(`
var c = new SolidColor();
c.rgb.red = 30;
c.rgb.green = 120;
c.rgb.blue = 220;
app.activeDocument.selection.selectAll();
app.activeDocument.selection.fill(c);
`)
```

Read state:

```js
await PhotoLM.eval("app.documents.length")
await PhotoLM.document.info()
await PhotoLM.layers.list()
```

Read the full operating guide from the live page:

```js
const skillMarkdown = await PhotoLM.skill()
```

Launch an XHR-driven agent task without touching the UI:

```js
// Loads JSON with fetch(), starts PhotoLM, runs the task, exports a hidden preview.
await PhotoLM.agent.runFromUrl("agent-tasks/swiftui-tuition-post.json")
```

Agents that cannot access page-world globals can navigate directly to:

```text
/?photolmAgentTask=agent-tasks/swiftui-tuition-post.json
```

Then inspect:

```js
document.querySelector("#photolm-agent-state").textContent
document.querySelector("#photolm-agent-export-preview")?.src
```

## API Reference

### Core

```js
PhotoLM.start()
PhotoLM.ready({ timeout })
PhotoLM.status()
PhotoLM.help()
PhotoLM.run(script, { timeout, returnAll })
PhotoLM.eval(expression, { timeout })
PhotoLM.skill()
```

`status()` reports whether the editor has started, whether it is ready, whether a command is pending, the number of received outputs, and the type of the last output.

Use `run()` for scripts with side effects, scripts that use `app.echoToOE()`, or scripts that return binary data through `saveToOE()`. Use `eval()` only for JSON-compatible expression results.

`skill()` fetches this Markdown file from the live site. Use it when a browser agent can reach `window.PhotoLM` but its external web-fetch tool could not read `/site-skill.md`.

### Agent Bridge

```js
PhotoLM.agent.state()
PhotoLM.agent.runTask(task, options)
PhotoLM.agent.runSteps(steps, options)
PhotoLM.agent.runPreset("swiftui-tuition-post", options)
PhotoLM.agent.runFromUrl("agent-tasks/swiftui-tuition-post.json", options)
PhotoLM.agent.skill()
PhotoLM.place.file(fileOrBlobOrArrayBufferOrDataUrl, options)
PhotoLM.place.dataUrl(dataUrl, options)
PhotoLM.place.url("assets/photo.png", options)
PhotoLM.layers.placeImage(fileOrBlobOrArrayBufferOrDataUrl, options)
PhotoLM.design.swiftUITuitionPost(options)
PhotoLM.design.swiftUITuitionPostSteps(options)
PhotoLM.design.swiftUITuitionPostScript(options)
PhotoLM.feedback.show()
PhotoLM.feedback.hide()
PhotoLM.feedback.move(24, 120)
PhotoLM.feedback.resetPosition()
PhotoLM.feedback.step("Adding headline")
```

Use the agent bridge when a browser automation tool cannot directly reach `window.PhotoLM` in the page's JavaScript world. `runFromUrl()` fetches JSON or text through the browser frontend, then executes it through the same serialized `PhotoLM.run()` queue.

Task JSON can contain:

```json
{
  "name": "Poster task",
  "preset": "swiftui-tuition-post",
  "options": {
    "export": true,
    "download": false,
    "preview": true,
    "format": "png"
  }
}
```

Or it can contain raw scripting:

```json
{
  "name": "Native script task",
  "script": "app.echoToOE('hello from task');",
  "export": { "format": "png", "preview": true }
}
```

For visible in-editor progress, prefer `steps` instead of one large script. Each step runs as its own queued command and then yields to the browser, so the canvas, History panel, and Layers panel can visibly update between operations:

```json
{
  "name": "Layer-by-layer poster",
  "steps": [
    {
      "label": "Create canvas",
      "kind": "document",
      "settleMs": 500,
      "script": "app.documents.add(1080, 1080, 72, 'Poster', NewDocumentMode.RGB, DocumentFill.WHITE);"
    },
    {
      "label": "Add headline",
      "kind": "text",
      "settleMs": 700,
      "script": "var l = app.activeDocument.artLayers.add(); l.kind = LayerKind.TEXT; l.textItem.contents = 'SwiftUI Tuition'; l.textItem.position = [90, 240]; l.textItem.size = 80;"
    }
  ],
  "export": { "format": "png", "preview": true }
}
```

You can also run steps directly:

```js
await PhotoLM.agent.runSteps([
  { label: "Add text layer", kind: "text", script: "var l = app.activeDocument.artLayers.add(); l.kind = LayerKind.TEXT; l.textItem.contents = 'Hello';" },
  { label: "Rename layer", kind: "layer", script: "app.activeDocument.activeLayer.name = 'Headline';" }
])
```

Steps can place uploaded or fetched image assets as layers. This is the preferred pattern when the design should visibly receive an image layer mid-flow:

```json
{
  "name": "Poster with image",
  "steps": [
    {
      "label": "Create canvas",
      "kind": "document",
      "script": "app.documents.add(1080, 1080, 72, 'Image Poster', NewDocumentMode.RGB, DocumentFill.WHITE);"
    },
    {
      "label": "Place hero image",
      "kind": "image",
      "settleMs": 800,
      "place": {
        "url": "assets/hero.png",
        "layerName": "Hero image",
        "x": 80,
        "y": 140,
        "width": 920,
        "height": 520,
        "fit": "cover"
      }
    }
  ],
  "export": { "format": "png", "preview": true }
}
```

When an agent generated an image locally, embed it as a `dataUrl` in the task JSON. This avoids CORS servers and avoids relying on PhotoLM's native URL placement path:

```json
{
  "label": "Place generated hero image",
  "kind": "image",
  "place": {
    "dataUrl": "data:image/png;base64,...",
    "layerName": "Generated hero image",
    "x": 0,
    "y": 0,
    "width": 1200,
    "height": 628,
    "fit": "cover"
  }
}
```

URL autorun parameters:

```text
?photolmAgentTask=agent-tasks/swiftui-tuition-post.json
?photolmAgentPreset=swiftui-tuition-post
?photolmAgentDownload=true
?photolmAgentExport=false
?photolmAgentTimeout=120000
```

Autorun status is written to hidden DOM for sandboxed agents:

```js
JSON.parse(document.querySelector("#photolm-agent-state").textContent)
document.documentElement.dataset.photolmAgentStatus
document.querySelector("#photolm-agent-export-preview")?.dataset.meta
```

Visual progress is written to a visible overlay at `#photolm-agent-hud`. It appears automatically during agent tasks and high-level helper calls. Agents can hide or show it:

```js
PhotoLM.feedback.show()
PhotoLM.feedback.hide()
PhotoLM.feedback.move(24, 120)
PhotoLM.feedback.resetPosition()
PhotoLM.feedback.clear()
PhotoLM.feedback.state()
```

The overlay is draggable by its header and remembers its position in `localStorage`. It defaults to the lower-left side so it does not cover the Layers panel.

For long native scripts, emit milestone feedback without polluting the command result by prefixing `app.echoToOE()` with `__photolm_feedback__`:

```js
await PhotoLM.run(`
app.echoToOE('__photolm_feedback__' + JSON.stringify({
  kind: 'text',
  label: 'Adding headline text'
}));
// add the headline layer here
app.echoToOE('__photolm_feedback__' + JSON.stringify({
  kind: 'layer',
  label: 'Building background layers'
}));
// keep designing
`)
```

Feedback messages are consumed by the wrapper, shown in the overlay, and omitted from the final `PhotoLM.run()` return value. This keeps automation results clean while giving humans watching the browser a live sense of progress.

### Open and Place

```js
await PhotoLM.open.url("https://example.com/photo.png")
await PhotoLM.place.url("https://example.com/photo.png")
await PhotoLM.place.dataUrl("data:image/png;base64,...", { layerName: "Hero" })
await PhotoLM.place.file(fileOrBlobOrArrayBuffer)
await PhotoLM.open.file(fileOrBlobOrArrayBuffer)
```

`open.url()` opens a URL as a document. `open.file()` accepts a `File`, `Blob`, `ArrayBuffer`, or typed array and opens it as a document.

`place.file()`, `place.dataUrl()`, and `place.url()` place an image into the active document as a new layer. They accept placement options:

```js
await PhotoLM.place.file(file, {
  layerName: "Hero photo",
  x: 80,
  y: 120,
  width: 920,
  height: 520,
  fit: "cover",      // contain, cover, stretch
  center: true,
  opacity: 92
})
```

`place.file()` accepts `File`, `Blob`, `ArrayBuffer`, typed arrays, or a `data:image/...` URL. By default, it sends the bytes directly into PhotoLM's active document and targets that document for insertion. The image bytes stay local to the browser; no server upload and no temporary CORS server are required.

Generated PNG/JPG/WebP/SVG/BMP/TIFF inputs are normalized through browser image decoding and re-encoded as a clean PNG before insertion. This strips metadata and encoder quirks that can make some AI-generated files fragile in native smart-object placement. Pass `{ normalize: false }` only when you deliberately need the original bytes.

By default, `place.file()`, `place.dataUrl()`, and `place.url()` use the robust `document-paste` strategy: PhotoLM opens the image as a temporary document, copies the pixels into the target document, closes the temporary source when possible, then applies the requested layer name, size, and position. This is the safest route for AI-generated raster assets and avoids blank smart-object layers.

Other strategies are available only when needed:

```js
await PhotoLM.place.url("https://example.com/photo.png", {
  strategy: "native",
  layerName: "Native placed image"
})

await PhotoLM.place.file(blob, {
  strategy: "smart-object",
  layerName: "Inserted smart object"
})
```

Agents usually cannot read arbitrary local file paths from a web page. Use one of these sources instead:

```js
// Browser-provided File, for example from a file input or drag/drop.
await PhotoLM.place.file(file, { layerName: file.name })

// Fetched asset from the current site or a CORS-enabled URL.
const blob = await fetch("assets/logo.png").then(r => r.blob())
await PhotoLM.place.file(blob, { name: "logo.png", layerName: "Logo", x: 40, y: 40, width: 160 })

// Agent-generated image encoded as a data URL.
await PhotoLM.place.dataUrl(generatedPngDataUrl, {
  layerName: "Generated hero",
  x: 0,
  y: 0,
  width: 1200,
  height: 628,
  fit: "cover"
})

// Direct URL placement when the image is already web-addressable.
await PhotoLM.place.url("assets/hero.png", { layerName: "Hero image", fit: "cover" })
```

For generated design assets, prefer this order:

1. `PhotoLM.place.file(blobOrArrayBuffer, { name, type, ... })` when the agent has bytes in the browser.
2. `PhotoLM.place.dataUrl(dataUrl, options)` when the task must travel as JSON.
3. `PhotoLM.place.url(url, options)` for web-addressable assets.
4. `PhotoLM.place.url(url, { strategy: "native" })` only when the default document-paste route cannot open the image.
5. `PhotoLM.place.file(blob, { strategy: "smart-object" })` only when you specifically need a native smart-object insertion and have verified it is not blank.

### Documents

```js
await PhotoLM.document.new({
  width: 1024,
  height: 768,
  resolution: 72,
  name: "Design",
  fill: "TRANSPARENT" // WHITE, TRANSPARENT, BACKGROUNDCOLOR
})

await PhotoLM.document.count()
await PhotoLM.document.info()
await PhotoLM.document.close("SaveOptions.DONOTSAVECHANGES")
```

### Layers

```js
await PhotoLM.layers.list()
await PhotoLM.layers.active()
await PhotoLM.layers.rename("Hero background")
await PhotoLM.layers.setVisible(false)
await PhotoLM.layers.setOpacity(65)
await PhotoLM.layers.add("New layer")
await PhotoLM.layers.setText("Updated text")
```

Layer helpers target the active document and usually the active layer. For exact control over non-active layers, use `PhotoLM.run()` with native scripting.

### History

```js
await PhotoLM.history.states()
await PhotoLM.history.undo()
await PhotoLM.history.redo()
```

`redo()` opens the History UI because native redo behavior can depend on the current history branch.

### Selection

```js
await PhotoLM.selection.selectAll()
await PhotoLM.selection.deselect()
await PhotoLM.selection.invert()
await PhotoLM.selection.clear()
await PhotoLM.selection.select([[0, 0], [400, 0], [400, 300], [0, 300]])
await PhotoLM.selection.fill({ r: 255, g: 220, b: 0 }, { opacity: 80 })
```

### Adjustments and Filters

Use convenience helpers for common actions:

```js
await PhotoLM.adjustments.brightnessContrast(20, -10)
await PhotoLM.adjustments.hueSaturation(0, 25, 0)
await PhotoLM.adjustments.desaturate()
await PhotoLM.filters.gaussianBlur(6)
```

Use generic native-method helpers for broader coverage:

```js
await PhotoLM.adjustments.apply("adjustLevels", [0, 1, 255])
await PhotoLM.filters.apply("applyGaussianBlur", [8])
```

The method name must be a simple JavaScript identifier. For complex filters, dialogs, and actions, prefer `PhotoLM.run()` with `executeAction(...)`.

### Export

```js
const png = await PhotoLM.export("png")
const jpg = await PhotoLM.export("jpg", { quality: 0.86, name: "preview.jpg" })
const psd = await PhotoLM.export("psd")

png.bytes      // Uint8Array
png.blob       // Blob
png.mimeType   // image/png
await png.dataUrl()
png.download("result.png")
```

Supported formats depend on the active document and PhotoLM's internal exporter. Common values are `png`, `jpg`, `webp`, `gif`, `svg`, `pdf`, `psd`, `tiff`, and `bmp`.

### Native UI Windows

Open built-in windows:

```js
await PhotoLM.ui.showWindow("newproject")
await PhotoLM.ui.showWindow("open_from_url")
await PhotoLM.ui.showWindow("templates")
await PhotoLM.ui.showWindow("eassets")
await PhotoLM.ui.showWindow("exportLayers")
await PhotoLM.ui.showWindow("script")
await PhotoLM.ui.showWindow("storwindow")
```

Known window IDs are available at:

```js
PhotoLM.ui.windows
```

### Templates

```js
await PhotoLM.templates.openWindow()
const allTemplates = await PhotoLM.templates.list()
const matches = await PhotoLM.templates.search("poster")
await PhotoLM.templates.open(templateId)
```

`templates.list()` reads the bundled `papi/tpls.json`. `templates.open(id)` posts the same internal message used by the Templates iframe.

### Gallery and Stock Images

```js
await PhotoLM.gallery.openWindow()
const localMatches = await PhotoLM.gallery.searchLocal("flower")
await PhotoLM.gallery.openImage(localMatches[0].largeImageURL)
```

`searchLocal()` reads the bundled `plugins/gallery.json`. Live Pixabay or Unsplash searches may require network access and service availability.

## Agent Recipes

### Inspect The Current Document

```js
await PhotoLM.ready()
const info = await PhotoLM.document.info()
const layers = await PhotoLM.layers.list()
```

### Change Text On The Active Text Layer

```js
await PhotoLM.layers.setText("New headline")
```

If the active layer is not a text layer, select a text layer in the UI or use a native script that targets the desired layer.

### Apply A Filter Then Export

```js
await PhotoLM.filters.gaussianBlur(3)
const output = await PhotoLM.export("png")
```

### Use Native Photoshop-Style Scripting

```js
await PhotoLM.run(`
var doc = app.activeDocument;
var layers = doc.layers;
for (var i = 0; i < layers.length; i++) {
  layers[i].visible = true;
}
app.echoToOE(JSON.stringify({ layerCount: layers.length }));
`)
```

### Trigger AI-Or Cloud-Dependent UI

```js
await PhotoLM.ui.showWindow("eassets")
```

AI tools and cloud storage can require online services, accounts, permissions, or credits. Prefer opening the relevant UI window when an operation needs user authorization.

### Create The SwiftUI Tuition Poster

Direct console route:

```js
await PhotoLM.ready()
const result = await PhotoLM.design.swiftUITuitionPost({
  title: "SwiftUI Tuition",
  badge: "2026 Intake",
  cta: "DM \"SWIFT\" to reserve your seat",
  export: true,
  preview: true,
  stepDelayMs: 650
})
result.export.byteLength
```

`PhotoLM.design.swiftUITuitionPost()` uses live steps by default, so the editor visibly builds the poster. Pass `{ liveSteps: false }` only when you want the older single-script behavior.

XHR route:

```js
await PhotoLM.agent.runFromUrl("agent-tasks/swiftui-tuition-post.json")
```

Navigation-only route:

```text
http://127.0.0.1:8103/?photolmAgentTask=agent-tasks/swiftui-tuition-post.json
```

## Error Handling

Wrap agent flows in `try/catch`:

```js
try {
  await PhotoLM.ready()
  await PhotoLM.document.new({ width: 800, height: 600, name: "safe-run" })
} catch (error) {
  console.error("PhotoLM automation failed:", error)
}
```

Timeouts default to 30000 ms:

```js
await PhotoLM.run("app.echoToOE('slow');", { timeout: 60000 })
```

## Constraints

- Always `await PhotoLM.ready()` before invoking editor features.
- Always `await` each command; the wrapper queues commands to protect `done`, `echoToOE`, and binary export outputs.
- Keep visual feedback enabled for live agent design sessions. Use `PhotoLM.agent.runSteps(...)` or task `steps` for actual UI repaint between major operations. Use `PhotoLM.feedback.step(...)` or feedback-prefixed `app.echoToOE(...)` only for status messages inside one command.
- Use `PhotoLM.place.file(...)` or `PhotoLM.place.dataUrl(...)` for generated or browser-available image bytes. The default `document-paste` strategy is the least fragile path for full designs.
- Use `PhotoLM.place.url(...)` when the image is already web-addressable. It uses the same document-paste placement strategy by default.
- Do not spin up a temporary CORS server just to place an agent-generated image. Convert the image to a `Blob`, `ArrayBuffer`, or `data:image/...` URL and call `PhotoLM.place.file()` / `PhotoLM.place.dataUrl()`.
- Web pages cannot directly read arbitrary local filesystem paths. The agent must provide browser-available bytes, a user-selected `File`, a same-origin URL, a CORS-readable URL, or a data URL.
- Use `PhotoLM.run()` for non-JSON script results.
- Use `PhotoLM.eval()` only when the expression can be serialized through `JSON.stringify`.
- For advanced PhotoLM features not wrapped directly, call the native scripting engine with `PhotoLM.run()` or open the native UI with `PhotoLM.ui.showWindow(id)`.
