Release v0.3.5
Some checks are pending
CI / cargo check (linux) (push) Waiting to run
CI / cargo check (macos) (push) Waiting to run
CI / cargo check (windows) (push) Waiting to run
CI / tsc + hotkey smoke (push) Waiting to run

This commit is contained in:
David Perov 2026-07-02 14:09:53 +03:00
parent 62352d8cb9
commit b83ea7254f
13 changed files with 139 additions and 49 deletions

View file

@ -40,6 +40,12 @@ It is designed for practical daily work: IDEs, chats, notes, CRM fields, email,
## Latest Changes
### v0.3.5
- Fixed local STT mode so managed localhost runtimes are called without a stale API bearer token and no longer show an API-key error for local runtime failures.
- Improved widget notice layout so the status icon stays visually centered next to long messages.
- Prevented call-capture startup errors from resetting the completed permissions onboarding state after restart.
### v0.3.4
- Fixed repeated permission prompts after restarting macOS: startup checks no longer trigger a microphone request, and Accessibility is not reset before re-requesting access.

View file

@ -0,0 +1,54 @@
# Release Review v0.3.5
## Release
- Version: 0.3.5
- Release branch: `release/v0.3.5`
- Target tag: `v0.3.5`
- Reviewer: Codex
- Date: 2026-07-02
## Scope
- Key changes included in this release:
- Local STT requests to localhost runtimes no longer attach the global API key as a bearer token.
- Widget error mapping now treats local STT `401/403` responses as local runtime errors instead of API-key failures.
- Widget notice icon layout uses a stable icon column so long messages stay visually aligned.
- Call-capture permission-like startup errors no longer reset the completed permissions onboarding flag.
- User-facing changes:
- Local model users should no longer see "check API key" when local runtime auth-like errors occur.
- Error notices render with a centered icon.
- Restarting after a call-capture startup failure should not reopen the permissions onboarding only because the app reset its own flag.
- Risky areas:
- Local STT authorization handling.
- Permission onboarding persistence after call-capture errors.
- Widget notice compact/expanded sizing.
## Checks run
- `bun run check:release`: passed
- `bun run build:release:macos`: local build reached `Talkis.app` and updater `.app.tar.gz`, then stopped because `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` is not present in the local environment; GitHub Actions is expected to run this step with repository secrets.
- Native/GitHub Windows build: GitHub Actions after tag
- Native/GitHub Linux build: GitHub Actions after tag
- Additional manual checks:
- `bunx tsc --noEmit`: passed before release prep
- `cargo check`: passed before release prep
## Manual review
- Hotkey flow: no hotkey reducer changes in this release.
- Onboarding permissions: reviewed `SettingsApp`, `PermissionScreen`, and widget call-capture error handling; removed app-side reset of completed onboarding from call-capture startup errors.
- Widget position and notice behavior: notice bubble positioning unchanged; notice content layout reviewed for compact and expanded message states.
- Transcription quality and short-utterance handling: no transcript filtering changes in this release.
- README refreshed: yes, `v0.3.5` added to Latest Changes.
## Findings
- Blockers: local macOS updater signing needs `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`; release publication proceeds through GitHub Actions secrets.
- Non-blocking issues: Windows/Linux artifacts are expected from GitHub Actions, not local macOS. Vite reports the existing large chunk warning during production builds.
- Follow-ups after release: verify GitHub Actions artifacts and updater metadata after tag publish.
## Decision
- Ready for `main` merge: yes
- Ready for tag publish: yes, via GitHub Actions release build

View file

