Built in mChatAI #4: Your AI made a thing. Where did it go?

Built in mChatAI #4: Your AI made a thing. Where did it go?

Generating the artifact is the easy half. September went to the other half: making the result open where you already are, and making the permission to do that honest.

Ask any chat AI for an image and you'll get one. Ask for a podcast episode, a CSV, a PDF — you'll get those too. The models are good at this now. It is genuinely not the hard part anymore.

Here's what happens next, and it's the same everywhere: you get a link. You click it. Your browser downloads a file with a name like image_final_2.png into a folder you didn't choose. Maybe Preview.app jumps to the foreground and steals your focus. You look at the thing, you close it, and three days later you cannot find it, and you cannot remember which conversation made it.

The artifact was real. The place for it wasn't.

That's the problem we spent September on, and almost none of it looks like AI work. It looks like plumbing. But it's the difference between a chat transcript and a tool.

The rule: reading a result shouldn't throw you out of the conversation

The Workbench is the room. You have a conversation on the left, and surfaces mount on the right — code, a live preview, a terminal, and a Products tab listing what got made.

The rule we committed to is narrower than "nothing ever leaves the app," and the precision matters. It's this: looking at a result should never eject you from the conversation you were having.

Leaving is still available — it's just moved to where leaving is the point. The Products tab keeps Reveal in Finder, Open in VS Code, and Open in Xcode, because when you want a generated mini-app's folder in a real editor, that is a deliberate act and Finder is the correct answer. What changed is the transcript: clicking a file in the conversation used to be an involuntary exit.

That sounds obvious stated as a principle. In code it meant deleting the easy path. Chat used to have generated-image controls that did the obvious thing:

Both now route through shared in-app viewers instead. Open mounts the file through the shared Image viewer beside the conversation. Reveal mounts the containing directory through a read-only Files viewer, with the generated file already selected — and the artifact's own viewer stays available as a neighboring tab, so you haven't lost your place by looking around.

The Files viewer isn't new code. It reuses the Harness working-folder browser and its inline file preview, because the last thing this app needs is a second file tree that drifts from the first one.

And because "we'll just be careful" is not an engineering strategy, there's now a contract lock that fails the build if MessageBubbleView ever regains a direct NSWorkspace Open/Reveal call. The convenient shortcut is not available to future us.

Verified live on a signed rebuild: a real generated file, gpt-image-2_2026-09-18T18-30-12Z.png, mounted in Chat's Image viewer. Reveal then mounted its generatedImages folder in the Files viewer with that PNG highlighted — without leaving Chat.

Five file types, one resolver

The routing above is worth nothing if it only works for images. Chat-produced files now resolve through a shared preview registry covering image, audio, video, text and PDF. Same registry backs the Workbench view, preview.show, and contextual preview.list.

One consequence is a small honesty fix I like more than it deserves: a registered viewer that fails the current session's runtime canPreview check can no longer report mounted: true. It used to be possible for the system to claim it had shown you something it hadn't. ArtifactPreview now also exposes its resolved active provider to the test oracle, so a test can't confuse "this id was requested" with "this surface actually mounted."

Two other things landed in the same pass:

Tables became objects. A fenced CSV or TSV block in a reply can be saved as a real Ledger collection — an actual product, not a code block. It previews beside Chat, opens full screen when you want it to, and the acceptance suite cleans it up afterward.

Audio got a real probe. We'd been carrying a skipped audio test, which is a polite way of saying we didn't know if it worked. Closed it with a live run: openai/tts-1 returned a non-empty MP3 through compute tokens and the file mounted in Chat's Audio viewer. While we were in there we also stopped treating Whisper as a synthesis model — it's transcription, and pretending otherwise was going to embarrass us eventually.

Model ids stopped being hardcoded. Image model ids and request shapes now come from mchatai-source/endproduct-pathways/image/models.json with a clamped bundled floor for offline use. Chat and Image Studio both generate with the content-selected gpt-image-2, and there is no production Swift dall-e-3 default left anywhere. When a provider ships a better image model, that's a content update, not a release.

The permission problem, which is the actual hard part

Mounting a surface beside your conversation is a write. Sometimes a dangerous one. So it needs consent — and consent is where tools like this usually go wrong in one of two directions.

Direction one: ask every single time. The user clicks "Allow" forty times in an afternoon, stops reading the cards entirely, and now your security theater has actively made things worse.

Direction two: ask once, vaguely, forever. The user has no idea what they agreed to.

Here's the shape we landed on.

Write- and dangerous-class surface raises mint the same orange WorkbenchPermissionRowView you already know from the rest of the app — not a bespoke Open-button consent flow that behaves subtly differently. Each decision gets a fragment-<UUID> domain id. Allow mounts the requested provider; Deny records the decision and leaves the CLI's live state alone. Identical pending raises are idempotent, so a retry can't spam you.

Then, if you explicitly grant bypass for a session, preview.show consults that grant: the surface mounts immediately, returns approvalBypassApplied: true, and creates no permission row at all. No more forty cards.

The part I actually want to point at is this:

That consent is persisted separately from the CLI's permissionMode.

Which means a bypass you chose survives relaunch — but an internal job that happens to run its CLI in bypass, like our own Harvest process, cannot manufacture your consent. Those two things look identical if you store them in one field. Keeping them apart is the whole ballgame. It's not a feature you'd ever notice working, and it's exactly the kind of thing you'd want someone to have thought about before you pointed an agent at your machine.

Small related touch: sessions created before this change display bypass (confirm) rather than silently inheriting a grant nobody gave. Diagnostics expose both the selected mode and whether the user grant is actually armed, because "the header says bypass" and "bypass is armed" turning out to be different things is precisely the bug that started this work.

There's also a boundary fix worth naming, since it cuts the other way. The reusable Harness WKWebView was carrying its development mini-app scripts into external pages opened from Workbench Preview — WKUserScript persists across navigations by default. So the preview capabilities that make sense for a mini-app you just built were also present on a real website. They aren't anymore. Capability follows origin.

The scoreboard

I try to publish these numbers rather than adjectives, because adjectives are free:

SuiteResult
UC.15 (preview fragments + approvals)18 / 0 / 0
UC.1319 / 0 / 0
UC.1411 / 0 / 0
Chat scenarios35 / 0 / 0
Image Studio4 / 0 / 0
WorkbenchSessionDriverTests18 / 18
Focused bypass contract lock1 / 1

All verified live after a normal signed rebuild and relaunch — not in a simulator, not with mocks standing in for the parts under test. The bypass grant and no-card behavior were then confirmed to survive another relaunch, because persistence claims that haven't been restarted twice aren't persistence claims.

Why this is the interesting work

The pitch for mChatAI+ is one sentence: your apps, driven by your coding agent — and every result is something you can open.

The first half of that sentence is model work, and models keep getting better at it for free. The second half is entirely ours, and none of it is glamorous: a preview registry, a viewer that can't lie about mounting, one permission card instead of two consent paths, a consent field that can't be forged by our own background jobs, a build-breaking lock on a tempting shortcut.

That's the half that decides whether you still have the thing next week.

Open the Products tab and scroll. Everything's there, and everything opens.


mChatAI+ runs on your own Mac. Your API keys live in your Keychain, results land on your disk, and the approval gates are visible. mchatai.com