Release 0.2.0 #4

Merged
dguiducci merged 96 commits from main into release 2026-08-17 18:07:20 +01:00
6 changed files with 74 additions and 1 deletions
Showing only changes of commit e29dc40202 - Show all commits
+2 -1
View File
@@ -4,7 +4,7 @@ This folder is written for **you, the assistant**, not for the human directly. I
Keep answers grounded in what's actually enabled and configured for this instance — check with the relevant tool (e.g. list installed/enabled plugins) rather than assuming everything described here is turned on. A feature documented here may not be enabled on this particular instance.
This index will grow over time. Right now it covers memory, projects, system agents, access grants and plugins; more sections (agents, connectors, security groups, shared folders…) will be added later.
This index will grow over time. Right now it covers memory, projects, system agents, access grants, voice input and plugins; more sections (agents, connectors, security groups, shared folders…) will be added later.
## Features
@@ -15,6 +15,7 @@ This index will grow over time. Right now it covers memory, projects, system age
| [system-agents.md](system-agents.md) | Background agents that run on a schedule (event triage, the two memory lints, the nightly conversation review of a supervised account): what they watch, why they only ever report, why a run can be skipped, and their settings |
| [settings.md](settings.md) | The admin's Config page: interface language, the compaction model picker, debug mode |
| [access.md](access.md) | Who can use which plugin or connector: the open default, removing access per person, and the role switch that keeps children out of it |
| [voice.md](voice.md) | Voice input: configuring a transcription model, and why the microphone button does nothing unless the page is served over HTTPS or localhost |
## Plugins
+32
View File
@@ -0,0 +1,32 @@
# Voice input (speak instead of typing)
Every chat surface — the desktop chat and the mobile one — can show a **microphone button** next to the composer. Pressing it records, pressing it again stops, and the transcribed text lands in the message box for the user to edit before sending. On the desktop chat, holding **Space** records for as long as it is held.
The button only appears when the instance has a **transcription model** configured. If a user says the microphone is missing, that is the first thing to check — it is not a permission problem.
## Configuring transcription (admin)
Sidebar → **Models****Transcription**. Add a model, pick the provider, then pick or type the model id. Two kinds of provider work:
- **A cloud provider** (OpenAI, OpenRouter, ElevenLabs…). Audio is sent to that provider for transcription, so the spoken words leave the machine — worth saying out loud to anyone who asks, and the reason the local option exists.
- **A local plugin** — [plugins/whisper_local.md](plugins/whisper_local.md) transcribes on the machine itself, nothing leaves it. A plugin-provided transcriber takes precedence over the configured cloud models.
Several models can coexist; the lowest priority number is tried first. The optional **language** hint (e.g. `it`) improves accuracy when the speaker's language is known in advance; leaving it empty lets the model detect it.
Some providers can list their transcription models for you, so the id can be picked from a dropdown; others cannot, and the form then asks for the model id to be typed by hand. Both paths end up in the same place — a provider that cannot list is not a provider that cannot transcribe.
## Why the microphone button can do nothing when pressed
This one confuses people, and the cause is the **browser**, not Skald.
Browsers only give a page access to the microphone over a **secure connection**: `https://`, or `http://localhost` (and `127.0.0.1`). Skald typically runs on a machine on the home network and is opened at an address like `http://192.168.1.50:9000` — plain `http` on a non-local address — and there the browser hides the microphone entirely. The chat says so with an error message when the button is pressed.
What to suggest, in order of how well it holds up:
1. **Open Skald at `http://localhost:9000` instead of the IP address.** Works instantly, but only on the machine Skald itself runs on.
2. **Put Skald behind HTTPS.** Configured once, on the server, and then every device works — phones included. A Tailscale mesh ([plugins/remote_connectivity.md](plugins/remote_connectivity.md)) gives a hostname and a real certificate; a reverse proxy such as Caddy is the other common route. This is the answer for anyone using Skald from more than one device.
3. **Allow the insecure address in the browser's settings.** Chrome, Edge and Brave have a flag (`chrome://flags/#unsafely-treat-insecure-origin-as-secure`) that accepts the exact origin, e.g. `http://192.168.1.50:9000`, and needs a relaunch. Firefox has `dom.securecontext.allowlist` in `about:config`, taking the bare hostname. It has to be redone on every device and every browser profile, and these flags are explicitly temporary.
**Safari has no such setting** — neither on Mac nor on iPhone/iPad. For Safari users, HTTPS is the only route. Since iPhones and iPads cannot use anything but Safari's engine, that also means voice input on a phone needs HTTPS regardless of which browser is installed.
The other messages the microphone button can produce are ordinary: access **denied** means the browser was told no for this site and the permission has to be re-allowed in its settings; **not supported** means the browser is too old or is running with media features stripped out.
+4
View File
@@ -80,6 +80,10 @@ export default {
'chat.rejected_by_user': 'Denied by user.',
'chat.truncated': 'Response truncated by the token limit (↓{tokens} tok).',
'chat.not_connected': 'Not connected — reconnecting. Your message is still in the box: press Enter again in a moment.',
'chat.mic.insecure': 'Your browser only allows the microphone over a secure connection. Open Skald at http://localhost (on the machine itself) or put it behind HTTPS.',
'chat.mic.unsupported': 'This browser does not support voice recording.',
'chat.mic.denied': 'Microphone access was denied. Allow it for this site in your browser settings, then try again.',
'chat.mic.failed': 'Could not start recording: {error}',
// ── Copilot render ─────────────────────────────────────────────────────────
'copilot.open_in_viewer': 'Open in viewer',
+4
View File
@@ -80,6 +80,10 @@ export default {
'chat.rejected_by_user': 'Refusé par l\'utilisateur.',
'chat.truncated': 'Réponse tronquée par la limite de tokens (↓{tokens} tok).',
'chat.not_connected': 'Non connecté — reconnexion en cours. Votre message est resté dans le champ : appuyez de nouveau sur Entrée dans un instant.',
'chat.mic.insecure': 'Votre navigateur n\'autorise le microphone que sur une connexion sécurisée. Ouvrez Skald sur http://localhost (depuis la machine elle-même) ou placez-le derrière HTTPS.',
'chat.mic.unsupported': 'Ce navigateur ne prend pas en charge l\'enregistrement vocal.',
'chat.mic.denied': 'Accès au microphone refusé. Autorisez-le pour ce site dans les réglages du navigateur, puis réessayez.',
'chat.mic.failed': 'Impossible de démarrer l\'enregistrement : {error}',
// ── Copilot render ─────────────────────────────────────────────────────────
'copilot.open_in_viewer': 'Ouvrir dans le visualiseur',
+4
View File
@@ -80,6 +80,10 @@ export default {
'chat.rejected_by_user': 'Negata dall\'utente.',
'chat.truncated': 'Risposta troncata dal limite di token (↓{tokens} tok).',
'chat.not_connected': 'Non connesso — riconnessione in corso. Il messaggio è rimasto nella casella: premi di nuovo Invio tra un istante.',
'chat.mic.insecure': 'Il browser consente il microfono solo su connessione sicura. Apri Skald su http://localhost (dalla macchina stessa) oppure mettilo dietro HTTPS.',
'chat.mic.unsupported': 'Questo browser non supporta la registrazione vocale.',
'chat.mic.denied': 'Accesso al microfono negato. Consentilo per questo sito nelle impostazioni del browser e riprova.',
'chat.mic.failed': 'Impossibile avviare la registrazione: {error}',
// ── Copilot render ─────────────────────────────────────────────────────────
'copilot.open_in_viewer': 'Apri nel visualizzatore',
+28
View File
@@ -1042,8 +1042,31 @@ export class ChatSession extends LightElement {
}
}
/**
* Why the microphone can be missing even when the server has a transcribe
* model: `navigator.mediaDevices` is only exposed in a **secure context**
* (HTTPS, or localhost/127.0.0.1) — over http on a LAN address the property
* is `undefined`, not a denied permission. That used to die in the `catch`
* below as a bare console line, leaving the button frozen with no
* explanation, so the unavailable cases are named here instead of guessed at.
*/
_micUnavailableReason() {
if (!navigator.mediaDevices?.getUserMedia) {
return window.isSecureContext === false
? t('chat.mic.insecure')
: t('chat.mic.unsupported');
}
if (typeof MediaRecorder === 'undefined') return t('chat.mic.unsupported');
return null;
}
async _startRecording(fromShortcut = false) {
if (this._recording) return;
const unavailable = this._micUnavailableReason();
if (unavailable) {
this._pushError(unavailable);
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this._audioChunks = [];
@@ -1072,6 +1095,11 @@ export class ChatSession extends LightElement {
this._recording = true;
} catch (err) {
console.error('mic error:', err);
this._pushError(
err?.name === 'NotAllowedError' || err?.name === 'SecurityError'
? t('chat.mic.denied')
: t('chat.mic.failed', { error: err?.message || err?.name || 'unknown' }),
);
}
}