Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9b930a3
Enable external reload (for PDF Converter)
hatton Jun 2, 2026
8e8912e
/exernal/selectBook
hatton Jun 5, 2026
fda1059
Small fix so "null" xmatter works
hatton Jun 5, 2026
57ff21e
Fix external/process-book "could not find book" on re-import
hatton Jun 8, 2026
e9e0111
Add external/process-book off-screen page fix-up (fast, shared WebVie…
hatton Jun 8, 2026
af98c65
Address review of process-book branch
hatton Jun 8, 2026
67ab5e4
Strip CKEditor from off-screen process-book pages (25.1s -> 17.7s, -29%)
hatton Jun 9, 2026
98d6782
Guard StyleEditor.AttachToBox against absent CKEditor
hatton Jun 9, 2026
4cc933b
BloomBridge external endpoints: process-book by path, collection-lang…
hatton Jun 9, 2026
f79a6e6
Merge remote-tracking branch 'origin/master' into API-for-Conversion
hatton Jun 9, 2026
d6d7f70
External endpoints: consistent -book naming; quiet process-book; opaq…
hatton Jun 10, 2026
9235e3a
review fixes
hatton Jun 10, 2026
06a014e
A pass by Fable
hatton Jun 10, 2026
108b880
Auto-fit image-over-text origami splits in process-book
hatton Jun 12, 2026
ccfae88
Fix in-flight save clobbering external reload; harden update-book flow
hatton Jun 12, 2026
3ce4040
Address external-API review comments: process-book safety, focus, ove…
hatton Jun 12, 2026
00095a9
Merge remote-tracking branch 'origin/master' into API-for-Conversion
hatton Jun 17, 2026
16f1c36
Merge remote-tracking branch 'origin/master' into API-for-Conversion
hatton Jun 18, 2026
a667da8
Address JohnThomson review comments on the external/BloomBridge API (…
hatton Jun 23, 2026
e1a1c1c
Register external/process-book with requiresSync:false (PR #7949 review)
hatton Jun 23, 2026
076daac
Add StoryWeaver eBook page size (portrait and landscape)
hatton Jun 26, 2026
302ef6f
@
hatton Jun 26, 2026
9b81b60
Potential fix for pull request finding
hatton Jun 26, 2026
d0aca61
Merge remote-tracking branch 'origin/master' into API-for-Conversion
hatton Jun 28, 2026
b5a2690
Merge remote-tracking branch 'origin/API-for-Conversion' into API-for…
hatton Jun 28, 2026
ed7b31f
Fix case-sensitive ebook 16x9 layout normalization (Devin bug)
hatton Jun 28, 2026
c613ca7
Address review: de-duplicate split-config and scroll-vs-overflow logic
hatton Jun 29, 2026
30299f0
Merge remote-tracking branch 'origin/master' into API-for-Conversion
hatton Jun 30, 2026
516570e
Update devin-review skill: post findings as resolvable review threads
hatton Jul 1, 2026
b278ba6
Raise off-screen capture delay budget from 2s to 4s
hatton Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions .claude/skills/run-bloom/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
name: run-bloom
description: Run, launch, screenshot, or drive the Bloom desktop app (Bloom.exe with embedded WebView2). Use when you need to start Bloom from this worktree, attach to a running Bloom, switch workspace tabs, take a screenshot, inspect DOM/console/network over CDP, or stop a Bloom instance.
---

# Run Bloom (desktop app)

Bloom is a C#/WinForms shell hosting a React UI in WebView2. You drive it
programmatically: launch with `./go.sh`, discover the instance's HTTP/CDP
ports, then attach over CDP. The driver scripts live in
`.github/skills/bloom-automation/` (shared with Copilot agents) plus
`screenshotBloom.mjs` in this skill directory. All paths below are relative
to the repo root; all commands are bash unless noted.

For deep detail (multi-instance rules, exe-backed Playwright tests, CDP
workflow), read `.github/skills/bloom-automation/SKILL.md`. This skill is the
verified quick path.

## Prerequisites

Dev machines already have: volta (provides node + yarn 1.22), .NET SDK 10,
WebView2 runtime. On a machine missing dependencies, run `./init.sh`
(fetches C# deps, yarn installs, initial `yarn build`) — documented but not
re-verified here; a failed C# build with CS0246 errors (missing
`PodcastUtilities` etc.) means `./init.sh` is needed.

Never run `yarn build` while a watch/dev build is running (see AGENTS.md).
Never launch a previously built `Bloom.exe` directly — it can be stale.

## Step 1: Is Bloom already running?

```bash
node .github/skills/bloom-automation/bloomProcessStatus.mjs --running-bloom --json
```

Look at `runningBloomInstances` — each entry has `httpPort`, `cdpPort`,
`processId`, `detectedRepoRoot`, `vitePort`. This discovery is HTTP-based
(scans Bloom's standard port range, asks each instance
`/bloom/api/common/instanceInfo`) and keeps working even when WMI breaks
(see Gotchas). If an instance from **this** worktree is running, reuse it
and skip to Step 3. Don't kill another worktree's Bloom without asking.

## Step 2: Launch from source

Run in a **background** task (it is a long-lived launcher — never wait for
exit):

```bash
./go.sh > /tmp/go-bloom.log 2>&1 # run_in_background
```

It starts Vite on a random port, waits for quiescence, then `dotnet watch
run` on `src/BloomExe`. Poll the log for the ready line (~2–4 min when the
C# build is warm):

```bash
until grep -qE "Bloom ready\. HTTP|exited shortly after" /tmp/go-bloom.log; do sleep 2; done
grep -E "Bloom ready\. HTTP" /tmp/go-bloom.log | tail -1
# => Bloom ready. HTTP 8092, CDP 8094, Bloom PID 51040.
```

The same info is on the machine-readable line
`BLOOM_AUTOMATION_READY {"processId":...,"httpPort":...,"cdpPort":...}`.
Use that HTTP port as the identity of your instance in every later command.
Multiple instances coexist; each takes the next port block (8089, 8092, …).

Sanity check the instance:

```bash
curl -s http://localhost:<httpPort>/bloom/api/common/instanceInfo
```

## Step 3: Drive it

Switch workspace tabs (clicks the real tab over CDP, waits for the backend
to report it active):

```bash
node .github/skills/bloom-automation/switchWorkspaceTab.mjs --http-port <httpPort> --tab edit --json
# --tab collection | edit | publish
```

Screenshot the embedded browser (lands in git-ignored `output/`):

```bash
node .claude/skills/run-bloom/screenshotBloom.mjs --http-port <httpPort> --out output/screenshots/bloom.png --json
```

For arbitrary DOM/console/network work, attach Playwright (loaded from
`src/BloomBrowserUI/react_components/component-tester`) to
`http://localhost:<cdpPort>` with `chromium.connectOverCDP` — see
`switchWorkspaceTab.mjs` and `screenshotBloom.mjs` as templates. On the Edit
tab, the page content lives inside the iframe named `page`; the top-level
document is shell UI.

## Step 4: Stop the instance you started

```bash
node .github/skills/bloom-automation/killBloomProcess.mjs --http-port <httpPort> --json
```

Check that `killedProcessIds` includes **the Bloom PID itself**, not just
its `dotnet` parents. In real runs here it was sometimes `[]` (WMI blind +
`taskkill` failing silently) and sometimes partial (parents killed, Bloom.exe
survived). For any PID still standing, fall back to PowerShell:

```powershell
Stop-Process -Id <bloomPid> -Force
```

Then stop the `./go.sh` background task itself (TaskStop or kill its shell).
This matters: after Bloom.exe dies, its `dotnet watch` chain stays alive
("Waiting for a file to change before restarting") and will **relaunch Bloom
on the next C# file edit** if left orphaned.

## Human path

`./go.sh` in a terminal; the Bloom window opens on the desktop; Ctrl+C shuts
the whole flow down.

## Gotchas (all hit in real runs)

- **WMI/wmic can go blind mid-session.** `bloomProcessStatus.mjs` (plain
mode) and `killBloomProcess.mjs` enumerate processes via `wmic`; on this
machine WMI stopped answering partway through a session — status reported
zero Bloom processes while one was demonstrably serving HTTP, and
`Get-CimInstance` hung for minutes. Trust the HTTP-based
`--running-bloom` discovery and `instanceInfo` over process enumeration.
- **`killBloomProcess.mjs` under-kills.** Observed both `killedProcessIds:
[]` for a valid target and partial kills where the `dotnet watch` parents
died but Bloom.exe survived. Always verify the port went dark
(`instanceInfo` curl fails) and the Bloom PID is gone; `Stop-Process` any
survivors.
- **Orphaned `dotnet watch` chains relaunch Bloom.** If Bloom.exe is killed
but its watcher chain survives (e.g. someone kills Blooms from Task
Manager), the watchers sit at "Waiting for a file to change" and respawn
Bloom on the next C# edit. Check with `bloomProcessStatus.mjs --json`
(`watchProcesses`) and `Stop-Process` stale ones.
- **Never type `taskkill /PID ...` in Git Bash** — MSYS rewrites `/PID` to
`C:/Program Files/Git/PID`. Use the node helpers or PowerShell.
- **Grep for `Bloom ready\. HTTP`, not `Bloom ready\.`** — early in the log
watchBloomExe prints an instructional message that *quotes* the phrase
`'Bloom ready.'`, which matches the looser pattern long before launch
completes.
- **`dotnet watch` noise:** the launch log contains scary
`⚠ msbuild: [Failure] Package 'X' was restored using .NETFramework...`
lines. They are warnings; launch still succeeds. Don't grep the log for
bare `Failure`/`error` as a failure signal — wait for `Bloom ready.` or
`exited shortly after` instead.
- **`bloomProcessStatus.mjs` may print
`Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)`** (libuv, on
exit, after the JSON is complete). Ignore it; the JSON on stdout is valid.
- **Agent shells may auto-background kill commands.** Capture the task
output file and read it; don't assume the inline result is the output.
- `body.className` of the top-level page is `developer` in dev builds;
page URL looks like `http://localhost:<port>/bloom/C%3A/...Temp/bloomXXXX.htm`.

## Tests

Exe-backed Playwright suite (not re-run this session; see
`.github/skills/bloom-automation/SKILL.md` for detail): from
`src/BloomBrowserUI/react_components/component-tester`,
`BLOOM_HTTP_PORT=<httpPort> yarn playwright test --config playwright.bloom-exe.config.ts`.
TypeScript unit tests: `yarn test` in `src/BloomBrowserUI` (Vitest).
200 changes: 200 additions & 0 deletions .claude/skills/run-bloom/screenshotBloom.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Screenshot the embedded WebView2 of a running Bloom.exe over CDP.
// Companion to the helpers in .github/skills/bloom-automation/; reuses their
// instance discovery so it targets the exact Bloom that owns a given HTTP port.
//
// Usage:
// node .claude/skills/run-bloom/screenshotBloom.mjs (--running-bloom | --http-port <port>) [--out <path>] [--json]
import { createRequire } from "node:module";
import { mkdirSync } from "node:fs";
import path from "node:path";
import {
fetchBloomInstanceInfo,
findRunningStandardBloomInstance,
getDefaultRepoRoot,
normalizeBloomInstanceInfo,
requireOptionValue,
requireTcpPortOption,
toLocalOrigin,
} from "../../../.github/skills/bloom-automation/bloomProcessCommon.mjs";

const parseArgs = () => {
const args = process.argv.slice(2);
const options = {
runningBloom: false,
httpPort: undefined,
out: undefined,
json: false,
};

for (let index = 0; index < args.length; index++) {
const arg = args[index];

if (arg === "--running-bloom") {
options.runningBloom = true;
continue;
}

if (arg === "--http-port") {
options.httpPort = requireTcpPortOption(
"--http-port",
requireOptionValue(args, index, "--http-port"),
);
index++;
continue;
}

if (arg.startsWith("--http-port=")) {
options.httpPort = requireTcpPortOption(
"--http-port",
arg.slice("--http-port=".length),
);
continue;
}

if (arg === "--out") {
options.out = requireOptionValue(args, index, "--out");
index++;
continue;
}

if (arg.startsWith("--out=")) {
options.out = arg.slice("--out=".length);
continue;
}

if (arg === "--json") {
options.json = true;
continue;
}

if (arg === "--help") {
printHelp();
process.exit(0);
}
}

return options;
};

const printHelp = () => {
console.log(
"Usage: node .claude/skills/run-bloom/screenshotBloom.mjs (--running-bloom | --http-port <port>) [--out <path>] [--json]",
);
};

const loadPlaywright = () => {
const repoRoot = getDefaultRepoRoot();
const componentTesterDir = path.join(
repoRoot,
"src",
"BloomBrowserUI",
"react_components",
"component-tester",
);
const requireFromComponentTester = createRequire(
path.join(componentTesterDir, "package.json"),
);

try {
return requireFromComponentTester("playwright");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(
`Could not load Playwright from ${componentTesterDir}. Run 'yarn install' in src/BloomBrowserUI/react_components/component-tester if dependencies are missing. Original error: ${message}`,
);
}
};

const resolveInstance = async (options) => {
if (options.httpPort) {
const response = await fetchBloomInstanceInfo(options.httpPort);
if (!response.reachable || !response.json) {
throw new Error(
`No Bloom instance reported common/instanceInfo on http://localhost:${options.httpPort}.`,
);
}

return normalizeBloomInstanceInfo(response.json, options.httpPort);
}

if (options.runningBloom) {
const instance = await findRunningStandardBloomInstance();
if (!instance) {
throw new Error(
"No running Bloom instance was found on Bloom's standard HTTP port range.",
);
}

return instance;
}

throw new Error("Specify either --running-bloom or --http-port <port>.");
};

const getBloomPage = (browser) => {
const pages = browser.contexts().flatMap((context) => context.pages());
return pages.find(
(candidate) =>
candidate.url().includes("/bloom/") &&
!candidate.url().startsWith("devtools://"),
);
};

const main = async () => {
const options = parseArgs();
const instance = await resolveInstance(options);
if (!instance.cdpPort) {
throw new Error(
"The selected Bloom instance did not report a CDP endpoint.",
);
}

const outPath = path.resolve(
getDefaultRepoRoot(),
options.out ?? path.join("output", "screenshots", "bloom.png"),
);
mkdirSync(path.dirname(outPath), { recursive: true });

const { chromium } = loadPlaywright();
const browser = await chromium.connectOverCDP(
toLocalOrigin(instance.cdpPort),
);

try {
const page = getBloomPage(browser);
if (!page) {
throw new Error(
`Could not find a Bloom WebView2 target on CDP port ${instance.cdpPort}.`,
);
}

await page.waitForLoadState("domcontentloaded");
await page.screenshot({ path: outPath });

const result = {
instance: {
processId: instance.processId,
httpPort: instance.httpPort,
cdpPort: instance.cdpPort,
},
pageUrl: page.url(),
screenshot: outPath,
};

if (options.json) {
console.log(JSON.stringify(result, null, 2));
return;
}

console.log(`Bloom HTTP port: ${result.instance.httpPort}`);
console.log(`Page URL: ${result.pageUrl}`);
console.log(`Screenshot: ${result.screenshot}`);
} finally {
await browser.close();
}
};

main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
Loading