@ -1,7 +1,7 @@
{
"name": "talkis",
"private": true,
"version": "0.3.4",
"version": "0.3.5",
"license": "AGPL-3.0-or-later",
"type": "module",
"packageManager": "bun@1.2.13",

View file

@ -14,10 +14,15 @@ function detectPlatform() {
const platform = process.argv[2] || process.env.TALKIS_RELEASE_PLATFORM || detectPlatform();
const bundleTargets = {
macos: ["app", "dmg"],
macos: ["app"],
windows: ["nsis"],
linux: ["appimage", "deb"],
};
const releaseOutputs = {
macos: ["app", "dmg"],
windows: bundleTargets.windows,
linux: bundleTargets.linux,
};
if (!Object.hasOwn(bundleTargets, platform)) {
throw new Error(`Unsupported release platform: ${platform}`);
@ -57,4 +62,4 @@ if (platform === "macos" && process.env.TALKIS_POSTPROCESS_MACOS_RELEASE !== "0"
run("bun", ["run", "postprocess:macos-release"]);
}
console.log(`Built ${platform} release bundles: ${bundleTargets[platform].join(", ")}`);
console.log(`Built ${platform} release bundles: ${releaseOutputs[platform].join(", ")}`);

2
src-tauri/Cargo.lock generated
View file

@ -4969,7 +4969,7 @@ dependencies = [
[[package]]
name = "talkis"
version = "0.3.4"
version = "0.3.5"
dependencies = [
"base64 0.22.1",
"block2 0.6.2",

View file

@ -1,6 +1,6 @@
[package]
name = "talkis"
version = "0.3.4"
version = "0.3.5"
description = "Talkis - Voice to Text Desktop Widget"
authors = ["trixter"]
license = "AGPL-3.0-or-later"

View file

@ -586,8 +586,13 @@ async fn transcribe_audio_bytes_internal(
let whisper_key = req
.whisper_api_key
.as_ref()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.unwrap_or(&req.api_key);
.or_else(|| {
(!is_local_endpoint)
.then_some(req.api_key.trim())
.filter(|s| !s.is_empty())
});
let stt_client = if is_local_endpoint {
reqwest::Client::builder()
@ -600,10 +605,12 @@ async fn transcribe_audio_bytes_internal(
(*client).clone()
};
let whisper_res = stt_client
.post(&whisper_url)
.bearer_auth(whisper_key)
.multipart(form)
let mut whisper_request = stt_client.post(&whisper_url).multipart(form);
if let Some(whisper_key) = whisper_key {
whisper_request = whisper_request.bearer_auth(whisper_key);
}
let whisper_res = whisper_request
.send()
.await
.map_err(|e| {

View file

@ -2,7 +2,7 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "Talkis",
"mainBinaryName": "Talkis",
"version": "0.3.4",
"version": "0.3.5",
"identifier": "com.trixter.talkis",
"build": {
"beforeDevCommand": "bun run prepare:sidecars && bun run dev",

View file

@ -128,6 +128,10 @@ export const widget = {
ru: "Не удалось запустить локальный runtime распознавания. Откройте Настройки -> Модели -> Локально и нажмите «Скачать» для нужной Whisper-модели.",
en: "Couldn't start the local recognition runtime. Open Settings -> Models -> Local and click “Download” for the Whisper model you need.",
},
"widget.error.localRuntimeRejected": {
ru: "Локальный runtime распознавания отклонил запрос. Перезапустите локальную модель или выберите её заново в Настройки -> Модели -> Локально.",
en: "The local recognition runtime rejected the request. Restart the local model or select it again in Settings -> Models -> Local.",
},
"widget.error.regionUnsupported": {
ru: "Сервис распознавания сейчас недоступен в вашем регионе. Попробуйте другой endpoint или VPN.",
en: "The recognition service is currently unavailable in your region. Try a different endpoint or a VPN.",

View file

@ -21,8 +21,6 @@ import {
getHistory,
getSettings,
reconcileInterruptedProcessing,
setPermissionsPassed,
setSystemAudioPermissionPassed,
type HistoryEntry,
} from "../../lib/store";
import {
@ -653,26 +651,6 @@ export function Widget() {
callMicPausedForVoiceRef.current = false;
if (isCallCapturePermissionError(error)) {
callSystemAudioPermissionReadyRef.current = false;
void setSystemAudioPermissionPassed(false).catch((storeError) => {
logError(
"CALL_CAPTURE",
`Failed to reset system audio permissions flag: ${
storeError instanceof Error
? storeError.message
: String(storeError)
}`,
);
});
void setPermissionsPassed(false).catch((storeError) => {
logError(
"CALL_CAPTURE",
`Failed to reset permissions flag: ${
storeError instanceof Error
? storeError.message
: String(storeError)
}`,
);
});
}
setCallError(message);
setCallSession(null);

View file

@ -2,6 +2,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react";
import type { ReactElement } from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { AlertCircle, Info } from "lucide-react";
import { NOTICE_AREA_HEIGHT, NOTICE_WIDGET_WIDTH, WIDGET_NOTICE_EVENT, type WidgetNoticeState } from "./widgetConstants";
@ -49,6 +50,8 @@ export function WidgetNoticeOverlay(): ReactElement | null {
const toggleExpanded = () => {
setExpanded((value) => !value);
};
const Icon = notice.tone === "error" ? AlertCircle : Info;
const iconColor = notice.tone === "error" ? "rgba(184,52,52,0.9)" : "rgba(0,0,0,0.58)";
return (
<div
@ -100,20 +103,36 @@ export function WidgetNoticeOverlay(): ReactElement | null {
}}
>
<div
style={
expanded
? { paddingRight: 4, whiteSpace: "pre-wrap", wordBreak: "break-word" }
: {
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
textOverflow: "ellipsis",
paddingRight: 4,
}
}
style={{
minHeight: NOTICE_AREA_HEIGHT - 20,
display: "grid",
gridTemplateColumns: "16px minmax(0, 1fr)",
alignItems: "center",
gap: 8,
}}
>
{notice.message}
<Icon
size={15}
strokeWidth={2.1}
aria-hidden="true"
style={{ flexShrink: 0, color: iconColor }}
/>
<div
style={
expanded
? { paddingRight: 4, whiteSpace: "pre-wrap", wordBreak: "break-word" }
: {
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
textOverflow: "ellipsis",
paddingRight: 4,
}
}
>
{notice.message}
</div>
</div>
</div>
</div>

View file

@ -37,9 +37,18 @@ function arrayBufferToBase64(buffer: ArrayBuffer): string {
return btoa(binary);
}
function toUserFacingErrorMessage(error: unknown): string {
function isLocalSttSettings(settings: AppSettings): boolean {
return (
settings.useOwnKey &&
settings.provider === "custom" &&
/127\.0\.0\.1|localhost/i.test(settings.whisperEndpoint || "")
);
}
function toUserFacingErrorMessage(error: unknown, settings: AppSettings): string {
const raw = formatErrorMessage(error);
const normalized = raw.toLowerCase();
const isLocalStt = isLocalSttSettings(settings);
const missingModelMatch = raw.match(/Model ['"]([^'"]+)['"] is not installed locally/i);
if (missingModelMatch) {
@ -63,6 +72,10 @@ function toUserFacingErrorMessage(error: unknown): string {
}
if (normalized.includes("403") || normalized.includes("forbidden")) {
if (isLocalStt) {
return tn("widget.error.localRuntimeRejected");
}
if (normalized.includes("subscription inactive") || normalized.includes("активная подписка") || normalized.includes("cloud mode")) {
return tn("widget.error.subscriptionRequired");
}
@ -87,6 +100,10 @@ function toUserFacingErrorMessage(error: unknown): string {
}
if (normalized.includes("401") || normalized.includes("unauthorized") || normalized.includes("invalid api key")) {
if (isLocalStt) {
return tn("widget.error.localRuntimeRejected");
}
return tn("widget.error.authFailed");
}
@ -358,7 +375,7 @@ export async function processRecordingBlob({
: String(error);
logError("API", `Pipeline raw error: ${rawErrorMessage}`);
const userFacingErrorMessage = toUserFacingErrorMessage(error);
const userFacingErrorMessage = toUserFacingErrorMessage(error, settings);
await finishProcessing({
...baseEntry,
status: "failed",
@ -449,7 +466,7 @@ export async function retryHistoryEntry(
return { hasTranscription: false, updatedEntry: interrupted };
}
const userFacingErrorMessage = toUserFacingErrorMessage(error);
const userFacingErrorMessage = toUserFacingErrorMessage(error, retrySettings);
const failedEntry: HistoryEntry = {
...entry,
status: "failed",