Skip to content

Capabilities and permissions

A slop can take a photo, record audio, or turn a saved recording into text. Declare the devices and host services it needs in slop.ts; request them from a button in the editor. Declaring a capability lets the app ask. The person’s system or browser permission still decides whether it can proceed.

slop.ts
import { defineSlop } from "hitslop";
export default defineSlop({
slug: "capture-notes",
title: "Capture Notes",
description: "Keep photos, recordings, and transcripts in one document.",
author: { name: "Your Name" },
categories: ["productivity"],
window: { kind: "standard", width: 560, height: 680 },
theme: { paper: "#fff7e6", ink: "#382d24" },
capabilities: ["camera", "microphone", "speech"],
});

Use only the entries your app needs. Omitting capabilities declares none.

Capability How to use it What it admits
camera navigator.mediaDevices.getUserMedia({ video: true }) Camera capture in the interactive editor.
microphone navigator.mediaDevices.getUserMedia({ audio: true }) Microphone capture, including recording with MediaRecorder.
speech speech.transcribe(attachment, options) from hitslop/svelte On-device transcription of a recording already stored in the document.

A combined audio/video request needs both camera and microphone. Transcribing an imported recording needs speech, but not microphone. Playing music through Web Audio or an <audio> element needs neither. Start audio playback from a user gesture.

These are the three supported declaration values. File selection, attachment storage, HTTPS data and media, and embedded HTTPS frames do not need an additional capability; see files and the web. Browser policies and CORS still apply.

Environment Camera and microphone speech.transcribe
Mac app, interactive document Declared devices, subject to macOS permission. On-device recognition when this Mac supports the requested language and permission is granted.
slop dev preview Declared devices, subject to browser support and permission. Unavailable: reason: "host". Test in the Mac app.
Local Chrome browser copy Declared devices, subject to browser support and permission. Unavailable: reason: "host".
Native icon, preview, or export renderer Refused. Render saved content instead. Unavailable.

In the Mac app, a capture request must come from the current document’s main page in a visible window. Embedded web frames cannot borrow its device access. A document being closed cannot start a new device request. Saved-state rendering leaves the live editor’s device access available.

macOS permissions belong to hitSlop, not to an individual .slop file. There is no separate per-slop consent dialog or permission manager yet. The declaration is an additional restriction on which slops may request a device; it is not a per-document permission grant. To change a system decision, open System Settings → Privacy & Security → Camera, Microphone, or Speech Recognition.

Camera and microphone capture use normal browser APIs, not a hitSlop wrapper:

// Call from a Start camera button. Use { audio: true } for the microphone,
// or { video: true, audio: true } when both are declared.
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream; // a <video autoplay muted playsinline> element
await video.play();
// When Stop is pressed, the page is hidden, or the component is destroyed:
for (const track of stream.getTracks()) track.stop();
video.srcObject = null;

Check that navigator.mediaDevices?.getUserMedia exists before offering capture. Handle NotAllowedError with a permission message and NotFoundError with a missing-device message. Keep an explicit capturing state, prevent duplicate starts, and stop a stream that arrives after the person cancels or closes the UI. Never request a device on mount.

Use canvas to turn a camera frame into a Blob. Use MediaRecorder to record a stream; check MediaRecorder.isTypeSupported() before choosing a MIME type, and wait for its final dataavailable/stop events before combining chunks. Show recording state and a Stop button. Keep streams and unfinished chunks in local view state.

A finished Blob becomes durable only when imported as an attachment and referenced by the document. Wrap it in a File with an appropriate name and MIME type. Each attachment is limited to 10 MiB; a document allows 100 MiB and 256 attachments. Bound recordings by size and handle an import refusal without discarding the person’s recording.

This minimal document keeps one recording and an editable transcript:

schema.ts
import { defineDocument, s } from "hitslop";
export default defineDocument({
recording: s.optional(s.string()),
transcript: s.text(),
});

Use these functions from the editor’s Keep recording and Transcribe buttons. The file may come from a file picker or a finished MediaRecorder recording:

Put this in the editor component’s script so getDocument runs during component initialization:

import { attachments, getDocument, isSpeechError, speech } from "hitslop/svelte";
import schema from "./schema";
const doc = getDocument(schema);
async function keepRecording(file: File) {
await attachments.import<typeof schema.descriptor>(file, (tx, ref) => {
tx.fields.recording.set(ref.id);
tx.fields.transcript.set("");
});
}
async function transcribe(): Promise<string> {
const id = doc.current.recording;
if (!id) return "Keep a recording first.";
try {
const { text } = await speech.transcribe(id, { locale: "en-US" });
// Do not put an earlier recording's result on a newly selected recording.
if (doc.current.recording !== id) return "The recording changed.";
await doc.fields.transcript.set(text);
return "Transcript added.";
} catch (error) {
if (!isSpeechError(error)) throw error;
return error.message;
}
}

Show the returned message and disable Transcribe while it is running. A transcript is returned text, not an automatic edit: save it through the document handles, as above. Use attachments.url(id) for playback. The same API also accepts the attachment reference returned by attachments.import instead of its ID.

Omit locale to use the person’s default recognition language. The result includes text and the locale used. Locale separators are normalized from _ to - before the host receives the request. Recognition runs on the Mac, with no server fallback. If the language cannot be recognized on-device, hitSlop reports it as unavailable before asking for speech permission. Adding a Dictation language in System Settings may help, but availability depends on the Mac and its installed language support.

The Mac provider accepts m4a, mp3, wav, flac, mp4, and mov recordings. Other formats, including Ogg and WebM, return unavailable with reason format without asking for speech permission. They can still be stored and played where supported. Let the host report format support instead of maintaining a format list in your app. Renaming a file does not convert it: hitSlop checks its bytes. A transcript is limited to 1 MiB.

Use isSpeechError(error) rather than instanceof; the host and authored app are separate bundles.

Code Meaning and response
unavailable Inspect reason: notDeclared, host, system, locale, permission, or format. Explain the limit and keep recording/playback usable.
cancelled The page was replaced, reloaded, or closed. Keep the recording; the person can retry.
failed The recording could not be read or recognized. Preserve it and show the message.

Speech recognition is separate from microphone permission. It can fail even when audio recording works. An older host without this service returns unavailable with host.

Build a new app version to change capabilities

Section titled “Build a new app version to change capabilities”

Capabilities are part of the embedded app, not editable document data. Change slop.ts, rebuild and register the template, then create a new document to try the new declaration. Rebuilding or reinstalling the Mac app does not upgrade the app inside an existing document. See build, register, and share.

The current API stops at camera, microphone, and speech. These are directions to explore, not supported options or release commitments:

  • Permissions tied to the app’s code, with a new decision after a remix or upgrade, rather than relying only on hitSlop-wide OS consent.
  • Additional devices such as MIDI, after their declaration and permission model is designed.
  • Packaged worker and audio-worklet entry points for libraries that need them. Today, blob/data executable code remains blocked; bundle-compatible libraries are required.

Browser transcription and a newer native speech backend are not implemented. Keep a useful path without those services. The availability page separates current platform support from broader future work.